日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

別再說 Spring AOP 默認用的是 JDK 動態(tài)代理

 昵稱10087950 2022-06-16 發(fā)布于江蘇

這篇是 Spring 面試題的第三篇了,我們來盤一下有關(guān) AOP 的面試題。

關(guān)于標題的回答,在第二部分,別急。

話不多說,發(fā)車!

說下 AOP

AOP,Aspect Oriented Programming,面向切面編程。

將一些通用的邏輯集中實現(xiàn),然后通過 AOP 進行邏輯的切入,減少了零散的碎片化代碼,提高了系統(tǒng)的可維護性。

具體是含義可以理解為:通過代理的方式,在調(diào)用想要的對象方法時候,進行攔截處理,執(zhí)行切入的邏輯,然后再調(diào)用真正的方法實現(xiàn)。

例如,你實現(xiàn)了一個 A 對象,里面有  addUser 方法,此時你需要記錄該方法的調(diào)用次數(shù)。

那么你就可以搞個代理對象,這個代理對象也提供了 addUser 方法,最終你調(diào)用的是代理對象的 addUser ,在這個代理對象內(nèi)部填充記錄調(diào)用次數(shù)的邏輯,最終的效果就類似下面代碼:

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

這就叫做面向切面編程,當(dāng)然具體的代理的代碼不是像上面這樣寫死的,而是動態(tài)切入

實現(xiàn)上代理大體上可以分為:動態(tài)代理靜態(tài)代理。

  • 動態(tài)代理,即在運行時將切面的邏輯進去,按照上面的邏輯就是你實現(xiàn) A 類,然后定義要代理的切入點和切面的實現(xiàn),程序會自動在運行時生成類似上面的代理類。
  • 靜態(tài)代理,在編譯時或者類加載時進行切面的織入,典型的 AspectJ 就是靜態(tài)代理。

Spring AOP默認用的是什么動態(tài)代理,兩者的區(qū)別

Spring AOP 的動態(tài)代理實現(xiàn)分別是:JDK 動態(tài)代理與 CGLIB。

默認的實現(xiàn)是 JDK 動態(tài)代理。

ok,這個問題沒毛?。▽嶋H應(yīng)用來說其實不太準確),然后面試官接著問那你平時有調(diào)試過嗎,確定你得到的代理對象是 JDK 動態(tài)代理實現(xiàn)的?

然后你信誓旦旦的說,對,我們都實現(xiàn)接口的,所以是 JDK 動態(tài)代理。

然而你簡歷上寫著項目使用的框架是 SpringBoot,我問你 SpringBoot 是什么版本,你說2.x。

然后我就可以推斷,你沒看過,你大概率僅僅只是網(wǎng)上看了相關(guān)的面試題。

要注意上面說的默認實現(xiàn)是 Spring Framework (最新版我沒去驗證),而 SpringBoot 2.x 版本已經(jīng)默認改成了 CGLIB。

而我們現(xiàn)在公司大部分使用的都是 SpringBoot 2.x 版本,所以你要說默認 JDK 動態(tài)代理也沒錯,但是不符合你平日使用的情況,對吧?

如果你調(diào)試過,或者看過調(diào)用棧,你肯定能發(fā)現(xiàn)默認用的是 CGLIB(當(dāng)然你要是沒用 SpringBoot 當(dāng)我沒說哈):

圖片

市面上大部分面試題答案寫的就是 JDK 動態(tài)代理,是沒錯,Spring 官網(wǎng)都這樣寫的。

但是咱們現(xiàn)在不都是用 SpringBoot 了嘛,所以這其實不符合我們當(dāng)下使用的情況。

因此,面試時候不要只說 Spring AOP 默認用的是 JDK 動態(tài)代理,把 SpringBoot 也提一嘴,這不就是讓面試官刮目一看嘛(不過指不定面試官也不知道~)

如果要修改 SpringBoot 使用 JDK 動態(tài)代理,那么設(shè)置 spring.aop.proxy-target-class=false

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

那為什么要改成默認用 CGLIB?

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

圖片
圖片

大佬說 JDK 動態(tài)代理要求接口,所以沒有接口的話會有報錯,很令人討厭,并且讓 CGLIB 作為默認也沒什么副作用,特別是 CGLIB 已經(jīng)被重新打包為 Spring 的一部分了,所以就默認 CGLIB 。

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

詳細issue 鏈接:https://github.com/spring-projects/spring-boot/issues/5423

JDK 動態(tài)代理

JDK 動態(tài)代理是基于接口的,也就是被代理的類一定要實現(xiàn)了某個接口,否則無法被代理。

主要實現(xiàn)原理就是:

  1. 首先通過實現(xiàn)一個 InvocationHandler 接口得到一個切面類。
  2. 然后利用 Proxy 糅合目標類的類加載器、接口和切面類得到一個代理類。
  3. 代理類的邏輯就是執(zhí)行切入邏輯,把所有接口方法的調(diào)用轉(zhuǎn)發(fā)到 InvocationHandler 的 invoke() 方法上,然后根據(jù)反射調(diào)用目標類的方法。

圖片我們再深入一點點了解下原理實現(xiàn)。

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

