跳到主要内容
约 7 分钟2 张卡

API 错误契约

API 错误契约规定错误响应的状态和字段,让客户端稳定判断失败类型。本文从错误字段入手,介绍 Spring MVC 怎样映射异常,再说明 HTTP 状态、日志和异常处理的范围。最后用契约测试检查不同错误入口是否遵守同一约定。文中的 ProblemDetail 示例适用于 Spring 6,也就是 Boot 3.x 使用的框架版本。

1. 面试核心​

先记住这几条,再读正文
  • 异常映射。 @RestControllerAdvice 配合 @ExceptionHandler,通过 MVC 异常解析链将异常集中映射为错误响应。卡 · 错误字段
  • 错误响应字段。 Spring 6 的 ProblemDetail 提供标准问题字段,并支持业务错误码和 trace ID 等扩展字段。
  • 处理器匹配。 同一 Controller 或 Advice 内,Spring MVC 优先匹配顶层异常,再按继承距离选择更具体的类型。卡 · 异常处理器选择
  • 异步异常。 通过 MVC 异步派发返回的异常,仍能交给 @ExceptionHandler 处理。
  • 响应提交边界。 响应已提交后,错误处理器无法重新设置完整响应。

2. 错误字段​

错误响应需要同时服务于程序判断和用户展示。Spring 6 的 ProblemDetail 用来表示 HTTP API 中的问题,本文按 RFC 9457 说明其五个标准字段:

  • type 用 URI 标识问题类型。
  • title 提供该问题类型的简短描述。
  • status 记录本次响应的 HTTP 状态码。
  • detail 解释本次失败的具体情况。
  • instance 用 URI 标识这一次问题出现的位置或实例。

下面的响应表示订单状态冲突,并扩展业务错误码和请求关联标识:

{
"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 和 traceId 是业务扩展字段。traceId 用于关联同一次请求的服务端日志,便于根据报错追踪原因。客户端按 HTTP 状态、type 或稳定错误码选择处理分支,字段校验错误也要采用稳定结构。detail 用于展示,内容可随语言变化,因此应与程序判断分开。

响应使用 application/problem+json。正文的 status 应与实际 HTTP 状态一致。Spring MVC 的消息转换器支持该格式。

3. 异常处理器​

异常处理器把 Java 异常转换成客户端能理解的响应。@RestControllerAdvice 集中声明处理方法,@ExceptionHandler 指定方法接收哪些异常。

@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);
}
}

示例把订单状态冲突映射为 HTTP 409,并返回稳定的 code 字段。异常类型承载失败分类,HTTP 状态告诉客户端和中间代理该怎样理解结果。

应用应分别映射五类失败:

  • 输入错误说明请求结构或参数需要修改。
  • 认证授权错误说明身份凭证或访问权限有问题。
  • 业务冲突说明当前资源状态无法接受该操作。
  • 依赖失败说明外部服务调用未能完成。
  • 未知异常返回 500,并在服务端日志中保留根因。

这样,客户端才能区分需要修改请求、重新认证或稍后重试的情况。

ResponseEntityExceptionHandler 提供 MVC 标准异常处理基类。继承它可统一绑定错误、媒体类型错误等响应。

4. 状态码​

选择状态码时,要表达当前请求为何失败。下表区分了客户端输入、业务状态和服务端处理中的常见问题。

状态语义
400请求语法、参数或校验错误
401缺少有效认证凭证;需返回 WWW-Authenticate
403服务端理解请求,但拒绝执行
404资源不存在,或服务端拒绝披露其存在
409操作与资源当前状态冲突
429请求频率超过限制
500服务端未预期错误
502网关收到无效的上游响应
503服务暂时无法处理请求
504网关等待上游响应超时

客户端决定重试前,还要检查幂等性,也就是重复请求是否会产生额外副作用。响应中的 Retry-After 可指示等待多久后再尝试,因此仅凭 5xx 无法决定是否安全重试。

依赖失败要结合当前服务的角色映射。例如 502 和 504 表达网关与上游之间的问题,普通业务服务应按实际失败原因选择状态。

5. 日志与响应​

错误响应提供客户端处理失败所需的信息,服务端日志则保留排查依据。响应只包含公开字段,堆栈、SQL、文件路径和内部类名留在受控诊断范围,密钥等敏感值应脱敏。

日志应记录 cause,也就是原始异常,以及 trace ID 和必要业务标识。路由可记录为 /orders/{id} 这类规范化模板,便于聚合同类请求。日志中的用户数据和敏感参数同样要脱敏。

预期业务冲突可记录简要信息,未知异常则在处理边界记录一次完整堆栈。若 Controller 和 Advice 都记录同一异常,会产生重复日志,增加定位成本。

6. ControllerAdvice 的处理范围​

判断异常能否统一处理,要先看它从哪里抛出。ControllerAdvice 参与 DispatcherServlet 的异常解析链。链中的 HandlerExceptionResolver 负责寻找合适的方式,把异常转换成响应。

