這篇是 Spring 面試題的第三篇了,我們來盤一下有關(guān) AOP 的面試題。 關(guān)于標題的回答,在第二部分,別急。 話不多說,發(fā)車! 說下 AOPAOP,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ù)的邏輯,最終的效果就類似下面代碼:
這就叫做面向切面編程,當(dāng)然具體的代理的代碼不是像上面這樣寫死的,而是動態(tài)切入。 實現(xiàn)上代理大體上可以分為:動態(tài)代理和靜態(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è)置 如果你提了這個,那面試官肯定會追問:
嘿嘿,答案我也為你準備好了,我們來看看: ![]() ![]() 大佬說 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)原理就是:
如果你反編譯的話,你能看到生成的代理類是會先在靜態(tài)塊中通過反射把所有方法都拿到存在靜態(tài)變量中,(我盲寫了一下)大致長這樣:
這樣就執(zhí)行到切入的邏輯了,且最終執(zhí)行了被代理類的 getUserInfo 方法。 就是中間商攔了一道咯,道理就是這個道理。 CGLIB在 Spring 里面,如果被代理的類沒有實現(xiàn)接口,那么就用 CGLIB 來完成動態(tài)代理。 CGLIB 是基于ASM 字節(jié)碼生成工具,它是通過繼承的方式來實現(xiàn)代理類,所以要注意 final 方法,這種方法無法被繼承。 簡單理解下,就是生成代理類的子類,如何生成呢? 通過字節(jié)碼技術(shù)動態(tài)拼接成一個子類,在其中織入切面的邏輯。 使用例子:
JDK 動態(tài)代理和 CGLIB 兩者經(jīng)常還可能被面試官問性能對比,所以咱們也列一下(以下內(nèi)容取自:haiq的博客):
我沒試過,有興趣的同學(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ù)雜。 |
|
來自: 昵稱10087950 > 《JAVA》