圖片上面就是把 getUserInfo 方法緩存了,然后在調(diào)用代理類的 getUserInfo 的時候,會調(diào)用你之前實現(xiàn)的 InvocationHandler 里面的 invoke。

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

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

CGLIB

在 Spring 里面,如果被代理的類沒有實現(xiàn)接口,那么就用 CGLIB 來完成動態(tài)代理。

CGLIB 是基于ASM 字節(jié)碼生成工具,它是通過繼承的方式來實現(xiàn)代理類,所以要注意 final 方法,這種方法無法被繼承。

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

通過字節(jié)碼技術(shù)動態(tài)拼接成一個子類,在其中織入切面的邏輯。

使用例子:

Enhancer en = new Enhancer();
//2.設(shè)置父類,也就是代理目標類,上面提到了它是通過生成子類的方式
en.setSuperclass(target.getClass());
//3.設(shè)置回調(diào)函數(shù),這個this其實就是代理邏輯實現(xiàn)類,也就是切面,可以理解為JDK 動態(tài)代理的handler
en.setCallback(this);
//4.創(chuàng)建代理對象,也就是目標類的子類了。
return en.create();

JDK 動態(tài)代理和 CGLIB 兩者經(jīng)常還可能被面試官問性能對比,所以咱們也列一下(以下內(nèi)容取自:haiq的博客):

  • jdk6 下,在運行次數(shù)較少的情況下,jdk動態(tài)代理與 cglib 差距不明顯,甚至更快一些;而當(dāng)調(diào)用次數(shù)增加之后, cglib 表現(xiàn)稍微更快一些
  • jdk7 下,情況發(fā)生了逆轉(zhuǎn)!在運行次數(shù)較少(1,000,000)的情況下,jdk動態(tài)代理比 cglib 快了差不多30%;而當(dāng)調(diào)用次數(shù)增加之后(50,000,000), 動態(tài)代理比 cglib 快了接近1倍
  • jdk8 表現(xiàn)和 jdk7 基本一致

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

能說說攔截鏈的實現(xiàn)嗎?

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

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

那 Spring 具體是如何鏈起這些調(diào)用順序的呢?

這就是攔截鏈干的事,實際上這些注解都對應(yīng)著不同的 interceptor 實現(xiàn)。

然后 Spring 會利用一個集合把所有類型的 interceptor 組合起來,我在代碼里用了 @Before、@After、@AfterReturning、@AfterThrowing這幾個注解。

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

圖片

然后通過一個對象 CglibMethodInvocation 將這個集合封裝起來,緊接著調(diào)用這個對象的 proceed 方法,可看到這個集合 chain 被傳入了。

圖片

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

要注意,這里就開始遞歸套娃了,核心調(diào)用邏輯就在這里:

圖片

可以看到有個 currentInterceptorIndex 變量,通過遞歸,每次新增這索引值,來逐得到下一個 interceptor 。

并且每次都傳入當(dāng)前對象并調(diào)用  interceptor#invoke ,這樣就實現(xiàn)了攔截鏈的調(diào)用,所以這是個遞歸。

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

圖片

invoke 的實現(xiàn)是先執(zhí)行切入的前置邏輯,然后再繼續(xù)調(diào)用 CglibMethodInvocation#proceed(也就是mi.proceed),進行下一個 interceptor 的調(diào)用。

總結(jié)下

Spring 根據(jù) @Before、@After、@AfterReturning、@AfterThrowing 這些注解。

往集合里面加入對應(yīng)的 Spring 提供的  MethodInterceptor 實現(xiàn)。

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

然后通過一個對象 CglibMethodInvocation 將這個集合封裝起來,緊接著調(diào)用這個對象的 proceed 方法。

具體是利用 currentInterceptorIndex 下標,利用遞歸順序地執(zhí)行集合里面的 MethodInterceptor ,這樣就完成了攔截鏈的調(diào)用。

我截個調(diào)用鏈的堆棧截圖,可以很直觀地看到調(diào)用的順序(從下往上看):

圖片

是吧,是按照順序一個一個往后執(zhí)行,然后再一個一個返回,就是遞歸唄。

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

這個 Interceptor 作為第一個被調(diào)用,實際上就是將創(chuàng)建的 CglibMethodInvocation 這個對象存入 threadlocal 中,方便后面 Interceptor 調(diào)用的時候能得到這個對象,進行一些調(diào)用。

圖片

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

ok,更多細節(jié)還是得自己看源碼的,應(yīng)付面試了解到這個程度差不多的,上面幾個關(guān)鍵點一拋,這個題絕對穩(wěn)了!

Spring AOP 和 AspectJ 有什么區(qū)別

從上面的題目我們已經(jīng)知道,兩者分別是動態(tài)代理和靜態(tài)代理的區(qū)別。

Spring AOP 是動態(tài)代理,AspectJ 是靜態(tài)代理。

從一個是運行時織入,一個在編譯時織入,我們稍微一想到就能知道,編譯時就準備完畢,那么在調(diào)用時候沒有額外的織入開銷,性能更好些。

且 AspectJ 提供完整的 AOP 解決方案,像 Spring AOP 只支持方法級別的織入,而 AspectJ 支持字段、方法、構(gòu)造函數(shù)等等,所以它更加強大,當(dāng)然也更加復(fù)雜。


    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多