Spring Boot 异常处理到底怎么玩的?从 DispatcherServlet 到全局兜底的全链路拆解

Spring Boot 异常处理到底怎么玩的?从 DispatcherServlet 到全局兜底的全链路拆解

你天天写 @RestControllerAdvice,但你知道 Spring Boot 是怎么把异常一路从 Controller 扛到你的全局处理器手里的吗?中间经过了哪些层?为什么有的异常能被捕获、有的却直接 500?这篇文章从 DispatcherServlet 的异常处理链出发,一路拆到 @ExceptionHandler@ControllerAdviceBasicErrorController,最后给出一套生产级异常处理方案。


一、从一个问题开始

你写了全局异常处理器:

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public Result<String> handle(Exception e) {
        return Result.fail(e.getMessage());
    }
}

然后写了个接口故意抛异常:

java 复制代码
@GetMapping("/test")
public String test() {
    int i = 1 / 0; // ArithmeticException
}

请求后,你确实拿到了统一的 JSON 响应。但是------

  1. 谁捕获了这个异常? DispatcherServlet?还是 Tomcat?
  2. 异常是怎么传递到 @ExceptionHandler 的?
  3. 为什么 Filter 里抛的异常 @ExceptionHandler 捕获不到?
  4. 如果 @ExceptionHandler 没匹配到,会发生什么?
  5. 生产环境的 Whitelabel Error Page 是谁渲染的?

这五个问题,对应异常处理的五个层次。逐层拆解。


二、Spring MVC 异常处理架构总览

css 复制代码
┌───────────────────────────────────────────────────────┐
│                  HTTP 请求                             │
└───────────────────────┬───────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────┐
│              Filter Chain(Servlet 容器层)             │
│  异常在这里不会被 @ExceptionHandler 捕获!              │
└───────────────────────┬───────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────┐
│           DispatcherServlet.doDispatch()               │
│  ┌─────────────────────────────────────────────┐      │
│  │ HandlerMapping → HandlerAdapter → Controller │      │
│  │           ↓ 异常 ↓                           │      │
│  │ processDispatchResult()                       │      │
│  │ → processHandlerException()                  │      │
│  │ → HandlerExceptionResolver 链                │      │
│  └─────────────────────────────────────────────┘      │
└───────────────────────┬───────────────────────────────┘
                        │ 无 Resolver 能处理
                        ▼
┌───────────────────────────────────────────────────────┐
│           Servlet 容器默认错误处理                      │
│           → 转发到 /error                              │
│           → BasicErrorController                      │
│           → Whitelabel Error Page                      │
└───────────────────────────────────────────────────────┘

核心思路:Spring MVC 的异常处理在 DispatcherServlet 内部完成,Filter 层的异常它管不到


三、第一层:DispatcherServlet 的异常捕获

3.1 doDispatch() 的 try-catch

DispatcherServlet 的核心方法 doDispatch() 大致结构:

java 复制代码
void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    try {
        ModelAndView mv = null;
        try {
            // 1. 找 Handler
            mappedHandler = getHandler(processedRequest);
            // 2. 找 Adapter
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            // 3. 执行 Handler(Controller 方法)
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        } catch (Exception ex) {
            // 4. 异常在这里被捕获!
            mv = processHandlerException(processedRequest, response, 
                                          mappedHandler, ex);
        }
        // 5. 渲染视图
        processDispatchResult(processedRequest, response, 
                              mappedHandler, mv, dispatchException);
    } catch (Exception ex) {
        // 6. 如果 processHandlerException 也抛异常,到这里
        // 最终交给 Servlet 容器处理
    }
}

关键:Controller 抛的异常在步骤 3 被捕获,交给 processHandlerException() 处理

3.2 processHandlerException() 做了什么?

java 复制代码
ModelAndView processHandlerException(HttpServletRequest request, 
                                      HttpServletResponse response, 
                                      Object handler, Exception ex) {
    // 遍历所有 HandlerExceptionResolver
    for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
        ModelAndView mv = resolver.resolveException(request, response, handler, ex);
        if (mv != null) {
            return mv; // 有 Resolver 能处理,返回
        }
    }
    return null; // 无 Resolver 能处理,返回 null
}

