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

文章目录

  • [Spring Boot 全局异常处理策略设计(三):@ExceptionHandler 与 @ControllerAdvice 生效原理源码解析](#Spring Boot 全局异常处理策略设计(三):@ExceptionHandler 与 @ControllerAdvice 生效原理源码解析)
    • [1. 从一个常见疑问说起](#1. 从一个常见疑问说起)
    • [2. ExceptionHandlerExceptionResolver 是谁在干活](#2. ExceptionHandlerExceptionResolver 是谁在干活)
    • [3. ExceptionHandlerExceptionResolver 的核心职责](#3. ExceptionHandlerExceptionResolver 的核心职责)
    • [4. @ExceptionHandler 方法是如何被扫描的](#4. @ExceptionHandler 方法是如何被扫描的)
      • [4.1 初始化阶段:扫描所有异常处理方法](#4.1 初始化阶段:扫描所有异常处理方法)
      • [4.2 扫描 @ControllerAdvice](#4.2 扫描 @ControllerAdvice)
      • [4.3 ControllerAdvice 的"作用范围"不是全局那么简单](#4.3 ControllerAdvice 的“作用范围”不是全局那么简单)
    • [5. @ExceptionHandler 方法是如何被缓存的](#5. @ExceptionHandler 方法是如何被缓存的)
      • [5.1 ExceptionHandlerMethodResolver](#5.1 ExceptionHandlerMethodResolver)
      • [5.2 一个方法可以处理多个异常](#5.2 一个方法可以处理多个异常)
      • [5.3 异常匹配是"最近优先"](#5.3 异常匹配是“最近优先”)
    • [6. 异常发生时,Resolver 是如何找方法的](#6. 异常发生时,Resolver 是如何找方法的)
      • [6.1 Controller 内部优先于 ControllerAdvice](#6.1 Controller 内部优先于 ControllerAdvice)
    • [7. @ExceptionHandler 方法是如何被执行的](#7. @ExceptionHandler 方法是如何被执行的)
      • [7.1 参数是如何自动注入的](#7.1 参数是如何自动注入的)
      • [7.2 返回值是如何写入响应的](#7.2 返回值是如何写入响应的)
    • [8. 为什么 @ResponseBody 能生效](#8. 为什么 @ResponseBody 能生效)
    • [9. 多个 @ControllerAdvice 的执行顺序](#9. 多个 @ControllerAdvice 的执行顺序)
      • [9.1 顺序规则](#9.1 顺序规则)
      • [9.2 为什么顺序很重要](#9.2 为什么顺序很重要)
    • [10. 常见"异常不生效"的根本原因](#10. 常见“异常不生效”的根本原因)
    • [11. 异常处理方法执行流程图](#11. 异常处理方法执行流程图)
    • [12. 本篇关键认知升级](#12. 本篇关键认知升级)
    • [13. 下一篇预告](#13. 下一篇预告)
    • 参考资料
    • 结束语

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

1. 从一个常见疑问说起

很多人在使用全局异常处理时,都会遇到类似问题:

  • 为什么这个异常没进我的 @ControllerAdvice?
  • 多个 @ExceptionHandler 时,哪个先生效?
  • 参数、返回值为什么能自动解析?
  • 为什么同一个异常,在不同 Controller 表现不一样?

这些问题,用"注解怎么写"是回答不了的,只能从源码解释


2. ExceptionHandlerExceptionResolver 是谁在干活

在上一篇中我们已经知道,异常最终会进入这条责任链:

复制代码
ExceptionHandlerExceptionResolver
→ ResponseStatusExceptionResolver
→ DefaultHandlerExceptionResolver

@ExceptionHandler 和 @ControllerAdvice 的真正执行者,就是:

ExceptionHandlerExceptionResolver


3. ExceptionHandlerExceptionResolver 的核心职责

从类注释就能看出它的定位:

java 复制代码
/**
 * An {@link HandlerExceptionResolver} that resolves exceptions through
 * {@link ExceptionHandler} methods.
 */

它只做一件事:

找到能处理当前异常的 @ExceptionHandler 方法,并执行它


4. @ExceptionHandler 方法是如何被扫描的

4.1 初始化阶段:扫描所有异常处理方法

在容器启动阶段,ExceptionHandlerExceptionResolver 会执行初始化逻辑:

java 复制代码
public void afterPropertiesSet() {
    initExceptionHandlerAdviceCache();
}

4.2 扫描 @ControllerAdvice

java 复制代码
private void initExceptionHandlerAdviceCache() {
    List<ControllerAdviceBean> adviceBeans =
        ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
}

关键点:

  • 扫描整个 Spring 容器
  • 找出所有标注了 @ControllerAdvice 的 Bean
  • 封装为 ControllerAdviceBean

4.3 ControllerAdvice 的"作用范围"不是全局那么简单

@ControllerAdvice 支持条件匹配:

java 复制代码
@ControllerAdvice(
    basePackages = "com.example.web",
    annotations = RestController.class
)

源码中通过 HandlerTypePredicate 判断是否适用当前 Controller。

👉 这也是为什么:

有些 Advice 明明存在,却对某些 Controller 不生效


5. @ExceptionHandler 方法是如何被缓存的

5.1 ExceptionHandlerMethodResolver

每一个 Controller 或 Advice,都会对应一个解析器:

java 复制代码
new ExceptionHandlerMethodResolver(beanType);

它会:

  • 扫描所有方法
  • 找出 @ExceptionHandler
  • 建立异常类型 → 方法的映射关系

5.2 一个方法可以处理多个异常

java 复制代码
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ErrorResponse handle(Exception e) {}

源码中会把它拆解成多条映射关系。


5.3 异常匹配是"最近优先"

如果存在继承关系:

text 复制代码
RuntimeException
└── IllegalArgumentException

IllegalArgumentException 会优先匹配,而不是父类异常。

这是通过 ExceptionDepthComparator 实现的。


6. 异常发生时,Resolver 是如何找方法的

异常真正发生后,会进入:

java 复制代码
protected ModelAndView doResolveHandlerMethodException(...)

核心逻辑:

  1. 先找 Controller 内部的 @ExceptionHandler
  2. 再找全局 @ControllerAdvice
  3. 找到就执行,找不到返回 null

6.1 Controller 内部优先于 ControllerAdvice

这是一个非常重要的优先级规则:

局部异常处理 > 全局异常处理

源码中体现为:

java 复制代码
getExceptionHandlerMethod(handlerMethod, exception)

先基于当前 Controller 查找。


7. @ExceptionHandler 方法是如何被执行的

一旦找到目标方法,Spring 会把它包装成:

java 复制代码
ServletInvocableHandlerMethod

这个类你在 MVC 参数解析中已经见过。


7.1 参数是如何自动注入的

java 复制代码
@ExceptionHandler(Exception.class)
public ErrorResponse handle(
    Exception ex,
    HttpServletRequest request
) {}

参数解析复用的正是:

  • HandlerMethodArgumentResolver 体系

👉 异常处理方法,本质上也是一个 MVC 方法。


7.2 返回值是如何写入响应的

返回值处理同样复用:

  • HandlerMethodReturnValueHandler
  • HttpMessageConverter

所以你可以:

  • 返回对象
  • 返回 ResponseEntity
  • 返回 void

8. 为什么 @ResponseBody 能生效

在 Spring Boot 中,常见写法是:

java 复制代码
@RestControllerAdvice

它本质等价于:

java 复制代码
@ControllerAdvice
@ResponseBody

@ResponseBody 的解析发生在:

  • 返回值处理阶段
  • 由 RequestResponseBodyMethodProcessor 完成

9. 多个 @ControllerAdvice 的执行顺序

9.1 顺序规则

优先级由以下规则决定:

  1. @Order
  2. Ordered 接口
  3. 默认顺序(最低优先级)
java 复制代码
@Order(1)
@RestControllerAdvice
class BizExceptionAdvice {}

@Order(2)
@RestControllerAdvice
class SystemExceptionAdvice {}

9.2 为什么顺序很重要

因为:

  • 第一个匹配成功的异常处理方法会直接返回
  • 后续 Advice 不再执行

10. 常见"异常不生效"的根本原因

现象 根本原因
Advice 不生效 basePackages 不匹配
方法不进 异常类型不匹配
被吞掉 前面 Resolver 已处理
返回 500 Resolver 返回 null

这些问题,只有看源码才能彻底理解


11. 异常处理方法执行流程图





异常抛出
ExceptionHandlerExceptionResolver
Controller 内是否有 @ExceptionHandler
执行 Controller 内方法
是否匹配 ControllerAdvice
执行 Advice 方法
返回 null

图1 @ExceptionHandler 方法解析与执行流程


12. 本篇关键认知升级

到这里,你应该已经清楚:

  1. @ExceptionHandler 并不是"魔法"
  2. ControllerAdvice 不是"全局兜底",而是有严格匹配规则
  3. 异常处理方法本质上是一个 MVC Handler
  4. Resolver 链决定了异常的最终命运

13. 下一篇预告

到目前为止,我们讲的还是 Spring MVC 层面的异常

但在 Spring Boot 中,还有一个绕不开的存在:

/error

  • 它什么时候被触发?
  • 为什么有的异常进了 /error,而不是 ControllerAdvice?
  • ErrorController、ErrorAttributes 是干什么的?

👉 下一篇,我们正式进入 Spring Boot 的异常"二次封装世界"。


参考资料

结束语

掘金点击访问Qiuner CSDN点击访问Qiuner GitHub点击访问Qiuner Gitee点击访问Qiuner

专栏 简介
📊 一图读懂系列 图文并茂,轻松理解复杂概念
📝 一文读懂系列 深入浅出,全面解析技术要点
🌟持续更新 保持学习,不断进步
🎯 人生经验 经验分享,共同成长

你好,我是Qiuner. 为帮助别人少走弯路而写博客

如果本篇文章帮到了你 不妨点个 吧~ 我会很高兴的 😄 (^ ~ ^) 。想看更多 那就点个关注吧 我会尽力带来有趣的内容 😎。

代码都在Github或Gitee上,如有需要可以去上面自行下载。记得给我点星星哦😍

如果你遇到了问题,自己没法解决,可以去我掘金评论区问。CSDN评论区和私信消息看不完 掘金消息少一点.

上一篇推荐 链接
Java程序员快又扎实的学习路线 点击该处自动跳转查看哦
一文读懂 AI 点击该处自动跳转查看哦
一文读懂 服务器 点击该处自动跳转查看哦
2024年创作回顾 点击该处自动跳转查看哦
一文读懂 ESLint配置 点击该处自动跳转查看哦
老鸟如何追求快捷操作电脑 点击该处自动跳转查看哦
未来会写什么文章? 预告链接
一文读懂 XX? 点击该处自动跳转查看哦
2025年终总结 点击该处自动跳转查看哦
一图读懂 XX? 点击该处自动跳转查看哦

掘金点击访问Qiuner CSDN点击访问Qiuner GitHub点击访问Qiuner Gitee点击访问Qiuner

专栏 简介
📊 一图读懂系列 图文并茂,轻松理解复杂概念
📝 一文读懂系列 深入浅出,全面解析技术要点
🌟持续更新 保持学习,不断进步
🎯 人生经验 经验分享,共同成长

你好,我是Qiuner. 为帮助别人少走弯路而写博客

如果本篇文章帮到了你 不妨点个 吧~ 我会很高兴的 😄 (^ ~ ^) 。想看更多 那就点个关注吧 我会尽力带来有趣的内容 😎。

代码都在Github或Gitee上,如有需要可以去上面自行下载。记得给我点星星哦😍

如果你遇到了问题,自己没法解决,可以去我掘金评论区问。CSDN评论区和私信消息看不完 掘金消息少一点.

上一篇推荐 链接
Java程序员快又扎实的学习路线 点击该处自动跳转查看哦
一文读懂 AI 点击该处自动跳转查看哦
一文读懂 服务器 点击该处自动跳转查看哦
2024年创作回顾 点击该处自动跳转查看哦
一文读懂 ESLint配置 点击该处自动跳转查看哦
老鸟如何追求快捷操作电脑 点击该处自动跳转查看哦
未来会写什么文章? 预告链接
一文读懂 XX? 点击该处自动跳转查看哦
2025年终总结 点击该处自动跳转查看哦
一图读懂 XX? 点击该处自动跳转查看哦
相关推荐
零度@17 小时前
Java 消息中间件 - RabbitMQ 全解(保姆级 2026)
java·rabbitmq·java-rabbitmq
u01040583617 小时前
企业微信自建应用权限模型与 RBAC 在 Spring Security 中的映射
java·spring·企业微信
墨雨晨曦8817 小时前
通过调用deepseek的api来实现智能客服
java
予枫的编程笔记17 小时前
Elasticsearch核心架构与基础原理:解密其极速性能的底层逻辑
java·大数据·人工智能·elasticsearch·搜索引擎·架构·全文检索
Seven9717 小时前
数据结构-图
java
Yu_iChan17 小时前
苍穹外卖Day09 地址簿模块
java·数据库·mybatis
Java天梯之路17 小时前
Spring Boot 钩子全集实战(五):ApplicationContextInitializer详解
java·spring boot·后端
后端小张17 小时前
【AI 学习】AI提示词工程:从入门到实战的全栈指南
java·人工智能·深度学习·学习·语言模型·prompt·知识图谱