驚人!Spring5 AOP 預設使用Cglib ?從現象到原始碼深度分析
Spring5 AOP 預設使用 Cglib 了?我第一次聽到這個說法是在一個微信群裡:
真的假的?查閱文件
剛看到這個說法的時候,我是保持懷疑態度的。
大家都知道 Spring5 之前的版本 AOP 在預設情況下是使用 JDK 動態代理的,那是不是 Spring5 版本真的做了修改呢?於是我開啟 Spring Framework 5.x 文件,再次確認了一下:
文件地址:https://docs.spring.io/spring/docs/5.2.0.RELEASE/spring-framework-reference/core.html#aop
簡單翻譯一下。Spring AOP 預設使用 JDK 動態代理,如果物件沒有實現介面,則使用 CGLIB 代理。當然,也可以強制使用 CGLIB 代理。
什麼?文件寫錯了?!
當我把官方文件發到群裡之後,又收到了這位同學的回覆:
SpringBoot 2.x 程式碼示例
為了證明文件寫錯了,這位同學還寫了一個 DEMO。下面,就由我來重現一下這個 DEMO 程式:
執行環境:SpringBoot 2.2.0.RELEASE 版本,內建 Spring Framework 版本為 5.2.0.RELEASE 版本。同時新增 spring-boot-starter-aop 依賴,自動裝配 Spring AOP。
public interface UserService { void work(); } @Service public class UserServiceImpl implements UserService { @Override public void work() { System.out.println("開始幹活...coding..."); } }
@Component
@Aspect
public class UserServiceAspect {
@Before("execution(* com.me.aop.UserService.work(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("UserServiceAspect.....()");
}
}
UserServiceImpl
實現了UserService
介面,同時使用UserServiceAspect
對UserService#work
方法進行前置增強攔截。
從執行結果來看,這裡的確使用了 CGLIB 代理而不是 JDK 動態代理。
難道真的是文件寫錯了?!
@EnableAspectJAutoProxy 原始碼註釋
在 Spring Framework 中,是使用@EnableAspectJAutoProxy
註解來開啟 Spring AOP 相關功能的。
Spring Framework 5.2.0.RELEASE 版本@EnableAspectJAutoProxy
註解原始碼如下:
通過原始碼註釋我們可以瞭解到:在 Spring Framework 5.2.0.RELEASE 版本中,proxyTargetClass
的預設取值依舊是false
,預設還是使用 JDK 動態代理。
難道文件和原始碼註釋都寫錯了?!
@EnableAspectJAutoProxy 的 proxyTargetClass 無效了?
接下來,我嘗試使用@EnableAspectJAutoProxy
來強制使用 JDK 動態代理。
執行環境:SpringBoot 2.2.0.RELEASE 版本,內建 Spring Framework 版本為 5.2.0.RELEASE 版本。
通過執行發現,還是使用了 CGLIB 代理。難道@EnableAspectJAutoProxy
的 proxyTargetClass
設定無效了?
Spring Framework 5.x
整理一下思路
- 有人說 Spring5 開始 AOP 預設使用 CGLIB 了
- Spring Framework 5.x 文件和
@EnableAspectJAutoProxy
原始碼註釋都說了預設是使用 JDK 動態代理 - 程式執行結果說明,即使繼承了介面,設定
proxyTargetClass
為false
,程式依舊使用 CGLIB 代理
等一下,我們是不是遺漏了什麼?
示例程式是使用 SpringBoot 來執行的,那如果不用 SpringBoot,只用 Spring Framework 會怎麼樣呢?
執行環境:Spring Framework 5.2.0.RELEASE 版本。
UserServiceImpl 和 UserServiceAspect 類和上文一樣,這裡不在贅述。
執行結果表明: 在 Spring Framework 5.x 版本中,如果類實現了介面,AOP 預設還是使用 JDK 動態代理。
再整理思路
- Spring5 AOP 預設依舊使用 JDK 動態代理,官方文件和原始碼註釋沒有錯。
- SpringBoot 2.x 版本中,AOP 預設使用 cglib,且無法通過
proxyTargetClass
進行修改。 - 那是不是 SpringBoot 2.x 版本做了一些改動呢?
再探 SpringBoot 2.x
結果上面的分析,很有可能是 SpringBoot2.x 版本中,修改了 Spring AOP 的相關配置。那就來一波原始碼分析,看一下內部到底做了什麼。
原始碼分析
原始碼分析,找對入口很重要。那這次的入口在哪裡呢?
@SpringBootApplication
是一個組合註解,該註解中使用@EnableAutoConfiguration
實現了大量的自動裝配。
EnableAutoConfiguration
也是一個組合註解,在該註解上被標誌了@Import
。關於@Import
註解的詳細用法,可以參看筆者之前的文章:https://mp.weixin.qq.com/s/7arh4sVH1mlHE0GVVbZ84Q
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
AutoConfigurationImportSelector
實現了DeferredImportSelector
介面。
在 Spring Framework 4.x 版本中,這是一個空介面,它僅僅是繼承了ImportSelector
介面而已。而在 5.x 版本中拓展了DeferredImportSelector
介面,增加了一個getImportGroup
方法:
在這個方法中返回了AutoConfigurationGroup
類。這是AutoConfigurationImportSelector
中的一個內部類,他實現了DeferredImportSelector.Group
介面。
在 SpringBoot 2.x 版本中,就是通過AutoConfigurationImportSelector.AutoConfigurationGroup#process
方法來匯入自動配置類的。
通過斷點除錯可以看到,和 AOP 相關的自動配置是通過org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
來進行配置的。
真相大白
看到這裡,可以說是真相大白了。在 SpringBoot2.x 版本中,通過AopAutoConfiguration
來自動裝配 AOP。
預設情況下,是肯定沒有spring.aop.proxy-target-class
這個配置項的。而此時,在 SpringBoot 2.x 版本中會預設使用 Cglib 來實現。
SpringBoot 2.x 中如何修改 AOP 實現
通過原始碼我們也就可以知道,在 SpringBoot 2.x 中如果需要修改 AOP 的實現,需要通過spring.aop.proxy-target-class
這個配置項來修改。
#在application.properties檔案中通過spring.aop.proxy-target-class來配置
spring.aop.proxy-target-class=false
這裡也提一下spring-configuration-metadata.json
檔案的作用:在使用application.properties
或application.yml
檔案時,IDEA 就是通過讀取這些檔案資訊來提供程式碼提示的,SpringBoot 框架自己是不會來讀取這個配置檔案的。
SringBoot 1.5.x 又是怎麼樣的
可以看到,在 SpringBoot 1.5.x 版本中,預設還是使用 JDK 動態代理的。
SpringBoot 2.x 為何預設使用 Cglib
SpringBoot 2.x 版本為什麼要預設使用 Cglib 來實現 AOP 呢?這麼做的好處又是什麼呢?筆者從網上找到了一些資料,先來看一個 issue。
Spring Boot issue #5423
Use @EnableTransactionManagement(proxyTargetClass = true) #5423
https://github.com/spring-projects/spring-boot/issues/5423
在這個 issue 中,丟擲了這樣一個問題:
翻譯一下:我們應該使用@EnableTransactionManagement(proxyTargetClass = true)來防止人們不使用介面時出現討厭的代理問題。
這個"不使用介面時出現討厭的代理問題"是什麼呢?思考一分鐘。
討厭的代理問題
假設,我們有一個UserServiceImpl
和UserService
類,此時需要在UserContoller
中使用UserService
。在 Spring 中通常都習慣這樣寫程式碼:
@Autowired
UserService userService;
在這種情況下,無論是使用 JDK 動態代理,還是 CGLIB 都不會出現問題。
但是,如果你的程式碼是這樣的呢:
@Autowired
UserServiceImpl userService;
這個時候,如果我們是使用 JDK 動態代理,那在啟動時就會報錯:
因為 JDK 動態代理是基於介面的,代理生成的物件只能賦值給介面變數。
而 CGLIB 就不存在這個問題。因為 CGLIB 是通過生成子類來實現的,代理物件無論是賦值給介面還是實現類這兩者都是代理物件的父類。
SpringBoot 正是出於這種考慮,於是在 2.x 版本中,將 AOP 預設實現改為了 CGLIB。
更多的細節資訊,讀者可以自己查閱上述 issue。
總結
- Spring 5.x 中 AOP 預設依舊使用 JDK 動態代理。
- SpringBoot 2.x 開始,為了解決使用 JDK 動態代理可能導致的型別轉化異常而預設使用 CGLIB。
- 在 SpringBoot 2.x 中,如果需要預設使用 JDK 動態代理可以通過配置項
spring.aop.proxy-target-class=false
來進行修改,proxyTargetClass
配置已無效。
延伸閱讀
issue:Default CGLib proxy setting default cannot be overridden by using core framework annotations (@EnableTransactionManagement, @EnableAspectJAutoProxy) #12194
https://github.com/spring-projects/spring-boot/issues/12194
這個 issue 也聊到了關於proxyTargetClass
設定失效的問題,討論內容包括:@EnableAspectJAutoProxy
、@EnableCaching
和 @EnableTransactionManagement
。感興趣的讀者可以自行查閱該 issue內容。
歡迎關注個人公眾號,一起學習成長: