什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

Spring5 AOP 默認使用 Cglib 了?我第一次聽到這個說法是在一個微信群裡:

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

真的假的?查閱文檔

剛看到這個說法的時候,我是保持懷疑態度的。

大家都知道 Spring5 之前的版本 AOP 在默認情況下是使用 JDK 動態代理的,那是不是 Spring5 版本真的做了修改呢?於是我打開 Spring Framework 5.x 文檔,再次確認了一下:

文檔地址:docs.spring.io/spring/docs…

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

簡單翻譯一下。Spring AOP 默認使用 JDK 動態代理,如果對象沒有實現接口,則使用 CGLIB 代理。當然,也可以強制使用 CGLIB 代理。

什麼?文檔寫錯了?!

當我把官方文檔發到群裡之後,又收到了這位同學的回覆:

什麼?Spring5 AOP 默認使用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.....()");
}
}
什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

UserServiceImpl實現了UserService接口,同時使用UserServiceAspect對UserService#work方法進行前置增強攔截。

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

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

@EnableAspectJAutoProxy 源碼註釋

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

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

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

通過源碼註釋我們可以瞭解到:在 Spring Framework 5.2.0.RELEASE 版本中,proxyTargetClass的默認取值依舊是false,默認還是使用 JDK 動態代理。

難道文檔和源碼註釋都寫錯了?!

@EnableAspectJAutoProxy 的 proxyTargetClass 無效了?

接下來,我嘗試使用@EnableAspectJAutoProxy來強制使用 JDK 動態代理。

運行環境:SpringBoot 2.2.0.RELEASE 版本,內置 Spring Framework 版本為 5.2.0.RELEASE 版本。

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

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

Spring Framework 5.x

整理一下思路

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

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

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

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

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

什麼?Spring5 AOP 默認使用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註解的詳細用法,可以參看筆者之前的文章:mp.weixin.qq.com/s/7arh4sVH1…

@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方法:

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

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

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

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

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

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

真相大白

看到這裡,可以說是真相大白了。在 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
什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

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

SringBoot 1.5.x 又是怎麼樣的

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

可以看到,在 SpringBoot 1.5.x 版本中,默認還是使用 JDK 動態代理的。

SpringBoot 2.x 為何默認使用 Cglib

SpringBoot 2.x 版本為什麼要默認使用 Cglib 來實現 AOP 呢?這麼做的好處又是什麼呢?筆者從網上找到了一些資料,先來看一個 issue。

Spring Boot issue #5423

Use @EnableTransactionManagement(proxyTargetClass = true) #5423

github.com/spring-proj…

在這個 issue 中,拋出了這樣一個問題:

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

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

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

討厭的代理問題

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

@Autowired
UserService userService;

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

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

@Autowired
UserServiceImpl userService;

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

什麼?Spring5 AOP 默認使用Cglib?從現象到源碼深度分析

因為 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配置已無效。

鏈接:https://juejin.im/post/5db7870a518825647178f16c


分享到:


相關文章: