今天想和小夥伴們聊一下我們在使用 Spring AOP 時,一個非常常見的概念 AspectJ。
1. 關於代理
小夥伴們知道,Java 23 種設計模式中有一種模式叫做代理模式,這種代理我們可以將之稱為靜態代理,Spring AOP 我們常説是一種動態代理,那麼這兩種代理的區別在哪裏呢?
1.1 靜態代理
這種代理在我們日常生活中其實非常常見,例如房屋中介就相當於是一個代理,當房東需要出租房子的時候,需要發佈廣告、尋找客户、清理房間。。。由於比較麻煩,因此房東可以將租房子這件事情委託給中間代理去做。這就是一個靜態代理。
我通過一個簡單的代碼來演示一下,首先我們有一個租房的接口,如下:
public interface Rent {
void rent();
}
房東實現了該接口,表示想要出租房屋:
public class Landlord implements Rent{
@Override
public void rent() {
System.out.println("房屋出租");
}
}
中介作為中間代理,也實現了該接口,同時代理了房東,如下:
public class HouseAgent implements Rent {
private Landlord landlord;
public HouseAgent(Landlord landlord) {
this.landlord = landlord;
}
public HouseAgent() {
}
@Override
public void rent() {
publishAd();
landlord.rent();
agencyFee();
}
public void publishAd() {
System.out.println("發佈招租廣告");
}
public void agencyFee() {
System.out.println("收取中介費");
}
}
可以看到,中介的 rent 方法中,除了調用房東的 rent 方法之外,還調用了 publishAd 和 agencyFee 兩個方法。
接下來客户租房,只需要和代理打交道就可以了,如下:
public class Client {
public static void main(String[] args) {
Landlord landlord = new Landlord();
HouseAgent houseAgent = new HouseAgent(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 中的配置即可:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.7.M3</version>
</dependency>
這樣,開發環境就準備好了。
接下來,假設我有一個銀行轉帳的方法:
public class MoneyService {
public void transferMoney() {
System.out.println("轉賬操作");
}
}
我想給這個方法添加事務,那麼我就新建一個 Aspect,如下:
public aspect TxAspect {
void around():call(void MoneyService.transferMoney()){
System.out.println("開啓事務");
try {
proceed();
System.out.println("提交事務事務");
} catch (Exception e) {
System.out.println("回滾事務");
}
}
}
這就是 AspectJ 的語法,跟 Java 有點像,但是不太一樣。需要注意的是,這個 TxAspect 不是一個 Java 類,它的後綴是 .aj。
proceed 表示繼續執行目標方法,前後邏輯比較簡單,我就不多説了。
最後,我們去運行轉賬服務:
public class Demo01 {
public static void main(String[] args) {
MoneyService moneyService = new MoneyService();
moneyService.transferMoney();
}
}
運行結果如下:
這就是一個靜態代理。
為什麼這麼説呢?我們通過 IDEA 來查看一下 TxAspect 編譯之後的結果:
@Aspect
public class TxAspect {
static {
try {
ajc$postClinit();
} catch (Throwable var1) {
ajc$initFailureCause = var1;
}
}
public TxAspect() {
}
@Around(
value = "call(void MoneyService.transferMoney())",
argNames = "ajc$aroundClosure"
)
public void ajc$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("回滾事務");
}
}
public static TxAspect aspectOf() {
if (ajc$perSingletonInstance == null) {
throw new NoAspectBoundException("org_javaboy_demo_p2_TxAspect", ajc$initFailureCause);
} else {
return ajc$perSingletonInstance;
}
}
public static boolean hasAspect() {
return ajc$perSingletonInstance != null;
}
}
再看一下編譯之後的啓動類:
public class Demo01 {
public Demo01() {
}
public static void main(String[] args) {
MoneyService moneyService = new MoneyService();
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 動態代理則無此要求。
假設我現在有一個計算器接口:
public interface ICalculator {
int add(int a, int b);
}
這個接口有一個實現類:
public class CalculatorImpl implements ICalculator {
@Override
public int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
}
現在,我想通過動態代理實現統計該接口的執行時間功能,JDK 動態代理如下:
public class Demo02 {
public static void main(String[] args) {
CalculatorImpl calculator = new CalculatorImpl();
ICalculator proxyInstance = (ICalculator) Proxy.newProxyInstance(Demo02.class.getClassLoader(), new Class[]{ICalculator.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = System.currentTimeMillis();
Object invoke = method.invoke(calculator, args);
long endTime = System.currentTimeMillis();
System.out.println(method.getName() + " 方法執行耗時 " + (endTime - startTime) + " 毫秒");
return invoke;
}
});
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>
假設我有一個計算器,如下:
public class Calculator {
public int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
}
大家注意,這個計算器就是一個實現類,沒有接口。
現在,我想統計這個計算器方法的執行時間,首先,我添加一個方法執行的攔截器:
public class CalculatorInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = methodProxy.invokeSuper(o, objects);
long endTime = System.currentTimeMillis();
System.out.println(method.getName() + " 方法執行耗時 " + (endTime - startTime) + " 毫秒");
return result;
}
}
當把代理方法攔截下來之後,額外要做的事情就在 intercept 方法中完成。通過執行 methodProxy.invokeSuper 可以調用到代理方法。
最後,配置 CGLIB,為方法配置增強:
public class Demo03 {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Calculator.class);
enhancer.setCallback(new CalculatorInterceptor());
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 使用更多。