Spring MVC 全局异常处理深度解析:@RestControllerAdvice 的原理与实践

二、整体架构:请求到响应的全链路

在深入细节之前,先建立一个整体认知。一次 HTTP 请求进入 Spring MVC 后,经历的路径如下:

HTTP 请求
    │
    ▼
┌─────────────────────────────────┐
│        DispatcherServlet        │  ← 所有请求的统一入口
│   ┌───────────────────────────┐ │
│   │      doDispatch()         │ │  ← 核心调度方法,含全局 try-catch
│   │                           │ │
│   │  try {                    │ │
│   │    执行 Controller 方法    │ │
│   │  } catch (Exception ex) { │ │
│   │    dispatchException = ex │ │
│   │  }                        │ │
│   │                           │ │
│   │  processDispatchResult()  │ │  ← 处理正常结果 or 异常
│   └───────────────────────────┘ │
└─────────────────────────────────┘
    │ 异常流转
    ▼
┌─────────────────────────────────┐
│   HandlerExceptionResolver 链   │  ← 异常解析器责任链
│                                 │
│  ExceptionHandlerExceptionResolver  ← 最核心的解析器
│      │                          │
│      ▼                          │
│  查找 @ExceptionHandler 映射表   │
│      │                          │
│      ▼                          │
│  反射调用 GlobalExceptionHandler │
└─────────────────────────────────┘
    │
    ▼
JSON 响应返回前端

三、核心机制:统一的 try-catch 在哪里?

3.1 DispatcherServlet.doDispatch()

Spring MVC 中,所有请求最终都经过 DispatcherServletdoDispatch() 方法。我们可以把它的核心逻辑简化为:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    Exception dispatchException = null;
    ModelAndView mv = null;

    try {
        // 1. 找到能处理该请求的 Handler(即你的 Controller 方法)
        HandlerExecutionChain mappedHandler = getHandler(processedRequest);

        // 2. 找到对应的 HandlerAdapter(用于适配不同类型的 Handler)
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

        // 3. 执行拦截器的前置方法
        mappedHandler.applyPreHandle(processedRequest, response);

        // 4. ⭐ 真正执行 Controller 方法
        //    如果这里抛出异常,后续正常逻辑不会走,异常被 catch 捕获
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    } catch (Exception ex) {
        // 5. ⭐ 统一捕获!所有 Controller 方法抛出的异常都会落到这里
        dispatchException = ex;
    }

    // 6. 统一处理结果(无论正常返回还是异常)
    processDispatchResult(request, response, mappedHandler, mv, dispatchException);
}

关键点: 这个 try-catch 是所有 Controller 方法共享的同一个,不是每个方法单独套一层。它位于请求调度的最顶层,保证了覆盖的全面性。

3.2 processDispatchResult() —— 分叉路口

doDispatch 执行完毕,进入 processDispatchResult 方法。这是正常流程与异常流程的分叉点:

private void processDispatchResult(
        HttpServletRequest request, HttpServletResponse response,
        HandlerExecutionChain mappedHandler, ModelAndView mv,
        Exception exception) throws Exception {

    if (exception != null) {
        // ⭐ 有异常:交给异常解析器链处理
        mv = processHandlerException(request, response, handler, exception);
    } else {
        // 正常流程:渲染 ModelAndView
        render(mv, request, response);
    }
}

四、异常是如何路由到你的处理方法的?

4.1 HandlerExceptionResolver 责任链

processHandlerException 内部会遍历一个 异常解析器(HandlerExceptionResolver)责任链,依次尝试处理异常:

protected ModelAndView processHandlerException(..., Exception ex) throws Exception {
    // 遍历所有注册的异常解析器
    for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
        ModelAndView exMv = resolver.resolveException(request, response, handler, ex);
        if (exMv != null) {
            return exMv; // 找到能处理的就返回,短路后续
        }
    }
    throw ex; // 所有解析器都无法处理,继续向上抛
}

Spring 默认注册了以下几个解析器(按优先级排列):

