驚人!Spring5 AOP 默認使用Cglib ?從現象到源碼深度分析

  • 2019 年 10 月 30 日
  • 筆記

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 Framework 5.x 文檔

簡單翻譯一下。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.....()");      }  }

默認使用Cglib代理了?

UserServiceImpl實現了UserService介面,同時使用UserServiceAspectUserService#work方法進行前置增強攔截。

從運行結果來看,這裡的確使用了 CGLIB 代理而不是 JDK 動態代理。

難道真的是文檔寫錯了?!

@EnableAspectJAutoProxy 源碼注釋

在 Spring Framework 中,是使用@EnableAspectJAutoProxy註解來開啟 Spring AOP 相關功能的。

Spring Framework 5.2.0.RELEASE 版本@EnableAspectJAutoProxy註解源碼如下:

@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 版本。

proxyTargetClass設置無效了?

通過運行發現,還是使用了 CGLIB 代理。難道@EnableAspectJAutoProxyproxyTargetClass設置無效了?

Spring Framework 5.x

整理一下思路

  1. 有人說 Spring5 開始 AOP 默認使用 CGLIB 了
  2. Spring Framework 5.x 文檔和 @EnableAspectJAutoProxy源碼注釋都說了默認是使用 JDK 動態代理
  3. 程式運行結果說明,即使繼承了介面,設置proxyTargetClassfalse,程式依舊使用 CGLIB 代理

等一下,我們是不是遺漏了什麼?

示常式序是使用 SpringBoot 來運行的,那如果不用 SpringBoot,只用 Spring Framework 會怎麼樣呢?

運行環境:Spring Framework 5.2.0.RELEASE 版本。
UserServiceImpl 和 UserServiceAspect 類和上文一樣,這裡不在贅述。

Spring Framework 5.x

Spring Framework 5.x使用CGLIB

運行結果表明: 在 Spring Framework 5.x 版本中,如果類實現了介面,AOP 默認還是使用 JDK 動態代理。

再整理思路

  1. Spring5 AOP 默認依舊使用 JDK 動態代理,官方文檔和源碼注釋沒有錯。
  2. SpringBoot 2.x 版本中,AOP 默認使用 cglib,且無法通過proxyTargetClass進行修改。
  3. 那是不是 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方法:

AutoConfigurationImportSelector#getImportGroup

在這個方法中返回了AutoConfigurationGroup類。這是AutoConfigurationImportSelector中的一個內部類,他實現了DeferredImportSelector.Group介面。

在 SpringBoot 2.x 版本中,就是通過AutoConfigurationImportSelector.AutoConfigurationGroup#process方法來導入自動配置類的。

導入配置類

通過斷點調試可以看到,和 AOP 相關的自動配置是通過org.springframework.boot.autoconfigure.aop.AopAutoConfiguration來進行配置的。

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

這裡也提一下spring-configuration-metadata.json文件的作用:在使用application.propertiesapplication.yml文件時,IDEA 就是通過讀取這些文件資訊來提供程式碼提示的,SpringBoot 框架自己是不會來讀取這個配置文件的。

SringBoot 1.5.x 又是怎麼樣的

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 中,拋出了這樣一個問題:

issue

翻譯一下:我們應該使用@EnableTransactionManagement(proxyTargetClass = true)來防止人們不使用介面時出現討厭的代理問題。

這個"不使用介面時出現討厭的代理問題"是什麼呢?思考一分鐘。

討厭的代理問題

假設,我們有一個UserServiceImplUserService類,此時需要在UserContoller中使用UserService。在 Spring 中通常都習慣這樣寫程式碼:

@Autowired  UserService userService;

在這種情況下,無論是使用 JDK 動態代理,還是 CGLIB 都不會出現問題。

但是,如果你的程式碼是這樣的呢:

@Autowired  UserServiceImpl userService;

這個時候,如果我們是使用 JDK 動態代理,那在啟動時就會報錯:

啟動報錯

因為 JDK 動態代理是基於介面的,代理生成的對象只能賦值給介面變數。

而 CGLIB 就不存在這個問題。因為 CGLIB 是通過生成子類來實現的,代理對象無論是賦值給介面還是實現類這兩者都是代理對象的父類。

SpringBoot 正是出於這種考慮,於是在 2.x 版本中,將 AOP 默認實現改為了 CGLIB。

更多的細節資訊,讀者可以自己查閱上述 issue。

總結

  1. Spring 5.x 中 AOP 默認依舊使用 JDK 動態代理。
  2. SpringBoot 2.x 開始,為了解決使用 JDK 動態代理可能導致的類型轉化異常而默認使用 CGLIB。
  3. 在 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內容。


歡迎關注個人公眾號,一起學習成長:

Coder小黑