白水关小昭 發表於 2026-1-9 09:48:02

Spring Boot 全局异常处理策略设计之@ExceptionHandler 与 @ControllerAdvice 生效原理源码解析

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>Spring Boot 全局异常处理策略设计(三):@ExceptionHandler 与 @ControllerAdvice 生效原理源码解析</li><ul class="second_class_ul"><li>1. 从一个常见疑问说起</li><li>2. ExceptionHandlerExceptionResolver 是谁在干活</li><li>3. ExceptionHandlerExceptionResolver 的核心职责</li><li>4. @ExceptionHandler 方法是如何被扫描的</li><ul class="third_class_ul"><li>4.1 初始化阶段:扫描所有异常处理方法</li><li>4.2 扫描 @ControllerAdvice</li><li>4.3 ControllerAdvice 的&ldquo;作用范围&rdquo;不是全局那么简单</li></ul><li>5. @ExceptionHandler 方法是如何被缓存的</li><ul class="third_class_ul"><li>5.1 ExceptionHandlerMethodResolver</li><li>5.2 一个方法可以处理多个异常</li><li>5.3 异常匹配是&ldquo;最近优先&rdquo;</li></ul><li>6. 异常发生时,Resolver 是如何找方法的</li><ul class="third_class_ul"><li>6.1 Controller 内部优先于 ControllerAdvice</li></ul><li>7. @ExceptionHandler 方法是如何被执行的</li><ul class="third_class_ul"><li>7.1 参数是如何自动注入的</li><li>7.2 返回值是如何写入响应的</li></ul><li>8. 为什么 @ResponseBody 能生效</li><ul class="third_class_ul"></ul><li>9. 多个 @ControllerAdvice 的执行顺序</li><ul class="third_class_ul"><li>9.1 顺序规则</li><li>9.2 为什么顺序很重要</li></ul><li>10. 常见&ldquo;异常不生效&rdquo;的根本原因</li><ul class="third_class_ul"></ul><li>11. 异常处理方法执行流程图</li><ul class="third_class_ul"></ul><li>12. 本篇关键认知升级</li><ul class="third_class_ul"></ul><li>13. 下一篇预告</li><ul class="third_class_ul"></ul><li>参考资料</li><ul class="third_class_ul"></ul></ul></ul></div><p class="maodian"></p><h2>Spring Boot 全局异常处理策略设计(三):@ExceptionHandler 与 @ControllerAdvice 生效原理源码解析</h2>
<p class="maodian"></p><h3>1. 从一个常见疑问说起</h3>
<p>很多人在使用全局异常处理时,都会遇到类似问题:</p>
<ul><li>为什么这个异常没进我的 @ControllerAdvice?</li><li>多个 @ExceptionHandler 时,哪个先生效?</li><li>参数、返回值为什么能自动解析?</li><li>为什么同一个异常,在不同 Controller 表现不一样?</li></ul>
<p>这些问题,用&ldquo;注解怎么写&rdquo;是回答不了的,<strong>只能从源码解释</strong>。</p>
<p class="maodian"></p><h3>2. ExceptionHandlerExceptionResolver 是谁在干活</h3>
<p>在上一篇中我们已经知道,异常最终会进入这条责任链:</p>
<div class="jb51code"><pre class="brush:java;">ExceptionHandlerExceptionResolver
→ ResponseStatusExceptionResolver
→ DefaultHandlerExceptionResolver</pre></div>
<p>而 <strong>@ExceptionHandler 和 @ControllerAdvice 的真正执行者</strong>,就是:</p>
<blockquote><p><strong>ExceptionHandlerExceptionResolver</strong></p></blockquote>
<p class="maodian"></p><h3>3. ExceptionHandlerExceptionResolver 的核心职责</h3>
<p>从类注释就能看出它的定位:</p>
<div class="jb51code"><pre class="brush:java;">/**
* An {@link HandlerExceptionResolver} that resolves exceptions through
* {@link ExceptionHandler} methods.
*/
</pre></div>
<p>它只做一件事:</p>
<blockquote><p><strong>找到能处理当前异常的 @ExceptionHandler 方法,并执行它</strong></p></blockquote>
<p class="maodian"></p><h3>4. @ExceptionHandler 方法是如何被扫描的</h3>
<p class="maodian"></p><h4>4.1 初始化阶段:扫描所有异常处理方法</h4>
<p>在容器启动阶段,ExceptionHandlerExceptionResolver 会执行初始化逻辑:</p>
<div class="jb51code"><pre class="brush:java;">public void afterPropertiesSet() {
    initExceptionHandlerAdviceCache();
}
</pre></div>
<p class="maodian"></p><h4>4.2 扫描 @ControllerAdvice</h4>
<div class="jb51code"><pre class="brush:java;">private void initExceptionHandlerAdviceCache() {
    List&lt;ControllerAdviceBean&gt; adviceBeans =
      ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
}
</pre></div>
<p>关键点:</p>
<ul><li>扫描整个 Spring 容器</li><li>找出所有标注了 @ControllerAdvice 的 Bean</li><li>封装为 ControllerAdviceBean</li></ul>
<p class="maodian"></p><h4>4.3 ControllerAdvice 的&ldquo;作用范围&rdquo;不是全局那么简单</h4>
<p>@ControllerAdvice 支持条件匹配:</p>
<div class="jb51code"><pre class="brush:java;">@ControllerAdvice(
    basePackages = "com.example.web",
    annotations = RestController.class
)
</pre></div>
<p>源码中通过 <strong>HandlerTypePredicate</strong> 判断是否适用当前 Controller。</p>
<p>👉 这也是为什么:</p>
<blockquote><p><strong>有些 Advice 明明存在,却对某些 Controller 不生效</strong></p></blockquote>
<p class="maodian"></p><h3>5. @ExceptionHandler 方法是如何被缓存的</h3>
<p class="maodian"></p><h4>5.1 ExceptionHandlerMethodResolver</h4>
<p>每一个 Controller 或 Advice,都会对应一个解析器:</p>
<div class="jb51code"><pre class="brush:java;">new ExceptionHandlerMethodResolver(beanType);
</pre></div>
<p>它会:</p>
<ul><li>扫描所有方法</li><li>找出 @ExceptionHandler</li><li>建立异常类型 &rarr; 方法的映射关系</li></ul>
<p class="maodian"></p><h4>5.2 一个方法可以处理多个异常</h4>
<div class="jb51code"><pre class="brush:java;">@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ErrorResponse handle(Exception e) {}
</pre></div>
<p>源码中会把它拆解成多条映射关系。</p>
<p class="maodian"></p><h4>5.3 异常匹配是&ldquo;最近优先&rdquo;</h4>
<p>如果存在继承关系:</p>
<div class="jb51code"><pre class="brush:java;">RuntimeException
└── IllegalArgumentException
</pre></div>
<p><strong>IllegalArgumentException 会优先匹配</strong>,而不是父类异常。</p>
<p>这是通过 <code>ExceptionDepthComparator</code> 实现的。</p>
<p class="maodian"></p><h3>6. 异常发生时,Resolver 是如何找方法的</h3>
<p>异常真正发生后,会进入:</p>
<div class="jb51code"><pre class="brush:java;">protected ModelAndView doResolveHandlerMethodException(...)
</pre></div>
<p>核心逻辑:</p>
<ol><li>先找 Controller 内部的 @ExceptionHandler</li><li>再找全局 @ControllerAdvice</li><li>找到就执行,找不到返回 null</li></ol>
<p class="maodian"></p><h4>6.1 Controller 内部优先于 ControllerAdvice</h4>
<p>这是一个非常重要的优先级规则:</p>
<blockquote><p><strong>局部异常处理 &gt; 全局异常处理</strong></p></blockquote>
<p>源码中体现为:</p>
<div class="jb51code"><pre class="brush:java;">getExceptionHandlerMethod(handlerMethod, exception)
</pre></div>
<p>先基于当前 Controller 查找。</p>
<p class="maodian"></p><h3>7. @ExceptionHandler 方法是如何被执行的</h3>
<p>一旦找到目标方法,Spring 会把它包装成:</p>
<div class="jb51code"><pre class="brush:java;">ServletInvocableHandlerMethod
</pre></div>
<p>这个类你在 MVC 参数解析中已经见过。</p>
<p class="maodian"></p><h4>7.1 参数是如何自动注入的</h4>
<div class="jb51code"><pre class="brush:java;">@ExceptionHandler(Exception.class)
public ErrorResponse handle(
    Exception ex,
    HttpServletRequest request
) {}
</pre></div>
<p>参数解析复用的正是:</p>
<ul><li>HandlerMethodArgumentResolver 体系</li></ul>
<p>👉 异常处理方法,本质上也是一个 MVC 方法。</p>
<p class="maodian"></p><h4>7.2 返回值是如何写入响应的</h4>
<p>返回值处理同样复用:</p>
<ul><li>HandlerMethodReturnValueHandler</li><li>HttpMessageConverter</li></ul>
<p>所以你可以:</p>
<ul><li>返回对象</li><li>返回 ResponseEntity</li><li>返回 void</li></ul>
<p class="maodian"></p><h3>8. 为什么 @ResponseBody 能生效</h3>
<p>在 Spring Boot 中,常见写法是:</p>
<div class="jb51code"><pre class="brush:java;">@RestControllerAdvice
</pre></div>
<p>它本质等价于:</p>
<div class="jb51code"><pre class="brush:java;">@ControllerAdvice
@ResponseBody
</pre></div>
<p>@ResponseBody 的解析发生在:</p>
<ul><li>返回值处理阶段</li><li>由 RequestResponseBodyMethodProcessor 完成</li></ul>
<p class="maodian"></p><h3>9. 多个 @ControllerAdvice 的执行顺序</h3>
<p class="maodian"></p><h4>9.1 顺序规则</h4>
<p>优先级由以下规则决定:</p>
<ol><li>@Order</li><li>Ordered 接口</li><li>默认顺序(最低优先级)</li></ol>
<div class="jb51code"><pre class="brush:java;">@Order(1)
@RestControllerAdvice
class BizExceptionAdvice {}
@Order(2)
@RestControllerAdvice
class SystemExceptionAdvice {}</pre></div>
<p class="maodian"></p><h4>9.2 为什么顺序很重要</h4>
<p>因为:</p>
<ul><li><strong>第一个匹配成功的异常处理方法会直接返回</strong></li><li>后续 Advice 不再执行</li></ul>
<p class="maodian"></p><h3>10. 常见&ldquo;异常不生效&rdquo;的根本原因</h3>
<table><thead><tr><th>现象</th><th>根本原因</th></tr></thead><tbody><tr><td>Advice 不生效</td><td>basePackages 不匹配</td></tr><tr><td>方法不进</td><td>异常类型不匹配</td></tr><tr><td>被吞掉</td><td>前面 Resolver 已处理</td></tr><tr><td>返回 500</td><td>Resolver 返回 null</td></tr></tbody></table>
<p>这些问题,<strong>只有看源码才能彻底理解</strong>。</p>
<p class="maodian"></p><h3>11. 异常处理方法执行流程图</h3>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/20261994555603.png" /></p>
<p style="text-align:center">图1 @ExceptionHandler 方法解析与执行流程</p>
<p class="maodian"></p><h3>12. 本篇关键认知升级</h3>
<p>到这里,你应该已经清楚:</p>
<ol><li>@ExceptionHandler 并不是&ldquo;魔法&rdquo;</li><li>ControllerAdvice 不是&ldquo;全局兜底&rdquo;,而是有严格匹配规则</li><li>异常处理方法本质上是一个 MVC Handler</li><li>Resolver 链决定了异常的最终命运</li></ol>
<p class="maodian"></p><h3>13. 下一篇预告</h3>
<p>到目前为止,我们讲的还是 <strong>Spring MVC 层面的异常</strong>。<br />但在 Spring Boot 中,还有一个绕不开的存在:</p>
<blockquote><p><strong>/error</strong></p></blockquote>
<ul><li>它什么时候被触发?</li><li>为什么有的异常进了 /error,而不是 ControllerAdvice?</li><li>ErrorController、ErrorAttributes 是干什么的?</li></ul>
<p>👉 <strong>下一篇,我们正式进入 Spring Boot 的异常&ldquo;二次封装世界&rdquo;。</strong></p>
<p class="maodian"></p><h3>参考资料</h3>
<ul><li>Spring Framework Reference &ndash; Exception Handling<br />https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html</li><li>ExceptionHandlerExceptionResolver 源码<br />https://github.com/spring-projects/spring-framework</li></ul>
<p>到此这篇关于Spring Boot 全局异常处理策略设计之@ExceptionHandler 与 @ControllerAdvice 生效原理源码解析的文章就介绍到这了,更多相关Spring Boot 全局异常内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>springboot全局异常处理方式@ControllerAdvice和@ExceptionHandler</li><li>SpringBoot&nbsp;@ExceptionHandler与@ControllerAdvice异常处理详解</li><li>SpringBoot的全局异常拦截实践过程</li><li>springboot全局异常处理方式</li><li>Spring Boot全局异常处理实战指南</li><li>Java后端Spring Boot全局异常处理最佳实践记录</li><li>SpringBoot全局异常处理方式</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: Spring Boot 全局异常处理策略设计之@ExceptionHandler 与 @ControllerAdvice 生效原理源码解析