ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring MVC 异常处理全链路:从抛出到响应

Spring MVC 异常处理全链路:从抛出到响应

目录

    • 引言:一个异常在 Spring MVC 中经历了什么?
    • 第一章:异常是在哪里被“抓住”的?
      • 1.1 `doDispatch` 中的 `try-catch`
      • 1.2 `processDispatchResult`:异常处理的总入口
      • 1.3 `processHandlerException`:遍历异常解析器
      • 1.4 `handlerExceptionResolvers` 列表是从哪里来的?
    • 第二章:`ExceptionHandlerExceptionResolver` 是如何介入的?
      • 2.1 为什么是它最先被调用?
      • 2.2 `resolveException` → `doResolveHandlerMethodException`
      • 2.3 `getExceptionHandlerMethod`:查找能处理当前异常的方法
      • 2.4 查找细节
        • 2.4.1 `advice.isApplicableToBeanType()`:适用范围过滤
        • 2.4.2 `resolver.resolveMethod(exception)`:异常类型层级匹配
    • 第三章:`exceptionHandlerAdviceCache` 是从哪里来的?
      • 3.1 启动时已经扫描好了
      • 3.2 缓存的数据结构
      • 3.3 `exceptionHandlerCache` 是懒加载的
    • 第四章:找到方法后,怎么执行?
      • 4.1 包装成 `ServletInvocableHandlerMethod`
      • 4.2 执行“三连击”
      • 4.3 `processDispatchResult` 收尾
    • 第五章:完整时序图 + 关键断点坐标
      • 5.1 从启动扫描到运行时执行的完整流程
      • 5.2 核心设计思想
      • 5.3 优先级总结
      • 5.4 关键断点坐标

引言:一个异常在 Spring MVC 中经历了什么?

假设你的 Controller 中有这样一个方法:

@RestControllerpublicclassUserController{@GetMapping("/user/{id}")publicUsergetUser(@PathVariableLongid){Useruser=userService.getById(id);if(user==null){thrownewUserNotFoundException("用户不存在: "+id);}returnuser;}}

同时你定义了一个全局异常处理器:

@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(UserNotFoundException.class)@ResponseStatus(HttpStatus.NOT_FOUND)publicErrorResponsehandleUserNotFound(UserNotFoundExceptione){returnnewErrorResponse(404,e.getMessage());}}

当请求/user/999时,UserNotFoundException被抛出。这个异常是怎么被 Spring 捕获的?怎么找到GlobalExceptionHandler中对应的方法?处理方法的返回值又是怎么变成 JSON 返回给前端的?本文将从源码角度还原这个完整过程。

第一章:异常是在哪里被“抓住”的?

1.1doDispatch中的try-catch

在DispatcherServlet.doDispatch()中,整个请求处理被包裹在一个try-catch块里:

