那么當這兩件事沖突時,Spring Boot?是怎么“解決”的呢?
答案是:它不解決,也無法解決。當這種情況發生時,你的應用程序會直接啟動失敗。
這不是 Spring Boot 的疏忽,而是由 CGLIB 的底層原理和 Java 語言的規則所決定的。
工作流程和失敗原因
讓我們來模擬一下 Spring Boot 啟動時會發生什么:
- Spring 容器開始創建所有的 Bean。
- 它找到了一個需要被 AOP 增強的 Bean(例如,一個被?@Service?注解的類,并且它的方法匹配了某個?@Aspect?切面)。
- Spring Boot 查看 AOP 配置,發現默認使用 CGLIB (proxy-target-class=true)。
- 它嘗試為這個 Bean?創建一個代理。CGLIB 上場,準備動態地創建這個 Bean?的一個子類。
- 此時,CGLIB?發現這個 Bean 的類是?final?的。
- Java 語法規定?final?類不能被繼承。CGLIB 的核心機制被阻斷了。
- CGLIB 拋出一個異常。
- 這個異常會向上傳遞,最終導致 Spring 容器無法創建這個 Bean。
- Bean 創建失敗,整個 Spring 應用程序的啟動過程被中斷,并拋出?BeanCreationException?或類似的錯誤。
你通常會在控制臺日志中看到非常明確的錯誤信息,它會告訴你:
> Caused by: java.lang.IllegalArgumentException: Cannot subclass?final class com.example.YourFinalService
這個錯誤是響亮而明確的 (Fail-fast)。它在啟動時就告訴你“此路不通”,而不是在運行時產生一些難以預料的奇怪行為。
為什么 Spring Boot 仍然選擇 CGLIB 作為默認?
這是一個設計上的權衡取舍。Spring Boot 的設計者認為:
- final?業務類是少數情況:在大多數業務應用開發中,開發者很少會將自己寫的?Service?或?Component?類聲明為?final。
- 內部調用 AOP 失效問題更常見、更隱蔽:相比之下,使用 JDK 代理時,開發者在同一個類中調用?this.anotherMethod()?導致 AOP 失效的問題,是一個非常常見且容易讓人困惑的陷阱。它不會報錯,只是靜默地不工作,非常難以排查。
所以,Spring Boot?選擇了“長痛不如短痛”:
- 默認 CGLIB:解決了那個常見且隱蔽的“內部調用”問題,讓 AOP 的行為在95%的場景下都符合直覺。
- 代價:當遇到那5%的?final?類場景時,它會以一種非常直接、暴力的方式(啟動失敗)來提醒開發者。
總結:開發者如何應對?:
- 首選方案:如果代碼可控,移除?final?關鍵字。這是最簡單、最直接的修復方式。
- 備選方案:如果不能移除?final,就為這個類提取一個接口,然后在注入點使用接口,讓 AOP 可以通過 JDK 代理工作(但這可能需要你手動將?spring.aop.proxy-target-class?設置為?false,或者進行更細粒度的控制)。
- 終極方案:如果以上都不行,才考慮使用?AspectJ?靜態織入。