优先级 解析器 职责
1 ExceptionHandlerExceptionResolver 处理 @ExceptionHandler 注解的方法
2 ResponseStatusExceptionResolver 处理 @ResponseStatus 注解的异常类
3 DefaultHandlerExceptionResolver 处理 Spring MVC 内置的标准异常

我们的 GlobalExceptionHandler 被第一个解析器处理。

4.2 ExceptionHandlerExceptionResolver 的工作原理

这是最核心的解析器,它的工作分为两个阶段:

阶段一:启动时扫描与建表(缓存)

Spring 容器启动时,ExceptionHandlerExceptionResolver 会:

  1. 扫描所有带 @ControllerAdvice / @RestControllerAdvice 注解的类。
  2. 遍历这些类中所有带 @ExceptionHandler 的方法。
  3. 建立一张映射表:异常类型 → 处理方法

以我们的代码为例,最终生成的映射表大致如下:

MethodArgumentNotValidException.class  →  handleValidationException()
IllegalArgumentException.class         →  handleIllegalArgumentException()
BusinessException.class                →  handleBusinessException()
Exception.class                        →  handleException()

阶段二:运行时匹配与执行

当接收到一个异常(比如 BusinessException)时,解析器会:

  1. 拿着 BusinessException 去映射表中查找。
  2. 先查精确匹配,找到 handleBusinessException()
  3. 若没有精确匹配,则向上查找父类,直至找到 Exception.class 对应的兜底方法。
  4. 通过 反射 调用匹配到的方法,传入异常实例。
  5. 将返回的 ResponseEntity 序列化为 JSON,写入 HTTP 响应。
// 伪代码示意
Method handlerMethod = exceptionHandlerCache.findMethod(exception.getClass());
Object result = handlerMethod.invoke(globalExceptionHandlerInstance, exception);
// 将 result(ResponseEntity)序列化为 JSON 响应

五、为什么不是 AOP?

这是一个很好的问题。@RestControllerAdvice概念上确实达到了 AOP(处理横切关注点)的目的,但底层实现机制完全不同

5.1 Spring AOP 的实现方式

Spring AOP 基于 动态代理

  • 对于实现了接口的类:使用 JDK 动态代理
  • 对于没有实现接口的类:使用 CGLIB 生成子类代理。

代理对象在方法调用前后动态织入横切逻辑(如 @Around@Before)。

// AOP 代理的伪代码
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        beforeAdvice();
        Object result = method.invoke(target, args); // 调用真实方法
        afterReturningAdvice(result);
        return result;
    } catch (Exception e) {
        afterThrowingAdvice(e);
        throw e;
    }
}

5.2 全局异常处理的实现方式

全局异常处理基于 Spring MVC 的请求处理链条

  • 没有生成代理类。
  • 拦截点在 DispatcherServlet 这个最外层的请求调度器。
  • 通过 HandlerExceptionResolver 机制,在捕获异常后动态查找并调用处理方法。

5.3 为什么 Spring 选择这种设计?

如果使用 AOP 来处理 Controller 异常,存在一个根本性的缺陷:

AOP 切面本身抛出的异常,无法被 Controller 层的 AOP 捕获。

例如:

  • 权限校验切面(@Before)抛出 AccessDeniedException
  • 日志切面(@Around)自身发生异常。
  • 数据绑定阶段(Controller 方法执行前)抛出 MethodArgumentNotValidException

这些异常都发生在 Controller 方法的 外层,AOP 无法兜底。

DispatcherServlettry-catch 处于更顶层,可以捕获以下所有情况:

DispatcherServlet.doDispatch()
    └── try {
            Interceptor.preHandle()       ← 拦截器异常 ✅ 能捕获
            HandlerAdapter.handle()
                └── AOP Proxy
                        └── Controller.method()   ← 业务异常 ✅ 能捕获
            数据绑定/参数校验               ← 校验异常 ✅ 能捕获
        } catch (Exception e) {
            // 全部在这里兜底
        }

六、代码解析:GlobalExceptionHandler 的每个细节

回看我们开头的代码,现在可以理解每一部分的设计意图了。

6.1 异常的优先级匹配

// 精确匹配:参数校验失败(400)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<RespResult<?>> handleValidationException(...) { ... }

// 精确匹配:非法参数(400)
@ExceptionHandler(value = {IllegalArgumentException.class})
public ResponseEntity<RespResult<?>> handleIllegalArgumentException(...) { ... }

// 精确匹配:自定义业务异常(400)
@ExceptionHandler(value = {BusinessException.class})
public ResponseEntity<RespResult<?>> handleBusinessException(...) { ... }

// 兜底:所有未预期的异常(500)
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<RespResult<?>> handleException(...) { ... }

匹配规则: Spring 总是优先选择最精确的异常类型。BusinessException 不会被 Exception.class 的处理方法误捕获。

6.2 参数校验异常的特殊处理

String errors = e.getBindingResult()
        .getAllErrors()
        .stream()
        .map(ObjectError::getDefaultMessage)  // 提取每个字段的校验消息
        .collect(Collectors.joining());        // 拼接为一条字符串

MethodArgumentNotValidException 内部可能包含多个字段的校验错误(如「用户名不能为空」「邮箱格式不正确」),这里将它们全部提取并拼接,一次性返回给前端。

6.3 自定义业务异常携带错误码

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
        .body(RespResult.failed(e.getCode(), e.getMessage()));

BusinessException 除了消息外,还携带了业务错误码(code)。这在前端需要根据不同错误码展示不同 UI 时非常有用(如弹窗 vs. 页面跳转)。

6.4 兜底异常处理的改进建议

// 当前实现
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<RespResult<?>> handleException(Exception e) {
    e.printStackTrace(); // TODO: 应替换为结构化日志
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(RespResult.failed(e.getMessage()));
}

代码中有 //todo print Exception 的注释,建议替换为:

@ExceptionHandler(value = {Exception.class})
public ResponseEntity<RespResult<?>> handleException(Exception e) {
    // ✅ 使用结构化日志,生产环境更易追踪
    log.error("Unhandled exception occurred", e);
    // ✅ 生产环境不应将内部错误细节暴露给前端
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(RespResult.failed("服务器内部错误,请稍后重试"));
}

七、完整流程回顾

以一次 BusinessException 的抛出为例,梳理完整链路:

1. 前端发送请求
        ↓
2. DispatcherServlet.doDispatch() 开始执行
        ↓
3. 调用 Controller 方法,方法内抛出 BusinessException
        ↓
4. BusinessException 被 doDispatch() 的 catch 块捕获
   dispatchException = businessException
        ↓
5. 进入 processDispatchResult(),检测到有异常
        ↓
6. 遍历 HandlerExceptionResolver 链
   → ExceptionHandlerExceptionResolver 接手处理
        ↓
7. 查找映射表:BusinessException → handleBusinessException()
        ↓
8. 反射调用 GlobalExceptionHandler.handleBusinessException(e)
        ↓
9. 方法返回 ResponseEntity<RespResult<?>>
        ↓
10. 序列化为 JSON,写入 HTTP 响应
        ↓
11. 前端收到统一格式的错误响应 { code: xxx, message: "..." }

八、总结

维度 说明
本质 基于 Spring MVC 请求处理链,而非 AOP 动态代理
统一 try-catch 位于 DispatcherServlet.doDispatch(),所有请求共享
路由机制 HandlerExceptionResolver 责任链 + 启动时缓存的映射表
执行方式 反射调用匹配到的 @ExceptionHandler 方法
优势 比 AOP 覆盖范围更广,能捕获拦截器、数据绑定等阶段的异常
响应格式 统一封装为 RespResult<?> + ResponseEntity,前端友好

@RestControllerAdvice + @ExceptionHandler 是 Spring MVC 中最推荐的全局异常处理方案。理解它的原理,不仅帮助我们写出更健壮的异常处理代码,也让我们在遇到异常未被捕获或响应格式异常等问题时,能快速定位根因。

posted on 2026-04-01 17:32  滚动的蛋  阅读(17)  评论(0)    收藏  举报

导航