需要区分以下三种异常路径:

  • Filter 在进入 MVC 前抛出的异常,由过滤链或 Servlet 容器处理。安全过滤链可使用自己的错误处理器,容器也可发起 error dispatch,即错误派发。
  • MVC 的 Callable 抛错后,会触发异步派发,重新进入 MVC 处理流程。DeferredResult.setErrorResult(exception) 也会把异常交给 MVC resolver。
  • 独立后台任务没有对应的 MVC 请求派发,其失败由任务结果或任务异常处理器承接。

响应提交后,状态和已写出的正文已经交给客户端,错误处理器无法重新设置完整响应。这时应记录序列化或写出失败,再按连接状态结束处理。客户端断开造成的 I/O 错误应单独统计,避免全部计为应用 500。

7. 字段校验错误​

客户端要把校验错误显示在对应输入框旁,就需要稳定的字段路径和错误码。下面的响应表示 quantity 未通过正数校验:

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

示例用 field 定位字段,用 code 表达校验规则。数组和嵌套对象也要约定路径表示方式,客户端才能找到对应输入项。响应省略密码等敏感拒绝值,展示文本则可由客户端或服务端按语言生成。这样,调整提示文案就不会改变稳定错误码。

8. 契约测试​

契约测试用于确认每个入口都返回约定的错误格式。对每类公开错误,检查 HTTP 状态、Content-Type、业务 code 和字段结构。再用敏感值作为输入,确认响应已省略这些值。

测试应覆盖以下错误入口与边界:

  • 路由不存在、请求方法错误和不可接受的媒体类型。
  • 参数校验失败和 Filter 拒绝请求。
  • MVC 异步异常与下游调用超时。
  • 序列化或响应写出失败,以及响应已经提交的情况。

这些用例能发现不同入口是否产生了互不兼容的格式。响应已提交的用例则检查记录和结束处理的行为。

9. 异常处理器选择​

多个方法都能处理同一个异常时,需要按查找范围和优先级判断。Controller 内的局部 @ExceptionHandler 优先于全局 Advice。多个 Advice 按 Ordered 或 @Order 排序。较小的值具有较高优先级。

同一 Controller 或 Advice 内,先匹配顶层异常。多个类型匹配时,选择继承距离更近的类型。顶层无匹配时再查 cause 链,也就是逐层查找包装在内部的原始异常。Spring 5.3 起支持任意深度的 cause,因此 Boot 2.7 和 3.x 都支持这条规则。

跨 Advice 时,优先级先决定查找顺序。高优先级 Advice 的 cause 匹配可先于低优先级的顶层匹配。所以,应将 Exception 兜底放在低优先级 Advice,避免提前截获本应由具体处理器处理的错误。方法在源码中的书写顺序不参与选择。

10. 常见问题​

10.1 所有响应都要使用统一包装吗?​

成功响应可以直接返回资源,错误响应保持稳定结构即可。团队使用统一包装时,也要保留正确的 HTTP 状态。如果所有结果都返回 200,代理、监控和客户端就会把失败当作成功,影响告警与重试判断。

10.2 可以捕获 Exception 作为最后兜底吗?​

11 · 面试题卡片​

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

出现公司:CVTE、珍爱网

考察重点:解释异常如何经过 resolver 链映射为 HTTP 状态和稳定错误字段,并区分 Filter、异步和响应提交的边界。

相关内容:错误字段至契约测试。

参考回答

使用 @RestControllerAdvice 集中声明 @ExceptionHandler,按异常类型生成统一错误响应。

业务冲突、校验、认证授权、依赖失败和未知异常分别映射到合适状态。Spring 6 可返回 ProblemDetail,再用 ResponseEntity 明确 HTTP 状态。响应附带稳定错误码和 trace ID,完整根因保留在脱敏后的服务端日志中。

这些处理器参与 MVC 的异常解析链。MVC 异步派发中的异常仍可进入该链,Filter 自身异常则交给过滤链或容器。独立后台任务由任务自己的机制承接失败。响应已提交后,应记录失败并结束当前写出。最后用契约测试检查状态、媒体类型、字段和敏感信息是否符合约定。

11.2 多个异常处理器都能匹配时,Spring MVC 怎样选择?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:按局部处理器、Advice 优先级和异常继承关系,推导顶层异常与 cause 最终会匹配哪个方法。

相关内容:异常处理器选择。

参考回答

Spring MVC 优先查找 Controller 的局部处理器,再按优先级查找全局 Advice。

@Order 数值越小,Advice 越早参与查找。

同一 Controller 或 Advice 内,先匹配顶层异常,多个类型匹配时选择继承距离更近的类型。顶层无匹配时,再沿 cause 链查找。因此,高优先级 Advice 的 cause 匹配可以先于低优先级的顶层匹配,源码中的方法顺序则不影响选择。

12. 参考资料​