Spring Boot 全局異常處理策略設(shè)計(jì)之@ExceptionHandler 與 @ControllerAdvice 生效原理源碼解析
Spring Boot 全局異常處理策略設(shè)計(jì)(三):@ExceptionHandler 與 @ControllerAdvice 生效原理源碼解析
1. 從一個(gè)常見疑問說起
很多人在使用全局異常處理時(shí),都會(huì)遇到類似問題:
- 為什么這個(gè)異常沒進(jìn)我的 @ControllerAdvice?
- 多個(gè) @ExceptionHandler 時(shí),哪個(gè)先生效?
- 參數(shù)、返回值為什么能自動(dòng)解析?
- 為什么同一個(gè)異常,在不同 Controller 表現(xiàn)不一樣?
這些問題,用“注解怎么寫”是回答不了的,只能從源碼解釋。
2. ExceptionHandlerExceptionResolver 是誰在干活
在上一篇中我們已經(jīng)知道,異常最終會(huì)進(jìn)入這條責(zé)任鏈:
ExceptionHandlerExceptionResolver → ResponseStatusExceptionResolver → DefaultHandlerExceptionResolver
而 @ExceptionHandler 和 @ControllerAdvice 的真正執(zhí)行者,就是:
ExceptionHandlerExceptionResolver
3. ExceptionHandlerExceptionResolver 的核心職責(zé)
從類注釋就能看出它的定位:
/**
* An {@link HandlerExceptionResolver} that resolves exceptions through
* {@link ExceptionHandler} methods.
*/
它只做一件事:
找到能處理當(dāng)前異常的 @ExceptionHandler 方法,并執(zhí)行它
4. @ExceptionHandler 方法是如何被掃描的
4.1 初始化階段:掃描所有異常處理方法
在容器啟動(dòng)階段,ExceptionHandlerExceptionResolver 會(huì)執(zhí)行初始化邏輯:
public void afterPropertiesSet() {
initExceptionHandlerAdviceCache();
}
4.2 掃描 @ControllerAdvice
private void initExceptionHandlerAdviceCache() {
List<ControllerAdviceBean> adviceBeans =
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
}
關(guān)鍵點(diǎn):
- 掃描整個(gè) Spring 容器
- 找出所有標(biāo)注了 @ControllerAdvice 的 Bean
- 封裝為 ControllerAdviceBean
4.3 ControllerAdvice 的“作用范圍”不是全局那么簡(jiǎn)單
@ControllerAdvice 支持條件匹配:
@ControllerAdvice(
basePackages = "com.example.web",
annotations = RestController.class
)
源碼中通過 HandlerTypePredicate 判斷是否適用當(dāng)前 Controller。
?? 這也是為什么:
有些 Advice 明明存在,卻對(duì)某些 Controller 不生效
5. @ExceptionHandler 方法是如何被緩存的
5.1 ExceptionHandlerMethodResolver
每一個(gè) Controller 或 Advice,都會(huì)對(duì)應(yīng)一個(gè)解析器:
new ExceptionHandlerMethodResolver(beanType);
它會(huì):
- 掃描所有方法
- 找出 @ExceptionHandler
- 建立異常類型 → 方法的映射關(guān)系
5.2 一個(gè)方法可以處理多個(gè)異常
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ErrorResponse handle(Exception e) {}
源碼中會(huì)把它拆解成多條映射關(guān)系。
5.3 異常匹配是“最近優(yōu)先”
如果存在繼承關(guān)系:
RuntimeException └── IllegalArgumentException
IllegalArgumentException 會(huì)優(yōu)先匹配,而不是父類異常。
這是通過 ExceptionDepthComparator 實(shí)現(xiàn)的。
6. 異常發(fā)生時(shí),Resolver 是如何找方法的
異常真正發(fā)生后,會(huì)進(jìn)入:
protected ModelAndView doResolveHandlerMethodException(...)
核心邏輯:
- 先找 Controller 內(nèi)部的 @ExceptionHandler
- 再找全局 @ControllerAdvice
- 找到就執(zhí)行,找不到返回 null
6.1 Controller 內(nèi)部?jī)?yōu)先于 ControllerAdvice
這是一個(gè)非常重要的優(yōu)先級(jí)規(guī)則:
局部異常處理 > 全局異常處理
源碼中體現(xiàn)為:
getExceptionHandlerMethod(handlerMethod, exception)
先基于當(dāng)前 Controller 查找。
7. @ExceptionHandler 方法是如何被執(zhí)行的
一旦找到目標(biāo)方法,Spring 會(huì)把它包裝成:
ServletInvocableHandlerMethod
這個(gè)類你在 MVC 參數(shù)解析中已經(jīng)見過。
7.1 參數(shù)是如何自動(dòng)注入的
@ExceptionHandler(Exception.class)
public ErrorResponse handle(
Exception ex,
HttpServletRequest request
) {}
參數(shù)解析復(fù)用的正是:
- HandlerMethodArgumentResolver 體系
?? 異常處理方法,本質(zhì)上也是一個(gè) MVC 方法。
7.2 返回值是如何寫入響應(yīng)的
返回值處理同樣復(fù)用:
- HandlerMethodReturnValueHandler
- HttpMessageConverter
所以你可以:
- 返回對(duì)象
- 返回 ResponseEntity
- 返回 void
8. 為什么 @ResponseBody 能生效
在 Spring Boot 中,常見寫法是:
@RestControllerAdvice
它本質(zhì)等價(jià)于:
@ControllerAdvice @ResponseBody
@ResponseBody 的解析發(fā)生在:
- 返回值處理階段
- 由 RequestResponseBodyMethodProcessor 完成
9. 多個(gè) @ControllerAdvice 的執(zhí)行順序
9.1 順序規(guī)則
優(yōu)先級(jí)由以下規(guī)則決定:
- @Order
- Ordered 接口
- 默認(rèn)順序(最低優(yōu)先級(jí))
@Order(1)
@RestControllerAdvice
class BizExceptionAdvice {}
@Order(2)
@RestControllerAdvice
class SystemExceptionAdvice {}9.2 為什么順序很重要
因?yàn)椋?/p>
- 第一個(gè)匹配成功的異常處理方法會(huì)直接返回
- 后續(xù) Advice 不再執(zhí)行
10. 常見“異常不生效”的根本原因
| 現(xiàn)象 | 根本原因 |
|---|---|
| Advice 不生效 | basePackages 不匹配 |
| 方法不進(jìn) | 異常類型不匹配 |
| 被吞掉 | 前面 Resolver 已處理 |
| 返回 500 | Resolver 返回 null |
這些問題,只有看源碼才能徹底理解。
11. 異常處理方法執(zhí)行流程圖