如果所有 Resolver 都返回 null,异常会继续往外抛,最终由 Servlet 容器处理。


四、第二层:HandlerExceptionResolver 链

Spring Boot 默认注册三个 Resolver,按顺序执行:

顺序 Resolver 作用
1 ExceptionHandlerExceptionResolver 处理 @ExceptionHandler
2 ResponseStatusExceptionResolver 处理 @ResponseStatus
3 DefaultHandlerExceptionResolver 处理 Spring MVC 标准异常

4.1 ExceptionHandlerExceptionResolver

这是最常用的 Resolver。它负责找 @ExceptionHandler 方法:

java 复制代码
// 核心逻辑(简化)
ModelAndView doResolveHandlerMethodException(request, response, handler, ex) {
    // 1. 找到 @ControllerAdvice 标注的类
    // 2. 在这些类中找 @ExceptionHandler 标注的方法
    // 3. 匹配异常类型(精确匹配优先,然后按继承关系找)
    // 4. 调用匹配的方法
    // 5. 将返回值包装成 ModelAndView
}

匹配规则

  • 精确匹配优先:@ExceptionHandler(ArithmeticException.class) 优先于 @ExceptionHandler(Exception.class)
  • 子类优先:如果异常同时匹配多个 @ExceptionHandler,最具体的那个胜出
  • 如果没有精确匹配,按声明顺序尝试

4.2 ResponseStatusExceptionResolver

处理 @ResponseStatus 注解:

java 复制代码
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "资源不存在")
public class ResourceNotFoundException extends RuntimeException {}

或者直接在方法上标注:

java 复制代码
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@GetMapping("/test")
public String test() {
    throw new IllegalArgumentException("参数错误");
}

这个 Resolver 的处理逻辑很简单:读取 @ResponseStatus 的 code 和 reason,设置 response 状态码。

4.3 DefaultHandlerExceptionResolver

处理 Spring MVC 自定义的标准异常:

异常 HTTP 状态码
HttpRequestMethodNotSupportedException 405
HttpMediaTypeNotSupportedException 415
MissingServletRequestParameterException 400
TypeMismatchException 400
MethodArgumentNotValidException 400

这些异常通常由 Spring MVC 框架自身抛出,比如请求方法不匹配、参数类型转换失败等。


五、第三层:@ControllerAdvice 的底层机制

5.1 @ControllerAdvice 是怎么被发现的?

Spring Boot 启动时,RequestMappingHandlerAdapter 的初始化过程:

java 复制代码
// 简化流程
void afterPropertiesSet() {
    // 1. 扫描所有 @ControllerAdvice 标注的 Bean
    this.controllerAdviceCache = 
        ControllerAdviceCache.find(this.applicationContext);
    
    // 2. 解析每个 @ControllerAdvice 中的 @ExceptionHandler
    for (Class<?> controllerAdvice : controllerAdvices) {
        Map<Class<? extends Throwable>, Method> mappings = 
            ExceptionHandlerMethodResolver.resolve(controllerAdvice);
        this.exceptionHandlerCache.put(controllerAdvice, mappings);
    }
}

5.2 @ControllerAdvice 的作用范围

java 复制代码
// 限制包范围
@ControllerAdvice("com.example.controller")
// 限制注解
@ControllerAdvice(annotations = RestController.class)
// 限制特定类
@ControllerAdvice(assignableTypes = OrderController.class)
属性 作用 默认值
basePackages 限制包范围 所有包
annotations 限制 Controller 注解 所有
assignableTypes 限制 Controller 类型 所有

注意 :如果不限制范围,@ControllerAdvice 对所有 Controller 生效,可能产生意料外的匹配。

5.3 @ExceptionHandler 的返回值处理

@ExceptionHandler 方法的返回值会被 HandlerMethodReturnValueHandler 处理:

返回类型 处理器 效果
ResponseEntity<T> ResponseEntityReturnValueHandler 直接设置状态码和 Body
Result<T>(自定义) RequestResponseBodyMethodProcessor 通过 @ResponseBody 序列化为 JSON
ModelAndView ModelAndViewMethodReturnValueHandler 渲染视图
String(视图名) ViewNameMethodReturnValueHandler 渲染视图

