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 图

下图展示了从客户端请求到统一响应返回的完整数据流,包含异常捕获、国际化解析与熔断降级场景。

统一响应体处理链路 统一响应体 Result<T> 处理链路 客户端请求 Controller Service 业务逻辑 异常抛出 全局异常处理器 I18n 消息解析 Result.error 封装 统一 JSON 响应返回 说明 正常流程:Controller → Service 异常流程:Service → 异常处理器 → I18n → Result.error → 返回

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。