1. 關(guān)于代理
小伙伴們知道,Java 23 種設(shè)計(jì)模式中有一種模式叫做代理模式,這種代理我們可以將之稱為靜態(tài)代理,Spring AOP 我們常說是一種動(dòng)態(tài)代理,那么這兩種代理的區(qū)別在哪里呢?
1.1 靜態(tài)代理
這種代理在我們?nèi)粘I钪衅鋵?shí)非常常見,例如房屋中介就相當(dāng)于是一個(gè)代理,當(dāng)房東需要出租房子的時(shí)候,需要發(fā)布廣告、尋找客戶、清理房間。。。由于比較麻煩,因此房東可以將租房子這件事情委托給中間代理去做。這就是一個(gè)靜態(tài)代理。
我通過一個(gè)簡(jiǎn)單的代碼來演示一下,首先我們有一個(gè)租房的接口,如下:
publicinterfaceRent{ voidrent(); }
房東實(shí)現(xiàn)了該接口,表示想要出租房屋:
publicclassLandlordimplementsRent{ @Override publicvoidrent(){ System.out.println("房屋出租"); } }
中介作為中間代理,也實(shí)現(xiàn)了該接口,同時(shí)代理了房東,如下:
publicclassHouseAgentimplementsRent{ privateLandlordlandlord; publicHouseAgent(Landlordlandlord){ this.landlord=landlord; } publicHouseAgent(){ } @Override publicvoidrent(){ publishAd(); landlord.rent(); agencyFee(); } publicvoidpublishAd(){ System.out.println("發(fā)布招租廣告"); } publicvoidagencyFee(){ System.out.println("收取中介費(fèi)"); } }
可以看到,中介的 rent 方法中,除了調(diào)用房東的 rent 方法之外,還調(diào)用了 publishAd 和 agencyFee 兩個(gè)方法。
接下來客戶租房,只需要和代理打交道就可以了,如下:
publicclassClient{ publicstaticvoidmain(String[]args){ Landlordlandlord=newLandlord(); HouseAgenthouseAgent=newHouseAgent(landlord); houseAgent.rent(); } }
這就是一個(gè)簡(jiǎn)單的代理模式。無論大家是否有接觸過 Java 23 種設(shè)計(jì)模式,上面這段代碼應(yīng)該都很好理解。
這是靜態(tài)代理。
1.2 動(dòng)態(tài)代理
動(dòng)態(tài)代理講究在不改變?cè)愒椒ǖ那闆r下,增強(qiáng)目標(biāo)方法的功能,例如,大家平時(shí)使用的 Spring 事務(wù)功能,在不改變目標(biāo)方法的情況下,就可以通過動(dòng)態(tài)代理為方法添加事務(wù)處理能力。再比如松哥在 TienChin 項(xiàng)目中所講的日志處理、接口冪等性處理、多數(shù)據(jù)源處理等,都是動(dòng)態(tài)代理能力的體現(xiàn)
從實(shí)現(xiàn)原理上,我們又可以將動(dòng)態(tài)代理劃分為兩大類:
編譯時(shí)增強(qiáng)。
運(yùn)行時(shí)增強(qiáng)。
1.2.1 編譯時(shí)增強(qiáng)
編譯時(shí)增強(qiáng),這種有點(diǎn)類似于 Lombok 的感覺,就是在編譯階段就直接生成了代理類,將來運(yùn)行的時(shí)候,就直接運(yùn)行這個(gè)編譯生成的代理類,AspectJ 就是這樣一種編譯時(shí)增強(qiáng)的工具。
AspectJ 全稱是 Eclipse AspectJ, 其官網(wǎng)地址是:http://www.eclipse.org/aspectj,截止到本文寫作時(shí),目前最新版本為:1.9.7。
從官網(wǎng)我們可以看到 AspectJ 的定位:
基于 Java 語言的面向切面編程語言。
兼容 Java。
易學(xué)易用。
使用 AspectJ 時(shí)需要使用專門的編譯器 ajc。
1.2.2 運(yùn)行時(shí)增強(qiáng)
運(yùn)行時(shí)增強(qiáng)則是指借助于 JDK 動(dòng)態(tài)代理或者 CGLIB 動(dòng)態(tài)代理等,在內(nèi)存中臨時(shí)生成 AOP 動(dòng)態(tài)代理類,我們?cè)?Spring AOP 中常說的動(dòng)態(tài)代理,一般是指這種運(yùn)行時(shí)增強(qiáng)。
我們平日開發(fā)寫的 Spring AOP,基本上都是屬于這一類。
2. AspectJ 和 Spring AOP
經(jīng)過前面的介紹,相信大家已經(jīng)明白了 AspectJ 其實(shí)也是 AOP 的一種實(shí)現(xiàn),只不過它是編譯時(shí)增強(qiáng)。
接下來,松哥再通過三個(gè)具體的案例,來和小伙伴們演示編譯時(shí)增強(qiáng)和運(yùn)行時(shí)增強(qiáng)。
2.1 AspectJ
首先,在 IDEA 中想要運(yùn)行 AspectJ,需要先安裝 AspectJ 插件,就是下面這個(gè):
安裝好之后,我們需要在 IDEA 中配置一下,使用 ajc 編譯器代替 javac(這個(gè)是針對(duì)當(dāng)前項(xiàng)目的設(shè)置,所以可以放心修改):
有如下幾個(gè)需要修改的點(diǎn):
首先修改編譯器為 ajc。
將使用的 Java 版本改為 8,這個(gè)一共有兩個(gè)地方需要修改。
設(shè)置 aspectjtools.jar 的位置,這個(gè) jar 包需要自己提前準(zhǔn)備好,可以從 Maven 官網(wǎng)下載,然后在這里配置 jar 的路徑,配置完成之后,點(diǎn)擊 test 按鈕進(jìn)行測(cè)試,測(cè)試成功就會(huì)彈出來圖中的彈框。
對(duì)于第 3 步所需要的 jar,也可以在項(xiàng)目的 Maven 中添加如下依賴,自動(dòng)下載,下載到本地倉庫之后,再刪除掉 pom.xml 中的配置即可:
org.aspectj aspectjtools 1.9.7.M3
這樣,開發(fā)環(huán)境就準(zhǔn)備好了。
接下來,假設(shè)我有一個(gè)銀行轉(zhuǎn)帳的方法:
publicclassMoneyService{ publicvoidtransferMoney(){ System.out.println("轉(zhuǎn)賬操作"); } }
我想給這個(gè)方法添加事務(wù),那么我就新建一個(gè) Aspect,如下:
publicaspectTxAspect{ voidaround():call(voidMoneyService.transferMoney()){ System.out.println("開啟事務(wù)"); try{ proceed(); System.out.println("提交事務(wù)事務(wù)"); }catch(Exceptione){ System.out.println("回滾事務(wù)"); } } }
這就是 AspectJ 的語法,跟 Java 有點(diǎn)像,但是不太一樣。需要注意的是,這個(gè) TxAspect 不是一個(gè) Java 類,它的后綴是 .aj。
proceed 表示繼續(xù)執(zhí)行目標(biāo)方法,前后邏輯比較簡(jiǎn)單,我就不多說了。
最后,我們?nèi)ミ\(yùn)行轉(zhuǎn)賬服務(wù):
publicclassDemo01{ publicstaticvoidmain(String[]args){ MoneyServicemoneyService=newMoneyService(); moneyService.transferMoney(); } }
運(yùn)行結(jié)果如下:
這就是一個(gè)靜態(tài)代理。
為什么這么說呢?我們通過 IDEA 來查看一下 TxAspect 編譯之后的結(jié)果:
@Aspect publicclassTxAspect{ static{ try{ ajc$postClinit(); }catch(Throwablevar1){ ajc$initFailureCause=var1; } } publicTxAspect(){ } @Around( value="call(voidMoneyService.transferMoney())", argNames="ajc$aroundClosure" ) publicvoidajc$around$org_javaboy_demo_p2_TxAspect$1$3b99afea(AroundClosureajc$aroundClosure){ System.out.println("開啟事務(wù)"); try{ ajc$around$org_javaboy_demo_p2_TxAspect$1$3b99afeaproceed(ajc$aroundClosure); System.out.println("提交事務(wù)事務(wù)"); }catch(Exceptionvar2){ System.out.println("回滾事務(wù)"); } } publicstaticTxAspectaspectOf(){ if(ajc$perSingletonInstance==null){ thrownewNoAspectBoundException("org_javaboy_demo_p2_TxAspect",ajc$initFailureCause); }else{ returnajc$perSingletonInstance; } } publicstaticbooleanhasAspect(){ returnajc$perSingletonInstance!=null; } }
再看一下編譯之后的啟動(dòng)類:
publicclassDemo01{ publicDemo01(){ } publicstaticvoidmain(String[]args){ MoneyServicemoneyService=newMoneyService(); transferMoney_aroundBody1$advice(moneyService,TxAspect.aspectOf(),(AroundClosure)null); } }
可以看到,都是修改后的內(nèi)容了。
所以說 AspectJ 的作用就有點(diǎn)類似于 Lombok,直接在編譯時(shí)期將我們的代碼改了,這就是編譯時(shí)增強(qiáng)。
2.2 Spring AOP
Spring AOP 在開發(fā)的時(shí)候,其實(shí)也使用了 AspectJ 中的注解,像我們平時(shí)使用的 @Aspect、@Around、@Pointcut 等,都是 AspectJ 里邊提供的,但是 Spring AOP 并未借鑒 AspectJ 的編譯時(shí)增強(qiáng),Spring AOP 沒有使用 AspectJ 的編譯器和織入器,Spring AOP 還是使用了運(yùn)行時(shí)增強(qiáng)。
運(yùn)行時(shí)增強(qiáng)可以利用 JDK 動(dòng)態(tài)代理或者 CGLIB 動(dòng)態(tài)代理來實(shí)現(xiàn)。我分別來演示。
2.2.1 JDK 動(dòng)態(tài)代理
JDK 動(dòng)態(tài)代理有一個(gè)要求,就是被代理的對(duì)象需要有接口,沒有接口不行,CGLIB 動(dòng)態(tài)代理則無此要求。
假設(shè)我現(xiàn)在有一個(gè)計(jì)算器接口:
publicinterfaceICalculator{ intadd(inta,intb); }
這個(gè)接口有一個(gè)實(shí)現(xiàn)類:
publicclassCalculatorImplimplementsICalculator{ @Override publicintadd(inta,intb){ System.out.println(a+"+"+b+"="+(a+b)); returna+b; } }
現(xiàn)在,我想通過動(dòng)態(tài)代理實(shí)現(xiàn)統(tǒng)計(jì)該接口的執(zhí)行時(shí)間功能,JDK 動(dòng)態(tài)代理如下:
publicclassDemo02{ publicstaticvoidmain(String[]args){ CalculatorImplcalculator=newCalculatorImpl(); ICalculatorproxyInstance=(ICalculator)Proxy.newProxyInstance(Demo02.class.getClassLoader(),newClass[]{ICalculator.class},newInvocationHandler(){ @Override publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ longstartTime=System.currentTimeMillis(); Objectinvoke=method.invoke(calculator,args); longendTime=System.currentTimeMillis(); System.out.println(method.getName()+"方法執(zhí)行耗時(shí)"+(endTime-startTime)+"毫秒"); returninvoke; } }); proxyInstance.add(3,4); } }
不需要任何額外依賴,都是 JDK 自帶的能力:
Proxy.newProxyInstance 方法表示要生成一個(gè)動(dòng)態(tài)代理對(duì)象。
newProxyInstance 方法有三個(gè)參數(shù),第一個(gè)是一個(gè)類加載器,第二個(gè)參數(shù)是一個(gè)被代理的對(duì)象所實(shí)現(xiàn)的接口,第三個(gè)則是具體的代理邏輯。
在 InvocationHandler 中,有一個(gè) invoke 方法,該方法有三個(gè)參數(shù),分別表示當(dāng)前代理對(duì)象,被攔截下來的方法以及方法的參數(shù),我們?cè)谠摲椒ㄖ锌梢越y(tǒng)計(jì)被攔截方法的執(zhí)行時(shí)間,通過方式執(zhí)行被攔截下來的目標(biāo)方法。
最終,第一步的方法返回了一個(gè)代理對(duì)象,執(zhí)行該代理對(duì)象,就有代理的效果了。
上面這個(gè)案例就是一個(gè) JDK 動(dòng)態(tài)代理。這是一種運(yùn)行時(shí)增強(qiáng),在編譯階段并未修改我們的代碼。
2.2.2 CGLIB 動(dòng)態(tài)代理
從 SpringBoot2 開始,AOP 默認(rèn)使用的動(dòng)態(tài)代理就是 CGLIB 動(dòng)態(tài)代理了,相比于 JDK 動(dòng)態(tài)代理,CGLIB 動(dòng)態(tài)代理支持代理一個(gè)類。
使用 CGLIB 動(dòng)態(tài)代理,需要首先添加依賴,如下:
cglib cglib 3.3.0
假設(shè)我有一個(gè)計(jì)算器,如下:
publicclassCalculator{ publicintadd(inta,intb){ System.out.println(a+"+"+b+"="+(a+b)); returna+b; } }
大家注意,這個(gè)計(jì)算器就是一個(gè)實(shí)現(xiàn)類,沒有接口。
現(xiàn)在,我想統(tǒng)計(jì)這個(gè)計(jì)算器方法的執(zhí)行時(shí)間,首先,我添加一個(gè)方法執(zhí)行的攔截器:
publicclassCalculatorInterceptorimplementsMethodInterceptor{ @Override publicObjectintercept(Objecto,Methodmethod,Object[]objects,MethodProxymethodProxy)throwsThrowable{ longstartTime=System.currentTimeMillis(); Objectresult=methodProxy.invokeSuper(o,objects); longendTime=System.currentTimeMillis(); System.out.println(method.getName()+"方法執(zhí)行耗時(shí)"+(endTime-startTime)+"毫秒"); returnresult; } }
當(dāng)把代理方法攔截下來之后,額外要做的事情就在 intercept 方法中完成。通過執(zhí)行 methodProxy.invokeSuper 可以調(diào)用到代理方法。
最后,配置 CGLIB,為方法配置增強(qiáng):
publicclassDemo03{ publicstaticvoidmain(String[]args){ Enhancerenhancer=newEnhancer(); enhancer.setSuperclass(Calculator.class); enhancer.setCallback(newCalculatorInterceptor()); Calculatorcalculator=(Calculator)enhancer.create(); calculator.add(4,5); } }
這里其實(shí)就是創(chuàng)建了字節(jié)增強(qiáng)器,為生成的代理對(duì)象配置 superClass,然后設(shè)置攔截下來之后的回調(diào)函數(shù)就行了,最后通過 create 方法獲取到一個(gè)代理對(duì)象。
這就是 CGLIB 動(dòng)態(tài)代理。
3. 小結(jié)
經(jīng)過上面的介紹,現(xiàn)在大家應(yīng)該搞明白了靜態(tài)代理、編譯時(shí)增強(qiáng)的動(dòng)態(tài)代理和運(yùn)行時(shí)增強(qiáng)的動(dòng)態(tài)代理了吧~
那么我們?cè)陧?xiàng)目中到底該如何選擇呢?
先來說 AspectJ 的幾個(gè)優(yōu)勢(shì)吧。
Spring AOP 由于要生成動(dòng)態(tài)代理類,因此,對(duì)于一些 static 或者 final 修飾的方法,是無法代理的,因?yàn)檫@些方法是無法被重寫的,final 修飾的類也無法被繼承。但是,AspectJ 由于不需要?jiǎng)討B(tài)生成代理類,一切都是編譯時(shí)完成的,因此,這個(gè)問題在 AspectJ 中天然的就被解決了。
Spring AOP 有一個(gè)局限性,就是只能用到被 Spring 容器管理的 Bean 上,其他的類則無法使用,AspectJ 則無此限制(話說回來,Java 項(xiàng)目 Spring 基本上都是標(biāo)配了,所以這點(diǎn)其實(shí)到也不重要)。
Spring AOP 只能在運(yùn)行時(shí)增強(qiáng),而 AspectJ 則支持編譯時(shí)增強(qiáng),編譯后增強(qiáng)以及運(yùn)行時(shí)增強(qiáng)。
Spring AOP 支持方法的增強(qiáng),然而 AspectJ 支持方法、屬性、構(gòu)造器、靜態(tài)對(duì)象、final 類/方法等的增強(qiáng)。
AspectJ 由于是編譯時(shí)增強(qiáng),因此運(yùn)行效率也要高于 Spring AOP。
。。。
雖然 AspectJ 有這么多優(yōu)勢(shì),但是 Spring AOP 卻有另外一個(gè)制勝法寶,那就是簡(jiǎn)單易用!
所以,我們?nèi)粘i_發(fā)中,還是 Spring AOP 使用更多。
審核編輯:劉清
-
編譯器
+關(guān)注
關(guān)注
1文章
1636瀏覽量
49178 -
計(jì)算器
+關(guān)注
關(guān)注
16文章
437瀏覽量
37396 -
JAVA語言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20116 -
AOP
+關(guān)注
關(guān)注
0文章
40瀏覽量
11111
原文標(biāo)題:似懂非懂的 AspectJ
文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論