博客 / 詳情

返回

似懂非懂的 AspectJ

今天想和小夥伴們聊一下我們在使用 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 的定位:

  1. 基於 Java 語言的面向切面編程語言。
  2. 兼容 Java。
  3. 易學易用。

使用 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(這個是針對當前項目的設置,所以可以放心修改):

有如下幾個需要修改的點:

  1. 首先修改編譯器為 ajc。
  2. 將使用的 Java 版本改為 8,這個一共有兩個地方需要修改。
  3. 設置 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 自帶的能力:

  1. Proxy.newProxyInstance 方法表示要生成一個動態代理對象。
  2. newProxyInstance 方法有三個參數,第一個是一個類加載器,第二個參數是一個被代理的對象所實現的接口,第三個則是具體的代理邏輯。
  3. 在 InvocationHandler 中,有一個 invoke 方法,該方法有三個參數,分別表示當前代理對象,被攔截下來的方法以及方法的參數,我們在該方法中可以統計被攔截方法的執行時間,通過方式執行被攔截下來的目標方法。
  4. 最終,第一步的方法返回了一個代理對象,執行該代理對象,就有代理的效果了。

上面這個案例就是一個 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 的幾個優勢吧。

  1. Spring AOP 由於要生成動態代理類,因此,對於一些 static 或者 final 修飾的方法,是無法代理的,因為這些方法是無法被重寫的,final 修飾的類也無法被繼承。但是,AspectJ 由於不需要動態生成代理類,一切都是編譯時完成的,因此,這個問題在 AspectJ 中天然的就被解決了。
  2. Spring AOP 有一個侷限性,就是隻能用到被 Spring 容器管理的 Bean 上,其他的類則無法使用,AspectJ 則無此限制(話説回來,Java 項目 Spring 基本上都是標配了,所以這點其實到也不重要)。
  3. Spring AOP 只能在運行時增強,而 AspectJ 則支持編譯時增強,編譯後增強以及運行時增強。
  4. Spring AOP 支持方法的增強,然而 AspectJ 支持方法、屬性、構造器、靜態對象、final 類/方法等的增強。
  5. AspectJ 由於是編譯時增強,因此運行效率也要高於 Spring AOP。
  6. 。。。

雖然 AspectJ 有這麼多優勢,但是 Spring AOP 卻有另外一個制勝法寶,那就是簡單易用

所以,我們日常開發中,還是 Spring AOP 使用更多。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.