所以 @RestControllerAdvice + 返回 Result<T> 的写法,本质是 @ResponseBody 把对象序列化为 JSON。


六、第四层:Filter 异常为什么捕获不到?

这是最常见的困惑:为什么 @ExceptionHandler 捕获不到 Filter 中的异常?

原因很简单:DispatcherServlet 的 try-catch 只包裹 Handler 执行过程,Filter 在 Servlet 容器层执行,不在 DispatcherServlet 的管辖范围内。

arduino 复制代码
请求 → Filter1 → Filter2 → ... → DispatcherServlet → Controller
                                    ↑ try-catch 在这里
异常在 Filter 中抛出 → 不经过 DispatcherServlet → 直接交给 Servlet 容器

6.1 解法一:Filter 中 try-catch + 转发到 /error

java 复制代码
@Component
public class AuthFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, 
                          FilterChain chain) {
        try {
            // 业务逻辑
            chain.doFilter(req, resp);
        } catch (Exception e) {
            // 转发到 /error,让 Spring Boot 的 ErrorController 处理
            req.setAttribute("javax.servlet.error.exception", e);
            req.getRequestDispatcher("/error").forward(req, resp);
        }
    }
}

6.2 解法二:Spring Boot 的 ErrorController

Spring Boot 默认注册了 BasicErrorController,处理 /error 路径:

java 复制代码
@Controller
@RequestMapping("${server.error.path:/error}")
public class BasicErrorController implements ErrorController {
    
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, 
            isIncludeStackTrace(request));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }
}

自定义 ErrorController:

java 复制代码
@RestController
public class CustomErrorController implements ErrorController {
    
    @RequestMapping("/error")
    public Result<Map<String, Object>> error(HttpServletRequest request) {
        Integer status = (Integer) request.getAttribute(
            "javax.servlet.error.status_code");
        Throwable ex = (Throwable) request.getAttribute(
            "javax.servlet.error.exception");
        
        Map<String, Object> detail = new HashMap<>();
        detail.put("status", status);
        detail.put("message", ex != null ? ex.getMessage() : "未知错误");
        
        return Result.fail(detail);
    }
}

6.3 对比

方案 优点 缺点
@ExceptionHandler 在 Controller 层,可以统一 JSON 格式 只能捕获 Controller 异常
Filter try-catch + 转发 能处理 Filter 异常 需要每个 Filter 手动处理
自定义 ErrorController 全局兜底,Filter 和 Controller 异常都能处理 无法区分异常类型做精细化处理

生产推荐@ExceptionHandler + 自定义 ErrorController 双层兜底。


七、第五层:Spring Boot 的错误处理自动配置

7.1 ErrorMvcAutoConfiguration

Spring Boot 通过 ErrorMvcAutoConfiguration 自动配置错误处理:

java 复制代码
@AutoConfiguration
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
public class ErrorMvcAutoConfiguration {
    
    // 1. 注册 BasicErrorController
    @Bean
    @ConditionalOnMissingBean(ErrorController.class)
    public BasicErrorController errorController() {
        return new BasicErrorController(
            this.serverProperties.getError(), this.errorViewResolvers);
    }
    
    // 2. 注册 Whitelabel Error Page 视图
    @Bean
    @ConditionalOnMissingBean(ErrorViewResolver.class)
    public WhitelabelErrorView whitelabelErrorView() {
        return new WhitelabelErrorView();
    }
}

7.2 错误页面配置

yaml 复制代码
server:
  error:
    path: /error          # 错误处理路径
    include-exception: true  # 响应中包含异常类名
    include-stacktrace: always  # 包含堆栈(仅开发环境!)
    include-message: always    # 包含异常消息
    whitelabel:
      enabled: false          # 关闭 Whitelabel 页面
配置 作用 生产推荐
include-exception 返回异常类名 false
include-stacktrace 返回堆栈信息 never
include-message 返回异常消息 on_param
whitelabel.enabled 启用白标错误页 false

