跳到主要内容

统一异常处理与 API 错误契约

API 错误响应要让客户端稳定判断失败类型,同时为服务端保留可追踪证据。Spring MVC 可以用 @RestControllerAdvice@ExceptionHandler 集中映射异常,但异常分类与 HTTP 语义仍需业务定义。

1. 先定义稳定的错误字段

Spring 提供 ProblemDetail 表示 HTTP API 问题,可以扩展业务字段:

{
"type": "https://coding101.dev/problems/order-state",
"title": "Order state conflict",
"status": 409,
"detail": "The order has already been paid",
"instance": "/orders/101",
"code": "ORDER_ALREADY_PAID",
"traceId": "..."
}

客户端依赖稳定的 code、HTTP status 和字段错误结构,不应解析面向人的 detail 文本。

2. 异常处理器负责映射

@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(OrderStateException.class)
ResponseEntity<ProblemDetail> handle(OrderStateException exception) {
ProblemDetail problem = ProblemDetail.forStatus(409);
problem.setTitle("Order state conflict");
problem.setProperty("code", "ORDER_STATE_CONFLICT");
return ResponseEntity.status(409).body(problem);
}
}

处理器选择遵循异常类型和 ControllerAdvice 范围。业务异常、输入错误、认证授权、依赖失败与未知错误应分别映射,不能所有失败都返回 HTTP 200 或 500。

3. 状态码表达协议结果

常见判断:

  • 400:请求结构或参数无法接受。
  • 401:缺少或无效身份认证。
  • 403:身份已知但无权执行。
  • 404:目标资源在当前可见范围不存在。
  • 409:当前资源状态与操作冲突。
  • 429:超过容量或速率限制。
  • 500:服务端未预期错误。
  • 502/503/504:网关或依赖可用性与超时。

是否允许重试还要结合方法幂等性和响应头,不能只看 5xx。

4. 日志与响应面向不同读者

响应不应包含堆栈、SQL、文件路径、密钥或内部类名。服务端日志保留异常 cause、trace ID、规范化路由和必要业务标识,并对敏感数据脱敏。

预期业务冲突通常不需要每次打印完整 ERROR 堆栈;未知异常应记录一次完整根因。Controller 和 Advice 同时记录会造成重复日志。

5. 并非所有异常都能由 ControllerAdvice 处理

Filter 在 DispatcherServlet 之前抛出的异常、响应已经提交后的序列化错误、异步后台任务失败,可能不进入同一 @ExceptionHandler。安全过滤链、Servlet error dispatch 和异步结果各有自己的错误边界。

客户端断开连接也可能在写响应时产生 I/O 异常,应单独统计,避免都归类为应用 500。

6. 校验错误需要字段级结构

{
"code": "VALIDATION_FAILED",
"errors": [
{"field": "quantity", "code": "Positive"}
]
}

不要回显密码等拒绝值。字段路径要稳定,数组和嵌套对象需约定表示方式;面向用户的本地化文本可以由客户端或服务端另行生成。

7. 契约测试覆盖异常路径

为每类公开错误验证 status、Content-Type、code、字段结构和敏感信息缺失。还要测试不存在路由、错误方法、不可接受媒体类型、Filter 拒绝和下游超时,确保不同入口不产生几套互不兼容格式。

8. 常见问题

8.1 是否应该把所有响应都包装成 {code, message, data}

不是必需。HTTP 已提供状态和头,成功响应可以直接返回资源;关键是错误结构稳定。团队若统一包装,也要避免 HTTP 永远 200 导致代理、监控和客户端语义失真。

8.2 捕获 Exception 作为最后兜底可以吗

可以在边界脱敏并记录未知错误,但应放在更具体处理器之后,不能吞掉已提交响应、取消信号或把所有错误误报为同一业务失败。

9. 面试题

9.1 Spring MVC 怎样实现统一异常处理

出现公司:CVTE、珍爱网

考察重点

  • ControllerAdvice、ExceptionHandler 与 resolver 链。
  • HTTP 状态和稳定业务错误码。
  • Filter、异步和响应提交边界。

相关内容:第 1 节“先定义稳定的错误字段”至第 7 节“契约测试覆盖异常路径”。

参考回答

使用 @RestControllerAdvice 集中声明 @ExceptionHandler,把业务冲突、校验、认证授权、依赖失败和未知异常分别映射为合适的 HTTP 状态及稳定错误码。可以使用 Spring 的 ProblemDetail,并附 trace ID;响应不暴露堆栈和内部信息。

ControllerAdvice 参与 MVC 的 HandlerExceptionResolver 链,Filter 自身异常、异步后台任务和响应提交后的错误未必在其范围内。要为这些边界分别处理,并用契约测试覆盖状态、媒体类型和敏感字段。