今天想和小伙伴們聊一下我們在使用 Spring AOP 時,一個非常常見的概念 AspectJ。
1. 關于代理
小伙伴們知道,JAVA 23 種設計模式中有一種模式叫做代理模式,這種代理我們可以將之稱為靜態代理,Spring AOP 我們常說是一種動態代理,那么這兩種代理的區別在哪里呢?
1.1 靜態代理
這種代理在我們日常生活中其實非常常見,例如房屋中介就相當于是一個代理,當房東需要出租房子的時候,需要發布廣告、尋找客戶、清理房間。。。由于比較麻煩,因此房東可以將租房子這件事情委托給中間代理去做。這就是一個靜態代理。
我通過一個簡單的代碼來演示一下,首先我們有一個租房的接口,如下:
publicinterfaceRent{
voidrent;
}
房東實現了該接口,表示想要出租房屋:
publicclassLandlordimplementsRent{
@Override
publicvoidrent{
System.out.println("房屋出租");
}
}
中介作為中間代理,也實現了該接口,同時代理了房東,如下:
publicclassHouseAgentimplementsRent{
privateLandlord landlord;
publicHouseAgent(Landlord landlord){
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){
Landlord landlord = newLandlord;
HouseAgent houseAgent = newHouseAgent(landlord);
houseAgent.rent;
}
}
這就是一個簡單的代理模式。無論大家是否有接觸過 Java 23 種設計模式,上面這段代碼應該都很好理解。
這是靜態代理。
1.2 動態代理
動態代理講究在不改變原類原方法的情況下,增強目標方法的功能,例如,大家平時使用的 Spring 事務功能,在不改變目標方法的情況下,就可以通過動態代理為方法添加事務處理能力。再比如松哥在 TienChin 項目中所講的日志處理、接口冪等性處理、多數據源處理等,都是動態代理能力的體現:
從實現原理上,我們又可以將動態代理劃分為兩大類:
-
編譯時增強。
-
運行時增強。
編譯時增強,這種有點類似于 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 中的配置即可:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.7.M3</version>
</dependency>
這樣,開發環境就準備好了。
接下來,假設我有一個銀行轉帳的方法:
publicclassMoneyService{
publicvoidtransferMoney{
System.out.println("轉賬操作");
}
}
我想給這個方法添加事務,那么我就新建一個 Aspect,如下:
publicaspect TxAspect {
voidaround:call(voidMoneyService.transferMoney){
System.out.println("開啟事務");
try{
proceed;
System.out.println("提交事務事務");
} catch(Exception e) {
System.out.println("回滾事務");
}
}
}
這就是 AspectJ 的語法,跟 Java 有點像,但是不太一樣。需要注意的是,這個 TxAspect 不是一個 Java 類,它的后綴是 .aj。
proceed 表示繼續執行目標方法,前后邏輯比較簡單,我就不多說了。
最后,我們去運行轉賬服務:
publicclassDemo01{
publicstaticvoidmain(String[] args){
MoneyService moneyService = newMoneyService;
moneyService.transferMoney;
}
}
運行結果如下:
這就是一個靜態代理。
為什么這么說呢?我們通過 IDEA 來查看一下 TxAspect 編譯之后的結果:
@Aspect
publicclassTxAspect{
static{
try{
ajc$postClinit;
} catch(Throwable var1) {
ajc$initFailureCause = var1;
}
}
publicTxAspect{
}
@Around(
value = "call(void MoneyService.transferMoney)",
argNames = "ajc$aroundClosure"
)
publicvoidajc$around$org_javaboy_demo_p2_TxAspect$1$3b99afea(AroundClosure ajc$aroundClosure) {
System.out.println("開啟事務");
try{
ajc$around$org_javaboy_demo_p2_TxAspect$1$3b99afeaproceed(ajc$aroundClosure);
System.out.println("提交事務事務");
} catch(Exception var2) {
System.out.println("回滾事務");
}
}
publicstaticTxAspect aspectOf{
if(ajc$perSingletonInstance == null) {
thrownewNoAspectBoundException("org_javaboy_demo_p2_TxAspect", ajc$initFailureCause);
} else{
returnajc$perSingletonInstance;
}
}
publicstaticbooleanhasAspect{
returnajc$perSingletonInstance != null;
}
}
再看一下編譯之后的啟動類:
publicclassDemo01{
publicDemo01{
}
publicstaticvoidmain(String[] args){
MoneyService moneyService = 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){
CalculatorImpl calculator = newCalculatorImpl;
ICalculator proxyInstance = (ICalculator) Proxy.newProxyInstance(Demo02.class.getClassLoader, newClass[]{ICalculator.class}, newInvocationHandler{
@Override
publicObject invoke(Object proxy, Method method, Object[] args)throwsThrowable {
longstartTime = System.currentTimeMillis;
Object invoke = 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 動態代理,需要首先添加依賴,如下:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
假設我有一個計算器,如下:
publicclassCalculator{
publicintadd(inta, intb){
System.out.println(a + "+"+ b + "="+ (a + b));
returna + b;
}
}
大家注意,這個計算器就是一個實現類,沒有接口。
現在,我想統計這個計算器方法的執行時間,首先,我添加一個方法執行的攔截器:
publicclassCalculatorInterceptorimplementsMethodInterceptor{
@Override
publicObject intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)throwsThrowable {
longstartTime = System.currentTimeMillis;
Object result = methodProxy.invokeSuper(o, objects);
longendTime = System.currentTimeMillis;
System.out.println(method.getName + " 方法執行耗時 "+ (endTime - startTime) + " 毫秒");
returnresult;
}
}
當把代理方法攔截下來之后,額外要做的事情就在 intercept 方法中完成。通過執行 methodProxy.invokeSuper 可以調用到代理方法。
最后,配置 CGLIB,為方法配置增強:
publicclassDemo03{
publicstaticvoidmain(String[] args){
Enhancer enhancer = newEnhancer;
enhancer.setSuperclass(Calculator.class);
enhancer.setCallback(newCalculatorInterceptor);
Calculator calculator = (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 使用更多。
END