生产环境一定要关闭堆栈信息和 Whitelabel 页面,否则会泄露内部信息。


八、生产级异常处理方案

8.1 自定义异常体系

java 复制代码
// 基础异常
public class BaseException extends RuntimeException {
    private final ErrorCode errorCode;
    
    public BaseException(ErrorCode errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public BaseException(ErrorCode errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
}

// 业务异常
public class BusinessException extends BaseException {
    public BusinessException(ErrorCode errorCode, String message) {
        super(errorCode, message);
    }
}

// 参数异常
public class ParamException extends BaseException {
    public ParamException(String message) {
        super(ErrorCode.PARAM_ERROR, message);
    }
}

8.2 错误码体系

java 复制代码
public enum ErrorCode {
    // 通用
    SUCCESS(0, "成功"),
    SYSTEM_ERROR(1, "系统异常"),
    PARAM_ERROR(2, "参数错误"),
    
    // 业务
    USER_NOT_FOUND(1001, "用户不存在"),
    ORDER_EXPIRED(2001, "订单已过期"),
    INSUFFICIENT_BALANCE(2002, "余额不足"),
    ;
    
    private final int code;
    private final String message;
    
    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
}

8.3 全局异常处理器

java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // 1. 业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusiness(BusinessException e) {
        log.warn("业务异常: code={}, msg={}", e.getErrorCode().getCode(), e.getMessage());
        return Result.fail(e.getErrorCode().getCode(), e.getMessage());
    }
    
    // 2. 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidation(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
            .collect(Collectors.joining("; "));
        log.warn("参数校验失败: {}", message);
        return Result.fail(ErrorCode.PARAM_ERROR.getCode(), message);
    }
    
    // 3. 参数绑定异常
    @ExceptionHandler(BindException.class)
    public Result<Void> handleBind(BindException e) {
        String message = e.getFieldErrors().stream()
            .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
            .collect(Collectors.joining("; "));
        return Result.fail(ErrorCode.PARAM_ERROR.getCode(), message);
    }
    
    // 4. Spring Security 认证异常
    @ExceptionHandler(AuthenticationException.class)
    public Result<Void> handleAuth(AuthenticationException e) {
        return Result.fail(401, "认证失败");
    }
    
    // 5. 权限异常
    @ExceptionHandler(AccessDeniedException.class)
    public Result<Void> handleAccessDenied(AccessDeniedException e) {
        return Result.fail(403, "无权限");
    }
    
    // 6. 兜底:所有其他异常
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("未处理异常", e);
        return Result.fail(ErrorCode.SYSTEM_ERROR.getCode(), "系统繁忙,请稍后重试");
    }
}

8.4 自定义 ErrorController 兜底

java 复制代码
@RestController
public class GlobalErrorController implements ErrorController {
    
    @RequestMapping("/error")
    public Result<Map<String, Object>> error(HttpServletRequest request) {
        Integer status = (Integer) request.getAttribute(
            "javax.servlet.error.status_code");
        Throwable ex = (Throwable) request.getAttribute(
            "javax.servlet.error.exception");
        
        String message = "未知错误";
        if (ex != null) {
            message = ex.getMessage();
            if (message == null) {
                message = ex.getClass().getSimpleName();
            }
        }
        
        Map<String, Object> detail = new HashMap<>();
        detail.put("status", status);
        detail.put("message", message);
        
        return Result.fail(status, message);
    }
}

九、常见踩坑与排错

9.1 @ExceptionHandler 不生效

原因 1@ControllerAdvice 的包路径不对,没扫描到 Controller。

解法 :检查包路径或用 @ControllerAdvice(basePackages = "com.example")

原因 2 :异常类型不匹配。@ExceptionHandler(NullPointerException.class) 捕获不了 ArithmeticException

解法 :用 @ExceptionHandler(Exception.class) 兜底,精确类型放在前面。

原因 3 :多个 @ControllerAdvice 中的 @ExceptionHandler 匹配同一种异常,执行顺序不可控。

解法 :用 @Order 控制优先级:

