Skip to content

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 方法抛出异常时,DispatcherServletprocessHandlerException 会遍历所有已注册的 HandlerExceptionResolver,找到能处理该异常类型的 @ExceptionHandler 方法:

java
@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 那个,因为 NPERuntimeException 的子类,且 RuntimeException 的匹配优先级高于 Exception

多个 @ControllerAdvice 的优先级

多个 @ControllerAdvice 共存时,通过 @Order 控制执行顺序。数字越小优先级越高(类似 Filter 的排序):

java
@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 端点:

yaml
server:
  error:
    include-message: always
    include-binding-errors: always
    include-stacktrace: never
    include-exception: true

默认的 /error 响应长这样(浏览器请求返回 whitelabel 错误页,API 请求返回 JSON):

json
{
  "timestamp": "2026-07-20T14:00:00.000+00:00",
  "status": 404,
  "error": "Not Found",
  "path": "/api/orders/99999"
}

如果需要自定义 /error 的返回字段,可以继承 DefaultErrorAttributes

java
@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 抛出的异常。解决办法有三:

  1. Filter 内部 try-catch:最直接,捕获后通过 HttpServletResponse 写 JSON 返回。
  2. 自定义 Filter 委托给 @ControllerAdvice:通过 RequestDispatcher.forward() 转发到 Controller 层,但实现复杂,不推荐。
  3. 使用 Spring Security 的 ExceptionTranslationFilter:如果你的 Filter 是安全相关的,Spring Security 的 ExceptionTranslationFilter 会捕获 AuthenticationExceptionAccessDeniedException,然后转发到 AuthenticationEntryPointAccessDeniedHandler 处理。这是最标准的方式。
java
@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 + @ExceptionHandlerController 抛出的业务/系统异常
容器层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

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。