這篇是 Spring 面試題的第三篇了,我們來(lái)盤一下有關(guān) AOP 的面試題。 關(guān)于標(biāo)題的回答,在第二部分,別急。 話不多說(shuō),發(fā)車! 說(shuō)下 AOPAOP,Aspect Oriented Programming,面向切面編程。 將一些通用的邏輯集中實(shí)現(xiàn),然后通過(guò) AOP 進(jìn)行邏輯的切入,減少了零散的碎片化代碼,提高了系統(tǒng)的可維護(hù)性。 具體是含義可以理解為:通過(guò)代理的方式,在調(diào)用想要的對(duì)象方法時(shí)候,進(jìn)行攔截處理,執(zhí)行切入的邏輯,然后再調(diào)用真正的方法實(shí)現(xiàn)。 例如,你實(shí)現(xiàn)了一個(gè) A 對(duì)象,里面有 addUser 方法,此時(shí)你需要記錄該方法的調(diào)用次數(shù)。 那么你就可以搞個(gè)代理對(duì)象,這個(gè)代理對(duì)象也提供了 addUser 方法,最終你調(diào)用的是代理對(duì)象的 addUser ,在這個(gè)代理對(duì)象內(nèi)部填充記錄調(diào)用次數(shù)的邏輯,最終的效果就類似下面代碼:
這就叫做面向切面編程,當(dāng)然具體的代理的代碼不是像上面這樣寫(xiě)死的,而是動(dòng)態(tài)切入。 實(shí)現(xiàn)上代理大體上可以分為:動(dòng)態(tài)代理和靜態(tài)代理。
Spring AOP默認(rèn)用的是什么動(dòng)態(tài)代理,兩者的區(qū)別Spring AOP 的動(dòng)態(tài)代理實(shí)現(xiàn)分別是:JDK 動(dòng)態(tài)代理與 CGLIB。 默認(rèn)的實(shí)現(xiàn)是 JDK 動(dòng)態(tài)代理。 ok,這個(gè)問(wèn)題沒(méi)毛病(對(duì)實(shí)際應(yīng)用來(lái)說(shuō)其實(shí)不太準(zhǔn)確),然后面試官接著問(wèn)那你平時(shí)有調(diào)試過(guò)嗎,確定你得到的代理對(duì)象是 JDK 動(dòng)態(tài)代理實(shí)現(xiàn)的? 然后你信誓旦旦的說(shuō),對(duì),我們都實(shí)現(xiàn)接口的,所以是 JDK 動(dòng)態(tài)代理。 然而你簡(jiǎn)歷上寫(xiě)著項(xiàng)目使用的框架是 SpringBoot,我問(wèn)你 SpringBoot 是什么版本,你說(shuō)2.x。 然后我就可以推斷,你沒(méi)看過(guò),你大概率僅僅只是網(wǎng)上看了相關(guān)的面試題。 要注意上面說(shuō)的默認(rèn)實(shí)現(xiàn)是 Spring Framework (最新版我沒(méi)去驗(yàn)證),而 SpringBoot 2.x 版本已經(jīng)默認(rèn)改成了 CGLIB。 而我們現(xiàn)在公司大部分使用的都是 SpringBoot 2.x 版本,所以你要說(shuō)默認(rèn) JDK 動(dòng)態(tài)代理也沒(méi)錯(cuò),但是不符合你平日使用的情況,對(duì)吧? 如果你調(diào)試過(guò),或者看過(guò)調(diào)用棧,你肯定能發(fā)現(xiàn)默認(rèn)用的是 CGLIB(當(dāng)然你要是沒(méi)用 SpringBoot 當(dāng)我沒(méi)說(shuō)哈): ![]() 市面上大部分面試題答案寫(xiě)的就是 JDK 動(dòng)態(tài)代理,是沒(méi)錯(cuò),Spring 官網(wǎng)都這樣寫(xiě)的。 但是咱們現(xiàn)在不都是用 SpringBoot 了嘛,所以這其實(shí)不符合我們當(dāng)下使用的情況。 因此,面試時(shí)候不要只說(shuō) Spring AOP 默認(rèn)用的是 JDK 動(dòng)態(tài)代理,把 SpringBoot 也提一嘴,這不就是讓面試官刮目一看嘛(不過(guò)指不定面試官也不知道~) 如果要修改 SpringBoot 使用 JDK 動(dòng)態(tài)代理,那么設(shè)置 如果你提了這個(gè),那面試官肯定會(huì)追問(wèn):
嘿嘿,答案我也為你準(zhǔn)備好了,我們來(lái)看看: ![]() ![]() 大佬說(shuō) JDK 動(dòng)態(tài)代理要求接口,所以沒(méi)有接口的話會(huì)有報(bào)錯(cuò),很令人討厭,并且讓 CGLIB 作為默認(rèn)也沒(méi)什么副作用,特別是 CGLIB 已經(jīng)被重新打包為 Spring 的一部分了,所以就默認(rèn) CGLIB 。 好吧,其實(shí)也沒(méi)有什么很特殊的含義,就是效果沒(méi)差多少,還少報(bào)錯(cuò),方便咯。 詳細(xì)issue 鏈接:https://github.com/spring-projects/spring-boot/issues/5423 JDK 動(dòng)態(tài)代理JDK 動(dòng)態(tài)代理是基于接口的,也就是被代理的類一定要實(shí)現(xiàn)了某個(gè)接口,否則無(wú)法被代理。 主要實(shí)現(xiàn)原理就是:
如果你反編譯的話,你能看到生成的代理類是會(huì)先在靜態(tài)塊中通過(guò)反射把所有方法都拿到存在靜態(tài)變量中,(我盲寫(xiě)了一下)大致長(zhǎng)這樣:
這樣就執(zhí)行到切入的邏輯了,且最終執(zhí)行了被代理類的 getUserInfo 方法。 就是中間商攔了一道咯,道理就是這個(gè)道理。 CGLIB在 Spring 里面,如果被代理的類沒(méi)有實(shí)現(xiàn)接口,那么就用 CGLIB 來(lái)完成動(dòng)態(tài)代理。 CGLIB 是基于ASM 字節(jié)碼生成工具,它是通過(guò)繼承的方式來(lái)實(shí)現(xiàn)代理類,所以要注意 final 方法,這種方法無(wú)法被繼承。 簡(jiǎn)單理解下,就是生成代理類的子類,如何生成呢? 通過(guò)字節(jié)碼技術(shù)動(dòng)態(tài)拼接成一個(gè)子類,在其中織入切面的邏輯。 使用例子:
JDK 動(dòng)態(tài)代理和 CGLIB 兩者經(jīng)常還可能被面試官問(wèn)性能對(duì)比,所以咱們也列一下(以下內(nèi)容取自:haiq的博客):
我沒(méi)試過(guò),有興趣的同學(xué)可以自己實(shí)驗(yàn)一下。 能說(shuō)說(shuō)攔截鏈的實(shí)現(xiàn)嗎?我們都知道 Spring AOP 提供了多種攔截點(diǎn),便捷我們對(duì) AOP 的使用,比如 @Before、@After、@AfterReturning、@AfterThrowing 等等。 方便我們?cè)谀繕?biāo)方法執(zhí)行前、后、拋錯(cuò)等地方進(jìn)行一些邏輯的切入。 那 Spring 具體是如何鏈起這些調(diào)用順序的呢? 這就是攔截鏈干的事,實(shí)際上這些注解都對(duì)應(yīng)著不同的 interceptor 實(shí)現(xiàn)。 然后 Spring 會(huì)利用一個(gè)集合把所有類型的 interceptor 組合起來(lái),我在代碼里用了 @Before、@After、@AfterReturning、@AfterThrowing這幾個(gè)注解。 于是集合里就有了這些 interceptor(多了一個(gè) expose...等下解釋),這是由 Spring 掃描到注解自動(dòng)加進(jìn)來(lái)的: ![]() 然后通過(guò)一個(gè)對(duì)象 CglibMethodInvocation 將這個(gè)集合封裝起來(lái),緊接著調(diào)用這個(gè)對(duì)象的 proceed 方法,可看到這個(gè)集合 chain 被傳入了。 ![]() 我們來(lái)看下 CglibMethodInvocation#proceed 方法邏輯。 要注意,這里就開(kāi)始遞歸套娃了,核心調(diào)用邏輯就在這里: ![]() 可以看到有個(gè) currentInterceptorIndex 變量,通過(guò)遞歸,每次新增這索引值,來(lái)逐得到下一個(gè) interceptor 。 并且每次都傳入當(dāng)前對(duì)象并調(diào)用 interceptor#invoke ,這樣就實(shí)現(xiàn)了攔截鏈的調(diào)用,所以這是個(gè)遞歸。 我們拿集合里面的 MethodBeforeAdviceInterceptor 來(lái)舉例看下,這個(gè)是目標(biāo)方法執(zhí)行的前置攔截,我們看下它的 invoke 實(shí)現(xiàn),有更直觀的認(rèn)識(shí): ![]() invoke 的實(shí)現(xiàn)是先執(zhí)行切入的前置邏輯,然后再繼續(xù)調(diào)用 CglibMethodInvocation#proceed(也就是mi.proceed),進(jìn)行下一個(gè) interceptor 的調(diào)用。 總結(jié)下: Spring 根據(jù) @Before、@After、@AfterReturning、@AfterThrowing 這些注解。 往集合里面加入對(duì)應(yīng)的 Spring 提供的 MethodInterceptor 實(shí)現(xiàn)。 比如上面的 MethodBeforeAdviceInterceptor ,如果你沒(méi)用 @Before,集合里就沒(méi)有 MethodBeforeAdviceInterceptor 。 然后通過(guò)一個(gè)對(duì)象 CglibMethodInvocation 將這個(gè)集合封裝起來(lái),緊接著調(diào)用這個(gè)對(duì)象的 proceed 方法。 具體是利用 currentInterceptorIndex 下標(biāo),利用遞歸順序地執(zhí)行集合里面的 MethodInterceptor ,這樣就完成了攔截鏈的調(diào)用。 我截個(gè)調(diào)用鏈的堆棧截圖,可以很直觀地看到調(diào)用的順序(從下往上看): ![]() 是吧,是按照順序一個(gè)一個(gè)往后執(zhí)行,然后再一個(gè)一個(gè)返回,就是遞歸唄。 然后我再解釋下上面的 chain 集合我們看到第一個(gè)索引位置的 ExposeInvocationInterceptor 。 這個(gè) Interceptor 作為第一個(gè)被調(diào)用,實(shí)際上就是將創(chuàng)建的 CglibMethodInvocation 這個(gè)對(duì)象存入 threadlocal 中,方便后面 Interceptor 調(diào)用的時(shí)候能得到這個(gè)對(duì)象,進(jìn)行一些調(diào)用。 ![]() 從名字就能看出 expose:暴露。 ok,更多細(xì)節(jié)還是得自己看源碼的,應(yīng)付面試了解到這個(gè)程度差不多的,上面幾個(gè)關(guān)鍵點(diǎn)一拋,這個(gè)題絕對(duì)穩(wěn)了! Spring AOP 和 AspectJ 有什么區(qū)別從上面的題目我們已經(jīng)知道,兩者分別是動(dòng)態(tài)代理和靜態(tài)代理的區(qū)別。 Spring AOP 是動(dòng)態(tài)代理,AspectJ 是靜態(tài)代理。 從一個(gè)是運(yùn)行時(shí)織入,一個(gè)在編譯時(shí)織入,我們稍微一想到就能知道,編譯時(shí)就準(zhǔn)備完畢,那么在調(diào)用時(shí)候沒(méi)有額外的織入開(kāi)銷,性能更好些。 且 AspectJ 提供完整的 AOP 解決方案,像 Spring AOP 只支持方法級(jí)別的織入,而 AspectJ 支持字段、方法、構(gòu)造函數(shù)等等,所以它更加強(qiáng)大,當(dāng)然也更加復(fù)雜。 |
|
來(lái)自: 昵稱10087950 > 《JAVA》