別再說 Spring AOP 預設用的是 JDK 動態代理

AOP

AOP,Aspect Oriented Programming,面向切面程式設計。

將一些通用的邏輯集中實現,然後透過 AOP 進行邏輯的切入,減少了零散的碎片化程式碼,提高了系統的可維護性。

具體是含義可以理解為:透過代理的方式,在呼叫想要的物件方法時候,進行攔截處理,執行切入的邏輯,然後再呼叫真正的方法實現。

例如,你實現了一個 A 物件,裡面有 addUser 方法,此時你需要記錄該方法的呼叫次數。

那麼你就可以搞個代理物件,這個代理物件也提供了 addUser 方法,最終你呼叫的是代理物件的 addUser ,在這個代理物件內部填充記錄呼叫次數的邏輯,最終的效果就類似下面程式碼:

class A代理 { A a;// 被代理的 A void addUser(User user) { count();// 計數 a。addUser(user); }}最終使用的是:A代理。addUser(user);

這就叫做

面向切面程式設計

,當然具體的代理的程式碼不是像上面這樣寫死的,

而是動態切入

實現上代理大體上可以分為:

動態代理

靜態代理

動態代理,即

在執行時

將切面的邏輯進去,按照上面的邏輯就是你實現 A 類,然後定義要代理的切入點和切面的實現,程式會自動在執行時生成類似上面的代理類。

靜態代理,

在編譯時或者類載入時

進行切面的織入,典型的 AspectJ 就是靜態代理。

Spring AOP預設用的是什麼動態代理,兩者的區別

Spring AOP 的動態代理實現分別是:JDK 動態代理與 CGLIB。

預設的實現是 JDK 動態代理。

ok,這個問題沒毛病(對實際應用來說其實不太準確),然後面試官接著問那你平時有除錯過嗎,確定你得到的代理物件是 JDK 動態代理實現的?

然後你信誓旦旦的說,對,我們都實現介面的,所以是 JDK 動態代理。

然而你簡歷上寫著專案使用的框架是 SpringBoot,我問你 SpringBoot 是什麼版本,你說2。x。

然後我就可以推斷,你沒看過,你大機率僅僅只是網上看了相關的面試題。

要注意上面說的預設實現是 Spring Framework (最新版我沒去驗證),而 SpringBoot 2。x 版本已經預設改成了 CGLIB

而我們現在公司大部分使用的都是 SpringBoot 2。x 版本,所以你要說預設 JDK 動態代理也沒錯,但是不符合你平日使用的情況,對吧?

如果你除錯過,或者看過呼叫棧,你肯定能發現預設用的是 CGLIB(當然你要是沒用 SpringBoot 當我沒說哈):

別再說 Spring AOP 預設用的是 JDK 動態代理

市面上大部分面試題答案寫的就是 JDK 動態代理,是沒錯,Spring 官網都這樣寫的。

但是咱們現在不都是用 SpringBoot 了嘛,所以這其實不符合我們當下使用的情況。

因此,面試時候不要只說 Spring AOP 預設用的是 JDK 動態代理,

把 SpringBoot 也提一嘴,這不就是讓面試官刮目一看嘛

(不過指不定面試官也不知道~)

如果要修改 SpringBoot 使用 JDK 動態代理,那麼設定

spring。aop。proxy-target-class=false

如果你提了這個,那面試官肯定會追問:

那為什麼要改成預設用 CGLIB?

嘿嘿,答案我也為你準備好了,我們來看看:

別再說 Spring AOP 預設用的是 JDK 動態代理

別再說 Spring AOP 預設用的是 JDK 動態代理

大佬說 JDK 動態代理要求介面,所以沒有介面的話會有報錯,很令人討厭,並且讓 CGLIB 作為預設也沒什麼副作用,特別是 CGLIB 已經被重新打包為 Spring 的一部分了,所以就預設 CGLIB 。

好吧,其實也沒有什麼很特殊的含義,就是效果沒差多少,還少報錯,方便咯。

詳細issue 連結:

https://github。com/spring-projects/spring-boot/issues/5423

JDK 動態代理

JDK 動態代理是基於介面的

,也就是被代理的類一定要實現了某個介面,否則無法被代理。

主要實現原理就是:

首先透過實現一個 InvocationHandler 介面得到一個切面類。

然後利用 Proxy 糅合目標類的類載入器、介面和切面類得到一個代理類。

代理類的邏輯就是執行切入邏輯,把所有介面方法的呼叫轉發到 InvocationHandler 的 invoke() 方法上,然後根據反射呼叫目標類的方法。

別再說 Spring AOP 預設用的是 JDK 動態代理

我們再深入一點點了解下原理實現。

如果你反編譯的話,你能看到生成的代理類是會先在靜態塊中透過反射把所有方法都拿到存在靜態變數中,(我盲寫了一下)大致長這樣:

別再說 Spring AOP 預設用的是 JDK 動態代理

上面就是把 getUserInfo 方法快取了,然後在呼叫代理類的 getUserInfo 的時候,會呼叫你之前實現的 InvocationHandler 裡面的 invoke。

這樣就執行到切入的邏輯了,且最終執行了被代理類的 getUserInfo 方法。

就是中間商攔了一道咯,道理就是這個道理。

CGLIB

在 Spring 裡面,如果被代理的類沒有實現介面,那麼就用 CGLIB 來完成動態代理。

CGLIB 是基於ASM 位元組碼生成工具,它是

透過繼承的方式來實現代理類

,所以要注意 final 方法,這種方法無法被繼承。

簡單理解下,就是生成代理類的子類,如何生成呢?

透過位元組碼技術動態拼接成一個子類,在其中織入切面的邏輯

使用例子:

Enhancer en = new Enhancer();//2。設定父類,也就是代理目標類,上面提到了它是透過生成子類的方式en。setSuperclass(target。getClass());//3。設定回撥函式,這個this其實就是代理邏輯實現類,也就是切面,可以理解為JDK 動態代理的handleren。setCallback(this);//4。建立代理物件,也就是目標類的子類了。return en。create();

JDK 動態代理和 CGLIB 兩者經常還可能被面試官問效能對比,所以咱們也列一下(以下內容取自:haiq的部落格):

jdk6 下,在執行次數較少的情況下,jdk動態代理與 cglib 差距不明顯,甚至更快一些;而當呼叫次數增加之後, cglib 表現稍微更快一些

jdk7 下,情況發生了逆轉!在執行次數較少(1,000,000)的情況下,jdk動態代理比 cglib 快了差不多30%;而當呼叫次數增加之後(50,000,000), 動態代理比 cglib 快了接近1倍

jdk8 表現和 jdk7 基本一致

我沒試過,有興趣的同學可以自己實驗一下。

能說說攔截鏈的實現嗎?

我們都知道 Spring AOP 提供了多種攔截點,便捷我們對 AOP 的使用,比如 @Before、@After、@AfterReturning、@AfterThrowing 等等。

方便我們在目標方法執行前、後、拋錯等地方進行一些邏輯的切入。

那 Spring 具體是如何鏈起這些呼叫順序的呢?

這就是攔截鏈乾的事,實際上這些註解都對應著不同的 interceptor 實現。

然後 Spring 會利用一個集合把所有型別的 interceptor 組合起來,我在程式碼裡用了 @Before、@After、@AfterReturning、@AfterThrowing這幾個註解。

於是集合裡就有了這些 interceptor(多了一個 expose。。。等下解釋),這是由 Spring 掃描到註解自動加進來的:

別再說 Spring AOP 預設用的是 JDK 動態代理

然後透過一個物件 CglibMethodInvocation 將這個集合封裝起來,緊接著呼叫這個物件的 proceed 方法,可看到這個集合 chain 被傳入了。

別再說 Spring AOP 預設用的是 JDK 動態代理

我們來看下 CglibMethodInvocation#proceed 方法邏輯。

要注意,這裡就開始

遞迴套娃

了,核心呼叫邏輯就在這裡:

別再說 Spring AOP 預設用的是 JDK 動態代理

可以看到有個 currentInterceptorIndex 變數,

透過遞迴,每次新增這索引值

,來逐得到下一個 interceptor 。

並且每次都傳入當前物件並呼叫 interceptor#invoke ,這樣就實現了攔截鏈的呼叫,所以這是個遞迴。

我們拿集合裡面的 MethodBeforeAdviceInterceptor 來舉例看下,這個是目標方法執行的前置攔截,我們看下它的 invoke 實現,有更直觀的認識:

別再說 Spring AOP 預設用的是 JDK 動態代理

invoke 的實現是先執行切入的前置邏輯,然後再繼續呼叫 CglibMethodInvocation#proceed(也就是mi。proceed),進行下一個 interceptor 的呼叫。

總結:

Spring 根據 @Before、@After、@AfterReturning、@AfterThrowing 這些註解。

往集合裡面加入對應的 Spring 提供的 MethodInterceptor 實現。

比如上面的 MethodBeforeAdviceInterceptor ,如果你沒用 @Before,集合裡就沒有 MethodBeforeAdviceInterceptor 。

然後透過一個物件 CglibMethodInvocation 將這個集合封裝起來,緊接著呼叫這個物件的 proceed 方法。

具體是利用 currentInterceptorIndex 下標,利用遞迴順序地執行集合裡面的 MethodInterceptor ,這樣就完成了攔截鏈的呼叫。

我截個呼叫鏈的堆疊截圖,可以很直觀地看到呼叫的順序(從下往上看):

別再說 Spring AOP 預設用的是 JDK 動態代理

是吧,是按照順序一個一個往後執行,然後再一個一個返回,就是遞迴唄。

然後我再解釋下上面的 chain 集合我們看到第一個索引位置的 ExposeInvocationInterceptor 。

這個 Interceptor 作為第一個被呼叫,實際上就是將建立的 CglibMethodInvocation 這個物件存入 threadlocal 中,方便後面 Interceptor 呼叫的時候能得到這個物件,進行一些呼叫。

別再說 Spring AOP 預設用的是 JDK 動態代理

從名字就能看出 expose:暴露。

ok,更多細節還是得自己看原始碼的,應付面試瞭解到這個程度差不多的,上面幾個關鍵點一拋,這個題絕對穩了!

Spring AOP 和 AspectJ 有什麼區別

從上面的題目我們已經知道,兩者分別是動態代理和靜態代理的區別。

Spring AOP 是動態代理,AspectJ 是靜態代理。

從一個是執行時織入,一個在編譯時織入,我們稍微一想到就能知道,編譯時就準備完畢,那麼在呼叫時候沒有額外的織入開銷,效能更好些。

且 AspectJ 提供完整的 AOP 解決方案,像 Spring AOP 只支援方法級別的織入,而 AspectJ 支援欄位、方法、建構函式等等,所以它更加強大,當然也更加複雜。