java 复制代码
@RestControllerAdvice
@Order(1) // 数字越小优先级越高
public class BusinessExceptionHandler { ... }

@RestControllerAdvice
@Order(2)
public class SystemExceptionHandler { ... }

9.2 异常被吞了

原因@ExceptionHandler 方法内部又抛了异常。

java 复制代码
@ExceptionHandler(Exception.class)
public Result<Void> handle(Exception e) {
    throw new RuntimeException("又炸了"); // 这个异常不会被另一个 @ExceptionHandler 捕获
}

解法@ExceptionHandler 方法内部不要再抛异常,所有分支都要有兜底返回。

9.3 异步线程的异常

@Async 方法中抛的异常不会被 @ExceptionHandler 捕获,因为执行线程已经不在 DispatcherServlet 的请求上下文中。

解法 :异步方法内部自行处理异常,或通过 AsyncUncaughtExceptionHandler 全局处理:

java 复制代码
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            log.error("异步任务异常: method={}", method.getName(), ex);
        };
    }
}

9.4 参数校验异常的消息格式

MethodArgumentNotValidException 默认的 getMessage() 格式很丑,不适合直接返回给前端。

解法 :在 @ExceptionHandler 中手动格式化:

java 复制代码
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
    String message = e.getBindingResult().getFieldErrors().stream()
        .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
        .collect(Collectors.joining("; "));
    return Result.fail(ErrorCode.PARAM_ERROR.getCode(), message);
}

输出效果:name: 不能为空; age: 必须大于0,比原始的 Validation failed for ... 友好得多。


十、异常处理最佳实践总结

要点 说明
自定义异常体系 区分业务异常/系统异常/参数异常,不直接用 RuntimeException
错误码体系 用枚举管理,code + message,前端根据 code 做差异化处理
@ExceptionHandler 分层 精确类型在前,Exception 兜底在后
ErrorController 兜底 Filter 异常 + 无匹配的异常,都由 ErrorController 兜底
关闭堆栈信息 生产环境 include-stacktrace: never,防止信息泄露
异步异常单独处理 @Async 异常走 AsyncUncaughtExceptionHandler
日志分级 业务异常 warn,系统异常 error,参数异常 info
不在 @ExceptionHandler 里抛异常 避免"异常套异常"的死循环

十一、总结

层次 机制 处理范围
DispatcherServlet processHandlerException() Controller 层异常
HandlerExceptionResolver 链 三级 Resolver 依次处理 @ExceptionHandler / @ResponseStatus / 默认
@ControllerAdvice 扫描 + 反射调用 仅 Controller 层
ErrorController /error 端点兜底 所有未被处理的异常
Servlet 容器 默认 500 页面 兜底

记住一句话:Spring Boot 异常处理 = DispatcherServlet 内部 Resolver 链 + ErrorController 外部兜底。Controller 异常走 Resolver,其他异常走 ErrorController。两层都配好,才能保证任何异常都返回统一格式的 JSON。

下一篇,我们聊聊 Spring Boot 的缓存机制------@Cacheable 背后到底发生了什么。

相关推荐
不才不才不不才2 小时前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
(轻舟已过万重山)2 小时前
第40章 Spring AI 实战:企业级 AI 应用架构
人工智能·spring·架构
堕落年代18 小时前
Ollama CPU 推理大提示词优化实测报告(细致化数据版)
java·后端·spring
cfm_291419 小时前
基于OAuth2.0实现微服务SSO单点登录
后端·spring·微服务·架构
攻城有术20 小时前
专项攻克-springcloud及其组件
后端·spring·spring cloud
叶总没有会21 小时前
5.1知识库概念进阶
java·人工智能·spring·阿里云·ai·原型模式
砍材农夫1 天前
物联网实战|Spring Boot MQTT平台 |emqx broker实战
spring boot·spring·spring cloud·mybatis
2401_894915531 天前
GEO 定位优化源码搭建常见报错排查:数据库、伪静态、接口调试
java·数据库·网络协议·tcp/ip·spring·unity
sugar__salt1 天前
Spring、Spring Boot 与配置文件核心知识点详解
java·spring boot·后端·spring·java-ee·maven·intellij-idea