圖1 @ExceptionHandler 方法解析與執(zhí)行流程
12. 本篇關(guān)鍵認(rèn)知升級(jí)
到這里,你應(yīng)該已經(jīng)清楚:
- @ExceptionHandler 并不是“魔法”
- ControllerAdvice 不是“全局兜底”,而是有嚴(yán)格匹配規(guī)則
- 異常處理方法本質(zhì)上是一個(gè) MVC Handler
- Resolver 鏈決定了異常的最終命運(yùn)
13. 下一篇預(yù)告
到目前為止,我們講的還是 Spring MVC 層面的異常。
但在 Spring Boot 中,還有一個(gè)繞不開的存在:
/error
- 它什么時(shí)候被觸發(fā)?
- 為什么有的異常進(jìn)了 /error,而不是 ControllerAdvice?
- ErrorController、ErrorAttributes 是干什么的?
?? 下一篇,我們正式進(jìn)入 Spring Boot 的異常“二次封裝世界”。
參考資料
- Spring Framework Reference – Exception Handling
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html - ExceptionHandlerExceptionResolver 源碼
https://github.com/spring-projects/spring-framework
到此這篇關(guān)于Spring Boot 全局異常處理策略設(shè)計(jì)之@ExceptionHandler 與 @ControllerAdvice 生效原理源碼解析的文章就介紹到這了,更多相關(guān)Spring Boot 全局異常內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis之動(dòng)態(tài)SQL和緩存使用及說明
這篇文章主要介紹了MyBatis之動(dòng)態(tài)SQL和緩存使用及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2026-05-05
解決idea打包成功但是resource下的文件沒有成功的問題
這篇文章主要介紹了解決idea打包成功但是resource下的文件沒有成功的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08
詳解Java關(guān)于JDK中時(shí)間日期的API
這篇文章主要介紹了詳解Java關(guān)于JDK中時(shí)間日期的API,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09
SpringBoot中@ComponentScan注解過濾排除不加載某個(gè)類的3種方法
這篇文章主要給大家介紹了關(guān)于SpringBoot中@ComponentScan注解過濾排除不加載某個(gè)類的3種方法,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用SpringBoot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-07-07
LogBack+MybatisPlus實(shí)現(xiàn)打印sql日志
本文主要介紹了LogBack+MybatisPlus實(shí)現(xiàn)打印sql日志,設(shè)置異步appender實(shí)現(xiàn)按天滾動(dòng)切割日志,保留3天歷史并自動(dòng)清理,替代P6Spy避免日志過大,同時(shí)需配置MybatisPlus日志輸出路徑,并區(qū)分MyBatis與MyBatisPlus日志輸出2025-09-09
教你怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧
本篇文章為大家詳細(xì)介紹了怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧,文中有非常詳細(xì)的代碼示例及注釋,對(duì)正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05
Springboot發(fā)送post請(qǐng)求的幾種方式總結(jié)
這篇文章主要為大家詳細(xì)介紹了Springboot發(fā)送post請(qǐng)求的幾種方式,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以了解一下2024-01-01

