人人都会AI编程

18.5 通用规范:统一返回结果、统一异常处理、统一日志体系

更新时间:2026-07-10

在一个团队协作的项目中,工程规范的价值远高于个人英雄主义。统一返回结果、统一异常处理、统一日志体系,这三项规范直接决定了前端对接体验、问题排查效率和系统可观测性。它们不是可选项,而是后端服务必须第一时间建立的基础设施

18.5.1 统一返回结果

API 响应如果不加约束,各个接口返回的结构会五花八门:有的返回对象本身,有的手动拼接 Map,有的成功时返回 200、有的返回 0。前端需要针对不同接口编写不同的解析逻辑,对接成本急剧攀升。

1. 定义通用的响应体

一个清晰的返回结构至少应该包含三个字段:状态码(code)、消息(message)、数据(data)。建议将响应体封装成不可变类,并提供一系列静态工厂方法,杜绝随意 new 对象导致的不一致。

public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    // 私有构造,强制通过静态方法创建
    private ApiResponse(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // 成功时的快捷构造
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }

    public static <T> ApiResponse<T> success() {
        return success(null);
    }

    // 失败时的快捷构造
    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }

    // getters ...
}

所有 Controller 方法统一返回 ApiResponse 包裹的结果,前端只需要判断 code 是否为 200,即可统一处理成功或失败的后续逻辑。

2. 分页结果的特殊处理

分页查询往往需要额外的总条数、页码等信息。建议在 ApiResponse 的基础上扩展一个 PageInfo 结构,或者使用 ApiResponse<PageResult<T>> 的方式,但必须保持外层结构一致,以免破坏前端的拦截器逻辑。

public class PageResult<T> {
    private List<T> records;
    private long total;
    private int pageNo;
    private int pageSize;
    // 构造、getters ...
}

Controller 中返回 ApiResponse<PageResult<OrderVO>>,外层 code/message 不变,data 内嵌套分页相关字段。

18.5.2 统一异常处理

返回体的统一只是一半工作,更重要的是一致地处理异常。线上故障发生时,最怕看到的是 Whitelabel Error Page 或一堆堆栈信息直接返回给客户端。统一异常处理的目标是:任何未捕获的异常最终都被转换为标准化的 ApiResponse 输出,同时将原始异常记录到日志中

1. 定义业务异常类

区分业务异常与系统异常十分必要。业务异常是已知的、可识别的错误,例如“订单不存在”“库存不足”等,它们应该返回特定的业务状态码和友好提示。系统异常则是意料之外的错误(如空指针、数据库连接失败),应统一返回“系统繁忙”并触发告警。

建议创建一个 BusinessException,携带错误码和消息:

public class BusinessException extends RuntimeException {
    private int code;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}

业务代码中遇到不符合前置条件的情况时,直接抛出 BusinessException,而不要手动构造 ApiResponse 返回,保持 Controller 层干净。

2. 全局异常拦截器

Spring 提供 @RestControllerAdvice 配合 @ExceptionHandler 实现全局异常捕获。在这个切面里,你可以针对不同类型的异常编写不同的处理方法,但最终都返回 ApiResponse

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<?> handleBusinessException(BusinessException e) {
        return ApiResponse.error(e.getCode(), e.getMessage());
    }

    // 处理参数校验异常(如@Validated抛出的异常)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<?> handleValidationException(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors()
                .stream()
                .map(f -> f.getField() + ": " + f.getDefaultMessage())
                .collect(Collectors.joining("; "));
        return ApiResponse.error(400, msg);
    }

    // 兜底处理未知异常
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleException(Exception e) {
        // 记录详细的堆栈日志
        log.error("未预期的异常", e);
        return ApiResponse.error(500, "系统繁忙,请稍后再试");
    }
}

这样做的好处是,Controller 内不再需要 try-catch 包裹,任何异常都会被全局处理,返回格式始终是 ApiResponse

3. 异常的合理分级

业务异常建议细分为不同的 code 段,如 1xxx 代表用户相关错误、2xxx 代表订单错误、3xxx 代表支付错误等。前端可根据 code 进行精细化提示或跳转。系统异常统一使用 500,敏感信息绝不暴露给调用方。

18.5.3 统一日志体系

日志是生产环境排查问题的眼睛。日志不规范,出故障时就是睁眼瞎。统一日志体系需要从格式、级别、埋点、输出四个维度建立标准。

1. 日志格式统一

所有服务应该输出结构一致、易于被日志平台(如 ELK)解析的日志。推荐使用 JSON 格式,或至少保证每一行日志都包含时间戳、日志级别、线程、TraceId、类名和消息。利用 Logback 或 Log4j2 的 Pattern 可以轻松实现:

%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId}] - %msg%n

这里 %X{traceId} 依赖 MDC(Mapped Diagnostic Context),是分布式追踪的基石。在网关或拦截器中生成并设置 TraceId,全链路日志即可串联。

2. 日志级别与输出规范

  • ERROR:影响功能正常运行、需要立刻响应的错误。比如数据库连接失败、RPC 调用异常、业务关键流程中断。务必在 GlobalExceptionHandler 中对未知异常记录 error 日志,并上报监控告警。
  • WARN:潜在风险或降级行为。如使用了不推荐的配置、触发限流、重试成功但消耗额外时间等,这些信息虽不致命但值得关注。
  • INFO:关键业务流程节点。在 Controller 层记录入参出参(注意脱敏),在 Service 层记录状态变更(如订单已支付、库存已扣减),便于回溯业务轨迹。切记 INFO 不是打点日志的杂物箱,要避免在循环中大量输出。
  • DEBUG:开发或调试阶段使用的详细信息,生产环境默认关闭。

3. 埋点与日志内容

日志不是写散文,要具备可检索性。推荐采用键值对或者结构化日志:

log.info("订单支付成功 orderId={}, payAmount={}, payChannel={}", 
         order.getId(), order.getPayAmount(), order.getChannel());

避免拼接字符串,既影响性能又不利于检索。涉及敏感信息(手机号、身份证、密码)时,必须脱敏处理,常用脱敏工具或注解实现。在服务切面上,通过 AOP 可以统一记录方法调用的耗时、参数、返回值(针对慢查询或关键交易),但要注意控制日志量。

4. 日志文件与收集

生产环境禁止将日志输出到控制台然后消失,要配置 Rolling File Appender,按天或按大小切分,保留一定天数。所有服务的日志最终应汇集到统一的日志中心(如 ElasticSearch + Kibana),配合 TraceId 实现全链路追踪。Spring Boot 默认集成 Logback,只需在 logback-spring.xml 中配置 Appender 与 Profile 区分即可。

18.5.4 三者协同的威力

这三个规范不是孤立存在的:统一异常处理依赖统一返回结构,将异常转化为标准 ApiResponse;统一日志体系则记录异常详情和业务关键节点,并携带统一返回中的状态码等信息。当一条请求到达时,网关生成 TraceId 并置入 MDC,Controller 记录请求日志,Service 抛出 BusinessException,全局异常处理器捕获并返回 ApiResponse,同时 ERROR 日志中包含了 TraceId 和完整堆栈。运维人员拿着 TraceId 在日志中心一键搜索,整个调用链的上下文一目了然。

建立这些规范需要一点前期投入,但它是保证一个多人协作、多服务交互系统能够平稳运行的底线工程。一旦形成共识并固化为脚手架或公共库,每一个新加入的模块都不再需要为此操心,团队的开发效率和线上诊断能力会进入一个全新阶段。