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