4. 统一响应体封装:Result<T> 泛型设计、错误码枚举、国际化消息支持
在微服务架构中,前端、网关与后端服务之间的数据交换需要一个契约一致的响应格式。如果每个接口都返回不同的 JSON 结构,客户端将不得不为每个接口编写定制的解析逻辑,导致维护成本急剧上升。本章将围绕 Result<T> 泛型响应体、ErrorCode 枚举以及国际化消息支持展开,构建一套可复用的错误处理基础设施。
4.1 为什么需要统一响应体
未统一前的典型响应混乱场景:
| 接口 | 成功响应示例 | 失败响应示例 |
|---|---|---|
/api/users |
{ "data": [...] } |
{ "error": "xxx" } |
/api/orders |
{ "code": 0, "result": {} } |
{ "message": "fail" } |
/api/products |
[{...}](直接返回数组) |
HTTP 500 + 无 body |
统一响应体带来的核心收益:
- 客户端标准化:前端/网关只需解析一种 JSON 结构,降低对接成本。
- 错误码可枚举:业务异常、系统异常、参数校验异常等都有明确的数字标识,便于监控告警。
- 国际化就绪:响应中的
message字段可根据客户端语言动态切换。 - 熔断降级兼容:当服务被熔断时,降级逻辑同样返回
Result<T>,保持链路一致性。
4.2 Result<T> 泛型设计
我们采用经典的“状态码 + 消息 + 数据”三段式结构。核心类定义如下:
public class Result<T> {
private int code; // 业务状态码,0 表示成功
private String message; // 提示消息,支持国际化 key 或直接文本
private T data; // 泛型数据体
// 私有构造器,禁止外部直接 new
private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
// ---------- 静态工厂方法 ----------
public static <T> Result<T> success(T data) {
return new Result<>(0, "success", data);
}
public static <T> Result<T> success() {
return new Result<>(0, "success", null);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
public static <T> Result<T> error(ErrorCode errorCode) {
return new Result<>(errorCode.getCode(), errorCode.getMessage(), null);
}
// getter / setter 省略
}
设计要点:
code == 0 固定表示成功,非零值对应各类错误。避免使用 HTTP 状态码直接作为业务码,因为 HTTP 状态码粒度太粗(如 400 可能包含参数错误、业务校验失败等多种场景)。
4.3 错误码枚举:ErrorCode
将所有业务错误码集中定义在枚举中,避免魔法数字散落在代码各处。枚举同时承载 code 和默认 message(英文),便于国际化时作为 fallback。
public enum ErrorCode {
// 系统级错误 (1xxxx)
SYSTEM_ERROR(10001, "System internal error"),
SERVICE_UNAVAILABLE(10002, "Service temporarily unavailable"),
RATE_LIMIT(10003, "Too many requests, please try later"),
// 参数校验错误 (2xxxx)
PARAM_INVALID(20001, "Invalid parameter"),
PARAM_MISSING(20002, "Required parameter is missing"),
// 业务错误 (3xxxx)
USER_NOT_FOUND(30001, "User not found"),
ORDER_NOT_EXIST(30002, "Order does not exist"),
INSUFFICIENT_BALANCE(30003, "Insufficient balance"),
// 熔断降级错误 (4xxxx)
CIRCUIT_BREAKER_OPEN(40001, "Circuit breaker is open, request rejected"),
FALLBACK_TRIGGERED(40002, "Fallback method executed");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
}
| 错误码区间 | 含义 | 示例 |
|---|---|---|
| 1xxxx | 系统级错误 | 数据库连接失败、服务不可用 |
| 2xxxx | 参数校验错误 | 必填字段为空、格式错误 |
| 3xxxx | 业务逻辑错误 | 用户不存在、余额不足 |
| 4xxxx | 熔断降级错误 | 断路器打开、降级方法执行 |
4.4 国际化消息支持
当服务需要面向多语言客户端时,Result.message 不应硬编码。我们通过 Spring 的 MessageSource 机制,根据客户端请求头 Accept-Language 动态解析消息。
4.4.1 消息资源文件
在 src/main/resources/i18n/ 目录下放置:
messages.properties(默认英文)messages_zh_CN.properties(简体中文)messages_ja_JP.properties(日文)
# messages.properties (默认)
error.10001=System internal error
error.20001=Invalid parameter
error.30001=User not found
error.40001=Circuit breaker is open, request rejected
# messages_zh_CN.properties
error.10001=系统内部错误
error.20001=无效的参数
error.30001=用户不存在
error.40001=断路器已打开,请求被拒绝
4.4.2 国际化工具类
@Component
public class I18nMessageUtil {
@Autowired
private MessageSource messageSource;
public String getMessage(ErrorCode errorCode, Locale locale) {
String key = "error." + errorCode.getCode();
try {
return messageSource.getMessage(key, null, errorCode.getMessage(), locale);
} catch (NoSuchMessageException e) {
// 如果找不到国际化 key,返回枚举中的默认英文消息
return errorCode.getMessage();
}
}
public String getMessage(int code, String defaultMsg, Locale locale) {
String key = "error." + code;
return messageSource.getMessage(key, null, defaultMsg, locale);
}
}
4.4.3 在全局异常处理器中集成
@RestControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private I18nMessageUtil i18nMessageUtil;
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException ex,
HttpServletRequest request) {
Locale locale = RequestContextUtils.getLocale(request);
String message = i18nMessageUtil.getMessage(ex.getErrorCode(), locale);
return Result.error(ex.getErrorCode().getCode(), message);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException ex,
HttpServletRequest request) {
Locale locale = RequestContextUtils.getLocale(request);
String message = i18nMessageUtil.getMessage(ErrorCode.PARAM_INVALID, locale);
return Result.error(ErrorCode.PARAM_INVALID.getCode(), message);
}
}
关键流程:客户端请求 → 全局异常处理器捕获异常 → 根据 ErrorCode 获取国际化 key → MessageSource 根据 Locale 解析消息 → 填充到 Result.message 返回。
4.5 完整响应链路 SVG 图
下图展示了从客户端请求到统一响应返回的完整数据流,包含异常捕获、国际化解析与熔断降级场景。
4.6 熔断降级场景下的响应一致性
当服务发生熔断时,降级方法(Fallback)必须返回与正常接口相同的 Result<T> 类型,否则客户端无法统一处理。以 Spring Cloud OpenFeign + Sentinel 为例:
@FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/users/{id}")
Result<UserDTO> getUserById(@PathVariable Long id);
}
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return id -> Result.error(ErrorCode.CIRCUIT_BREAKER_OPEN);
}
}
避坑提示:降级方法中不要返回
null 或抛出未捕获异常。始终返回 Result.error(),并携带明确的熔断错误码(如 40001),以便客户端识别并展示降级提示。
4.7 本章小结
- Result<T> 泛型响应体统一了成功与失败的数据结构,
code、message、data三段式设计覆盖所有场景。 - ErrorCode 枚举 集中管理业务错误码,按区间划分(系统/参数/业务/熔断),避免魔法数字。
- 国际化消息 通过 Spring MessageSource 实现,根据客户端 Locale 动态切换提示文本,降级时同样生效。
- 熔断降级场景 必须保持响应体格式一致,降级方法返回
Result.error()而非null。