bigywater.tech
Spring · 43

异常处理:@ExceptionHandler、@ControllerAdvice 与 ProblemDetail

业务异常怎么返回统一的错误体?ProblemDetail 是什么、怎么开?多个 @ControllerAdvice 谁先谁后?为什么我的 @ResponseStatus 不生效了?Filter 里的异常谁来管?这篇在 Spring Boot 4.0.8 的 Spring MVC 7.0.9 上逐个验证。

409 → 500
加了一个 RuntimeException 兜底的 Advice 之后,带 @ResponseStatus(CONFLICT) 的异常的状态码变化
422 problem+json
自定义业务异常通过 ProblemDetail 返回,带自定义字段 code
不经过 Advice
Filter 里抛的异常仍走默认的 /error JSON

默认

实测 · 本机执行输出
  GET /e/illegal → 500 [application/json] {"status": 500, "error": "Internal Server Error", "path": "/e/illegal"}
  GET /e/rse → 418 [application/json] {"status": 418, "error": "I'm a teapot", "path": "/e/rse"}
  GET /e/annotated → 409 [application/json] {"status": 409, "error": "Conflict", "path": "/e/annotated"}
  GET /e/biz → 500 [application/json] {"status": 500, "error": "Internal Server Error", "path": "/e/biz"}
  GET /e/arg → 422 [text/plain] [控制器内的 @ExceptionHandler] 参数不合法
  GET /e/param → 400 [application/json] {"status": 400, "error": "Bad Request", "path": "/e/param"}
  GET /nope → 404 [application/json] {"status": 404, "error": "Not Found", "path": "/nope"}
  GET /e/illegal  [头 X-Boom] → 500 [application/json] {"status": 500, "error": "Internal Server Error", "path": "/e/illegal"}

Boot 的默认错误 JSON 只有状态码、错误名和路径,业务异常信息丢失。

ProblemDetail

实测 · 本机执行输出
  GET /e/illegal → 500 [application/json] {"status": 500, "error": "Internal Server Error", "path": "/e/illegal"}
  GET /e/rse → 418 [application/problem+json] {"detail": "我是茶壶", "status": 418, "title": "I'm a teapot"}
  GET /e/annotated → 409 [application/json] {"status": 409, "error": "Conflict", "path": "/e/annotated"}
  GET /e/biz → 500 [application/json] {"status": 500, "error": "Internal Server Error", "path": "/e/biz"}
  GET /e/arg → 422 [text/plain] [控制器内的 @ExceptionHandler] 参数不合法
  GET /e/param → 400 [application/problem+json] {"detail": "Required parameter 'q' is not present.", "status": 400, "title": "Bad Request"}
  GET /nope → 404 [application/problem+json] {"detail": "No static resource nope.", "status": 404, "title": "Not Found"}
  GET /e/illegal  [头 X-Boom] → 500 [application/json] {"status": 500, "error": "Internal Server Error", "path": "/e/illegal"}

开关只管标准 MVC 异常;自己的异常用 ProblemDetail 返回并可加自定义字段。

Advice 的顺序

实测 · 本机执行输出
  GET /e/illegal → 500 [text/plain] [Order(1) 的另一个 Advice 抢先处理了 IllegalStateException] 非法状态
  GET /e/rse → 418 [application/problem+json] {"detail": "我是茶壶", "status": 418, "title": "I'm a teapot"}
  GET /e/annotated → 500 [text/plain] [Advice 里 RuntimeException 兜底] ConflictEx: null
  GET /e/biz → 422 [application/problem+json] {"detail": "业务异常:余额不足", "status": 422, "title": "业务错误", "type": "https://example.com/errors/biz", "code": 1001}
  GET /e/arg → 422 [text/plain] [控制器内的 @ExceptionHandler] 参数不合法
  GET /e/param → 400 [application/problem+json] {"detail": "Required parameter 'q' is not present.", "status": 400, "title": "Bad Request"}
  GET /nope → 404 [application/problem+json] {"detail": "No static resource nope.", "status": 404, "title": "Not Found"}
  GET /e/illegal  [头 X-Boom] → 500 [application/json] {"status": 500, "error": "Internal Server Error", "path": "/e/illegal"}

@Order 小的先处理,控制器内优先;兜底处理器会吞掉 @ResponseStatus。

边界

实测 · 本机执行输出
  GET /e/inhandler(UnsupportedOperationException → Advice 兜底处理器里又抛了 IllegalStateException)→ 500 {"timestamp":"2026-10-19T03:33:21.079Z","status":500,"error":"Internal Server Error","path":"/e/inhandler"}
  GET /e/local(控制器内有 IllegalArgumentException 处理器,Advice 里也有)→ 422 [控制器内的 @ExceptionHandler] 本地处理器应该处理我

处理器里再抛异常、Filter 里的异常都不经过 Advice。

分步演示

异常处理
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-webmvc,内嵌 Tomcat 11);响应里的 timestamp 和 instance 字段在输出时被略去;Advice 用 @ConditionalOnProperty 按属性开关,三种配置各启动一次
    • 没有验证:@ControllerAdvice 的 basePackages / annotations 限定范围;ErrorController 自定义;WebFlux 的异常处理;ErrorResponse 接口的自定义实现;spring.web.error.* 配置项(Boot 4 的新分组);@ExceptionHandler 参数里取 HandlerMethod 与 WebRequest;国际化的 ProblemDetail 消息
    京ICP备2026067502号-1京公网安备11010502063487号