在一个团队协作的项目中,工程规范的价值远高于个人英雄主义。统一返回结果、统一异常处理、统一日志体系,这三项规范直接决定了前端对接体验、问题排查效率和系统可观测性。它们不是可选项,而是后端服务必须第一时间建立的基础设施。
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 在日志中心一键搜索,整个调用链的上下文一目了然。
建立这些规范需要一点前期投入,但它是保证一个多人协作、多服务交互系统能够平稳运行的底线工程。一旦形成共识并固化为脚手架或公共库,每一个新加入的模块都不再需要为此操心,团队的开发效率和线上诊断能力会进入一个全新阶段。