1. 程式人生 > >驚人!Spring5 AOP 預設使用Cglib ?從現象到原始碼深度分析

驚人!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介面,同時使用UserServiceAspectUserService#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 代理。難道@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 版本中,如果類實現了介面,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方法:

在這個方法中返回了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.propertiesapplication.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)來防止人們不使用介面時出現討厭的代理問題。

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

討厭的代理問題

假設,我們有一個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內容。


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