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 中,所有请求最终都经过 DispatcherServlet 的 doDispatch() 方法。我们可以把它的核心逻辑简化为:
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 会:
- 扫描所有带
@ControllerAdvice/@RestControllerAdvice注解的类。 - 遍历这些类中所有带
@ExceptionHandler的方法。 - 建立一张映射表:
异常类型 → 处理方法。
以我们的代码为例,最终生成的映射表大致如下:
MethodArgumentNotValidException.class → handleValidationException()
IllegalArgumentException.class → handleIllegalArgumentException()
BusinessException.class → handleBusinessException()
Exception.class → handleException()
阶段二:运行时匹配与执行
当接收到一个异常(比如 BusinessException)时,解析器会:
- 拿着
BusinessException去映射表中查找。 - 先查精确匹配,找到
handleBusinessException()。 - 若没有精确匹配,则向上查找父类,直至找到
Exception.class对应的兜底方法。 - 通过 反射 调用匹配到的方法,传入异常实例。
- 将返回的
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 无法兜底。
而 DispatcherServlet 的 try-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 中最推荐的全局异常处理方案。理解它的原理,不仅帮助我们写出更健壮的异常处理代码,也让我们在遇到异常未被捕获或响应格式异常等问题时,能快速定位根因。
浙公网安备 33010602011771号