Spring Boot 异常处理机制(@ControllerAdvice / ErrorController)
提出问题
Spring Boot 应用运行时,异常无处不在:空指针、参数校验失败、数据库连接超时、业务逻辑错误……如果每个 Controller 都自己写 try-catch,代码会膨胀到不可维护,而且异常响应格式五花八门——前端对接时得针对每个接口做不同的错误解析。统一的异常处理机制是后端工程化的基础设施。面试中通常会问:@ControllerAdvice 和 ErrorController 各负责什么?Filter 抛的异常为啥 @ControllerAdvice 抓不到?怎么自定义错误响应字段?这些不是简单的 API 记忆题,而是考察你对 Spring MVC 异常处理全链路(从 Handler 到 DispatcherServlet 再到容器层)的理解深度。
分析问题
@ControllerAdvice 的原理与用法
@ControllerAdvice 本质是一个 @Component,Spring 在启动时会扫描所有带 @ControllerAdvice 的类,将其中的 @ExceptionHandler 方法注册到 ExceptionHandlerExceptionResolver 中。当 Controller 方法抛出异常时,DispatcherServlet 的 processHandlerException 会遍历所有已注册的 HandlerExceptionResolver,找到能处理该异常类型的 @ExceptionHandler 方法:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiError> handleValidation(ValidationException e) {
ApiError error = new ApiError(400, "参数校验失败", e.getErrors());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiError> handleBusiness(BusinessException e) {
ApiError error = new ApiError(e.getCode(), e.getMessage(), null);
return ResponseEntity.status(HttpStatus.resolve(e.getHttpStatus())).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleUnknown(Exception e) {
log.error("未捕获异常", e);
ApiError error = new ApiError(500, "服务器内部错误", null);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}异常匹配规则:按异常类型的继承树从上往下找,子类优先。如果同时有 @ExceptionHandler(Exception.class) 和 @ExceptionHandler(RuntimeException.class),抛 NullPointerException 时会命中 RuntimeException 那个,因为 NPE 是 RuntimeException 的子类,且 RuntimeException 的匹配优先级高于 Exception。
多个 @ControllerAdvice 的优先级
多个 @ControllerAdvice 共存时,通过 @Order 控制执行顺序。数字越小优先级越高(类似 Filter 的排序):
@Order(1)
@ControllerAdvice(basePackages = "com.example.moduleA")
public class ModuleAExceptionHandler { ... }
@Order(2)
@ControllerAdvice(basePackages = "com.example.moduleB")
public class ModuleBExceptionHandler { ... }匹配规则:先按 Order 排序,从高优先级开始找,找到能处理该异常类型的就返回,不再继续。所以如果 @Order(1) 的 Handler 声明了 @ExceptionHandler(Exception.class),它会吃掉所有异常,导致 @Order(2) 的 Handler 永远不生效。生产上建议:全局 Handler 放最低优先级(@Order(Ordered.LOWEST_PRECEDENCE)),模块级 Handler 放高优先级,各自处理本模块的特定异常,兜底异常交给全局 Handler。
容器层:BasicErrorController
当 @ControllerAdvice 也没处理(或者异常发生在 DispatcherServlet 范围之外),异常会传播到容器层。Spring Boot 的自动配置 ErrorMvcAutoConfiguration 注册了一个 BasicErrorController(实现了 ErrorController 接口),默认映射到 /error 端点:
server:
error:
include-message: always
include-binding-errors: always
include-stacktrace: never
include-exception: true默认的 /error 响应长这样(浏览器请求返回 whitelabel 错误页,API 请求返回 JSON):
{
"timestamp": "2026-07-20T14:00:00.000+00:00",
"status": 404,
"error": "Not Found",
"path": "/api/orders/99999"
}如果需要自定义 /error 的返回字段,可以继承 DefaultErrorAttributes:
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> attrs = super.getErrorAttributes(webRequest, options);
Map<String, Object> result = new HashMap<>();
result.put("code", attrs.get("status"));
result.put("message", attrs.get("error"));
result.put("path", attrs.get("path"));
result.put("timestamp", attrs.get("timestamp"));
result.put("requestId", webRequest.getAttribute("requestId", SCOPE_REQUEST));
return result;
}
}Filter 中抛出的异常怎么办
Filter 在 DispatcherServlet 之前执行,属于 Servlet 容器层,@ControllerAdvice 的 @ExceptionHandler 拦截不到 Filter 抛出的异常。解决办法有三:
- Filter 内部 try-catch:最直接,捕获后通过
HttpServletResponse写 JSON 返回。 - 自定义 Filter 委托给 @ControllerAdvice:通过
RequestDispatcher.forward()转发到 Controller 层,但实现复杂,不推荐。 - 使用 Spring Security 的 ExceptionTranslationFilter:如果你的 Filter 是安全相关的,Spring Security 的
ExceptionTranslationFilter会捕获AuthenticationException和AccessDeniedException,然后转发到AuthenticationEntryPoint和AccessDeniedHandler处理。这是最标准的方式。
@Component
public class RateLimitFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 限流检查
if (!rateLimitService.tryAcquire()) {
throw new RateLimitException("请求过于频繁");
}
chain.doFilter(request, response);
} catch (RateLimitException e) {
// Filter 层异常必须在 Filter 内处理
HttpServletResponse resp = (HttpServletResponse) response;
resp.setStatus(429);
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁\"}");
}
}
}总结
异常处理链路一览
| 层 | 处理方式 | 适用范围 |
|---|---|---|
| Filter 层 | Filter 内部 try-catch 或 ExceptionTranslationFilter | 鉴权、限流、请求日志等前置拦截 |
| Controller 层 | @ControllerAdvice + @ExceptionHandler | Controller 抛出的业务/系统异常 |
| 容器层 | BasicErrorController (/error 端点) | 404、@ControllerAdvice 未覆盖的兜底异常 |
面试话术示例
"Spring Boot 的异常处理分两层:应用层用 @ControllerAdvice 做统一 JSON 响应,底层用 BasicErrorController 兜底 404 和其他未处理异常。Filter 层的异常需要在 Filter 内自行处理,我一般用 try-catch 写 JSON 返回,或者用 Spring Security 的 ExceptionTranslationFilter 委托给 AuthenticationEntryPoint。自定义 ErrorAttributes 可以统一 /error 端点的返回格式,但大多数场景下 @ControllerAdvice 已经够用。"
关键点
- @ControllerAdvice 的异常匹配规则:按异常继承树,子类优先;多个 Advice 按 @Order 排序
- 至少有一个兜底方法标注
@ExceptionHandler(Exception.class),防止漏网异常 - Filter 抛的异常 @ControllerAdvice 抓不到,必须在 Filter 内自行处理
- 生产上建议全局 Advice 用
@Order(Ordered.LOWEST_PRECEDENCE),模块级 Advice 用高优先级 - 自定义
/error响应通过继承DefaultErrorAttributes实现,比覆盖ErrorController更简单
参考:Spring Boot 源码 — BasicErrorController、ErrorMvcAutoConfiguration;Spring MVC 源码 — ExceptionHandlerExceptionResolver