protectedvoiddoDispatch(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{HttpServletRequestprocessedRequest=request;HandlerExecutionChainmappedHandler=null;ModelAndViewmv=null;ExceptiondispatchException=null;// ← 异常暂存变量try{// 路由匹配、参数解析、反射调用 Controller...mv=ha.handle(processedRequest,response,mappedHandler.getHandler());}catch(Exceptionex){dispatchException=ex;// ← 异常被捕获,存入变量}// 无论是否发生异常,最终都进入 processDispatchResultprocessDispatchResult(processedRequest,response,mappedHandler,mv,dispatchException);}

关键点:

  • 异常不会被立即处理,而是赋值给dispatchException,暂存起来。
  • doDispatch不会把异常往外抛,而是继续执行processDispatchResult,让收尾环节统一处理。

1.2processDispatchResult:异常处理的总入口

privatevoidprocessDispatchResult(HttpServletRequestrequest,HttpServletResponseresponse,HandlerExecutionChainmappedHandler,ModelAndViewmv,Exceptionexception)throwsException{booleanerrorView=false;if(exception!=null){// 有异常!进入异常处理分支Objecthandler=(mappedHandler!=null?mappedHandler.getHandler():null);mv=processHandlerException(request,response,handler,exception);errorView=(mv!=null);}// ... 后续视图渲染或 JSON 输出}

1.3processHandlerException:遍历异常解析器

processHandlerException是异常处理的真正入口:

protectedModelAndViewprocessHandlerException(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{ModelAndViewexMv=null;// 遍历所有已注册的 HandlerExceptionResolverfor(HandlerExceptionResolverresolver:this.handlerExceptionResolvers){exMv=resolver.resolveException(request,response,handler,ex);if(exMv!=null){break;// 第一个能处理的 Resolver 返回结果后立即停止}}if(exMv!=null){returnexMv;}// 没有任何 Resolver 能处理,将异常重新抛出throwex;}

这里出现了一个关键的列表:handlerExceptionResolvers。那么,这个列表是从哪里来的?

1.4handlerExceptionResolvers列表是从哪里来的?

DispatcherServlet在启动时的initStrategies()阶段,会调用initHandlerExceptionResolvers()来初始化这个列表:

privatevoidinitHandlerExceptionResolvers(ApplicationContextcontext){this.handlerExceptionResolvers=null;// 从 Spring 容器中找出所有 HandlerExceptionResolver 类型的 BeanMap<String,HandlerExceptionResolver>matchingBeans=BeanFactoryUtils.beansOfTypeIncludingAncestors(context,HandlerExceptionResolver.class,true,false);if(!matchingBeans.isEmpty()){this.handlerExceptionResolvers=newArrayList<>(matchingBeans.values());// 按 Order 排序AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);}}

在 Spring Boot 的默认自动配置中,这个列表包含三个实现类:

顺序实现类处理目标
1ExceptionHandlerExceptionResolver@ControllerAdvice+@ExceptionHandler
2ResponseStatusExceptionResolver异常类上的@ResponseStatus注解
3DefaultHandlerExceptionResolverSpring 内置标准异常

ExceptionHandlerExceptionResolver的Order值最小(0),所以它会最先被尝试。

第二章:ExceptionHandlerExceptionResolver是如何介入的?

2.1 为什么是它最先被调用?

ExceptionHandlerExceptionResolver实现了Ordered接口,getOrder()返回0。而其他两个 Resolver 的Order值更大,所以它排在列表的最前面,在processHandlerException遍历时第一个被执行。

2.2resolveException→doResolveHandlerMethodException

当遍历到ExceptionHandlerExceptionResolver时,会调用它的resolveException方法,最终进入doResolveHandlerMethodException:

protectedModelAndViewdoResolveHandlerMethodException(HttpServletRequestrequest,HttpServletResponseresponse,HandlerMethodhandlerMethod,Exceptionexception){// 1. 查找能处理当前异常的 @ExceptionHandler 方法ServletInvocableHandlerMethodexceptionHandlerMethod=getExceptionHandlerMethod(handlerMethod,exception);if(exceptionHandlerMethod==null){returnnull;// 没找到,交给下一个 Resolver}// 2. 设置参数解析器、返回值处理器、数据绑定工厂// (复用普通 Controller 的同一套工具)exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);exceptionHandlerMethod.setDataBinderFactory(getDataBinderFactory(handlerMethod));// 3. 创建 ModelAndView 容器ModelAndViewContainermavContainer=newModelAndViewContainer();// 4. 执行异常处理方法exceptionHandlerMethod.invokeAndHandle(webRequest,mavContainer,exception,...);// 5. 返回 ModelAndView(可能为 null)returngetModelAndView(mavContainer,modelFactory,webRequest);}

2.3getExceptionHandlerMethod:查找能处理当前异常的方法

查找逻辑分为两个阶段,局部优先于全局:

第一阶段:从exceptionHandlerCache查找 Controller 内部的@ExceptionHandler

// exceptionHandlerCache 是懒加载的ExceptionHandlerMethodResolverresolver=this.exceptionHandlerCache.get(handlerType);if(resolver==null){resolver=newExceptionHandlerMethodResolver(handlerType);this.exceptionHandlerCache.put(handlerType,resolver);}Methodmethod=resolver.resolveMethod(exception);if(method!=null){returnnewServletInvocableHandlerMethod(handlerMethod.getBean(),method,...);}

如果 Controller 自己定义了@ExceptionHandler,它会优先处理,全局@ControllerAdvice不会生效。

第二阶段:遍历exceptionHandlerAdviceCache查找@ControllerAdvice

for(Map.Entry<ControllerAdviceBean,ExceptionHandlerMethodResolver>entry:this.exceptionHandlerAdviceCache.entrySet()){ControllerAdviceBeanadvice=entry.getKey();// 检查这个 Advice 是否适用于当前 Controllerif(advice.isApplicableToBeanType(handlerType)){ExceptionHandlerMethodResolverresolver=entry.getValue();Methodmethod=resolver.resolveMethod(exception);if(method!=null){returnnewServletInvocableHandlerMethod(advice.resolveBean(),method,...);}}}returnnull;

2.4 查找细节

2.4.1advice.isApplicableToBeanType():适用范围过滤

@ControllerAdvice可以通过以下属性限制生效范围:

属性作用
默认(无配置)所有 Controller
assignableTypes只对指定的 Controller 类生效
basePackages只对指定包下的 Controller 生效
annotations只对标注了特定注解的 Controller 生效

只有当当前 Controller 满足这些条件时,该ControllerAdvice才会被纳入查找范围。

2.4.2resolver.resolveMethod(exception):异常类型层级匹配

ExceptionHandlerMethodResolver内部维护了一个Map<Class<? extends Throwable>, Method>,查找时按以下顺序:

  1. 精确匹配:查找 Key 是否为exception.getClass()
  2. 父类匹配:向上查找exception.getClass().getSuperclass()
  3. 接口匹配:查找异常类实现的接口
  4. 递归向上:重复 2 和 3,一直追溯到Throwable.class

“最具体优先”原则:如果有多个匹配,选择异常类型继承树上距离最近的那个。例如UserNotFoundException会优先匹配@ExceptionHandler(UserNotFoundException.class),而不是@ExceptionHandler(RuntimeException.class)。

第三章:exceptionHandlerAdviceCache是从哪里来的?

3.1 启动时已经扫描好了

在第二章中,我们看到运行时查找@ExceptionHandler依赖于exceptionHandlerAdviceCache。这个缓存不是在运行时构建的,而是在启动阶段就已经准备好了。

ExceptionHandlerExceptionResolver实现了InitializingBean,在afterPropertiesSet()中调用了initExceptionHandlerAdviceCache():

protectedvoidinitExceptionHandlerAdviceCache(){// 1. 从容器中找出所有标注了 @ControllerAdvice 的 BeanList<ControllerAdviceBean>adviceBeans=ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());for(ControllerAdviceBeanadviceBean:adviceBeans){// 2. 解析该 Bean 中所有 @ExceptionHandler 方法ExceptionHandlerMethodResolverresolver=newExceptionHandlerMethodResolver(adviceBean.getBeanType());if(resolver.hasExceptionMappings()){// 3. 存入缓存this.exceptionHandlerAdviceCache.put(adviceBean,resolver);}}}

3.2 缓存的数据结构

exceptionHandlerAdviceCache └── Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> │ └── ExceptionHandlerMethodResolver └── Map<异常类型, 处理方法>
  • ControllerAdviceBean:封装了@ControllerAdvice类的实例和元数据
  • ExceptionHandlerMethodResolver:解析并缓存该 Advice 中所有@ExceptionHandler方法,建立异常类型 → Method的映射

3.3exceptionHandlerCache是懒加载的

与exceptionHandlerAdviceCache不同,exceptionHandlerCache(存储 Controller 内部的@ExceptionHandler)是懒加载的,在运行时第一个异常到达时才构建:

ExceptionHandlerMethodResolverresolver=this.exceptionHandlerCache.get(handlerType);if(resolver==null){resolver=newExceptionHandlerMethodResolver(handlerType);this.exceptionHandlerCache.put(handlerType,resolver);}

第四章:找到方法后,怎么执行?

4.1 包装成ServletInvocableHandlerMethod

一旦找到匹配的exceptionHandlerMethod,Spring 会把它包装成ServletInvocableHandlerMethod,并给它装配上:

  • 参数解析器:和普通 Controller 用的是同一套(30+ 个)
  • 返回值处理器:支持@ResponseBody、ResponseEntity等
  • 数据绑定工厂:支持@RequestParam、@ModelAttribute等

这意味着@ExceptionHandler方法和普通 Controller 方法拥有完全相同的参数和返回值处理能力。

4.2 执行“三连击”

invokeAndHandle内部执行三个步骤:

第一击:参数解析

  • Exception e→ 自动注入当前抛出的异常对象
  • HttpServletRequest→ 自动注入原始请求对象
  • @RequestParam→ 从请求参数中取值
  • @RequestBody→ 从请求体中解析 JSON

第二击:反射调用

ObjectreturnValue=method.invoke(bean,args);

进入你写的@ExceptionHandler方法,执行业务逻辑。

第三击:返回值处理

返回值类型处理方式
有@ResponseBody/@RestControllerAdvice通过HttpMessageConverter输出 JSON
ResponseEntity<T>自定义状态码和响应头,body 通过HttpMessageConverter输出
String(视图名)封装为ModelAndView,交给ViewResolver渲染

4.3processDispatchResult收尾

异常处理方法执行完成后,返回到processDispatchResult:

  • 如果有ModelAndView,执行视图渲染
  • 如果是 JSON 响应,已通过HttpMessageConverter写入response流
  • 最终调用mappedHandler.triggerAfterCompletion(),触发拦截器的afterCompletion方法

第五章:完整时序图 + 关键断点坐标

5.1 从启动扫描到运行时执行的完整流程

【启动阶段】 ExceptionHandlerExceptionResolver.afterPropertiesSet() │ ├── initExceptionHandlerAdviceCache() │ │ │ ├── findAnnotatedBeans() → 找出所有 @ControllerAdvice │ └── 构建 exceptionHandlerAdviceCache │ ├── Key: ControllerAdviceBean │ └── Value: ExceptionHandlerMethodResolver │ └── Map<异常类型, 处理方法> 【运行时】 Controller 抛出异常 │ ↓ doDispatch() catch → dispatchException │ ↓ processDispatchResult() → processHandlerException() │ ├── 遍历 handlerExceptionResolvers │ (列表在启动时由 initHandlerExceptionResolvers() 构建) │ ↓ ├── ExceptionHandlerExceptionResolver.resolveException() │ ↓ ├── doResolveHandlerMethodException() │ ├── getExceptionHandlerMethod() │ │ ├── exceptionHandlerCache(局部,懒加载) │ │ └── exceptionHandlerAdviceCache(全局,启动时构建) │ │ ├── advice.isApplicableToBeanType() │ │ └── resolver.resolveMethod() │ │ └── 精确 → 父类 → 接口 → Throwable │ │ │ ├── 设置参数解析器、返回值处理器 │ │ │ └── invokeAndHandle() │ ├── getMethodArgumentValues() │ ├── doInvoke() │ └── handleReturnValue() │ ├── @ResponseBody → HttpMessageConverter 写 JSON │ └── 其他 → 封装 ModelAndView │ └── processDispatchResult() 收尾 → 拦截器 afterCompletion

5.2 核心设计思想

设计思想体现
策略模式HandlerExceptionResolver接口定义了可插拔的异常处理策略,多个实现类各司其职
优先级控制Order接口控制 Resolver 的执行顺序,局部优先于全局
复用设计@ExceptionHandler方法与普通 Controller 共享同一套参数解析器和返回值处理器
缓存机制exceptionHandlerAdviceCache启动时构建,运行时直接使用,避免重复扫描

5.3 优先级总结

维度优先级规则
Resolver 执行顺序ExceptionHandlerExceptionResolver(Order=0)>ResponseStatusExceptionResolver>DefaultHandlerExceptionResolver
异常处理位置局部(Controller 内部)> 全局(@ControllerAdvice)
异常类型匹配精确匹配 > 父类匹配 > 接口匹配 >Throwable
全局 Advice 排序@Order数值越小优先级越高(默认最低优先级)

5.4 关键断点坐标

断点位置作用
DispatcherServlet.processDispatchResult()异常处理总入口
DispatcherServlet.processHandlerException()遍历 Resolver 列表
ExceptionHandlerExceptionResolver.doResolveHandlerMethodException()核心查找入口
ExceptionHandlerExceptionResolver.getExceptionHandlerMethod()局部/全局匹配逻辑
ExceptionHandlerMethodResolver.resolveMethod()异常类型层级匹配
ServletInvocableHandlerMethod.invokeAndHandle()执行三连击的入口
AbstractMessageConverterMethodProcessor.writeWithMessageConverters()JSON 序列化并写出

本文从doDispatch的try-catch出发,一路追到processHandlerException遍历 Resolver 列表,深入ExceptionHandlerExceptionResolver的getExceptionHandlerMethod查找逻辑,覆盖了启动扫描、运行时匹配、反射调用、返回值处理的全链路,最后给出完整时序图和断点坐标,帮助你在源码中建立清晰的定位。😊

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进