關於 AspectJ


基於 Java 動態代理或者是 cglib,是在執行時期動態生成目標物件之子類作為代理類別,也就是在執行時期將關切點織入(Weaving)主要流程,不過,也有在載入時期或編譯時期織入的技術。

AspectJ 就是支援編譯時期織入的方案,它對 Java 程式語言做了些擴充,可以透過 aspectbefore 等語法來定義橫切入主要流程的關切點,並透過自己的 ajc 編譯出 .class 檔案,在靜態時期完成關注點的織入,它的觀念與設計也影響了不少 AOP 框架的實現,例如 Spring AOP。

Spring 從 2.0 就開始支援 AspectJ,並努力與 AspectJ 的使用方式一致,因而,透過實際操作一下 AspectJ,有助於在 Spring 中運用 AspectJ 進行 AOP 設計。

你可以在 AspectJ 下載新的 AspectJ,在這邊使用的是 AspectJ 1.9.2,下載檔案是 aspectj-1.9.2.jar,可以使用 java -jar aspectj-1.9.2.jar 來執行安裝,過程中會詢問你 JDK 安裝位置,以及你 AspectJ 安裝的目標資料夾。

完成安裝之後,AspectJ 安裝的目標資料夾中有個 bin 目錄,其中包含了 ajc 編譯器的指令稿,記得加入 PATH 之中以便於使用。

現在假設你有個 Hello 撰寫如下:

package cc.openhome.model;

public class Hello {
    public void hello(String name) {
        System.out.printf("Hello, %s%n", name);
    }
}

並有個 Main 類別:

package cc.openhome;

import cc.openhome.model.Hello;

public class Main {
    public static void main(String[] args) {
        new Hello().hello("XD");
    }
}

如果你想要在呼叫 Hello 實例的方法前進行日誌,那麼可以撰寫一個副檔名為 .aj 或 .java 的檔案:

package cc.openhome.aspect;

import java.util.Arrays;
import java.util.logging.Logger;

public aspect LoggingAspect {
    before() : execution(* cc.openhome.model.Hello.*(..)) {
        Object target = thisJoinPoint.getTarget();
        String methodName = thisJoinPoint.getSignature().getName();
        Object[] args = thisJoinPoint.getArgs();
        Logger.getLogger(target.getClass().getName()).info(String.format("%s.%s(%s)",
                target.getClass().getName(), methodName, Arrays.toString(args)));
    }
}

在這邊可以看到 aspectbeforeexecution 甚至是 thisJointPoint 等關鍵字,這是 AspectJ 對 Java 語言的擴充。

LoggingAspect 中的日誌是個關切點,不過就像 Servlet 中過濾器服務那樣,這類關切點與主要流程的關切點是橫切的,辨識出這類的關切點,將它們設計為可重用、可抽換的元件,這類元件稱為 Aspect,設計的過程稱為 Aspect-oriented Programming,也就是 AOP 的全名。

使用 AspectJ 設計 Aspect 元件時,可以使用 aspect,不用實作介面或繼承類別;橫切進主要流程的服務稱為 Advice,由於現在是希望某方法被呼叫前執行日誌,可以使用 before() 來定義方法,表示這是個 Before Advice,一個 Aspect 元件,實際上可以有多個 Advice,有興趣的話,可以自行參考 AspectJ 的文件。

Advice 會在程式執行時的某些點上接入,這些點稱為 Join Point,用來定義是否符合 Join Point 的斷言稱為 Pointcut,execution(* cc.openhome.model.Hello.*(..)) 就是用來定義是否符合 Join Point 的斷言。

在執行 Advice 時,Join Point 的資訊會封裝為 JoinPoint 實例,在 Advice 之中,可以透過 thisJoinPoint 來取得。

以上面的例子來說,execution(* cc.openhome.model.Hello.*(..)) 表示,會在 Hello 的任何方法執行時進行日誌,第一個 * 表示任何傳回型態,第二個 * 表示所有方法,而 .. 表示任何引數。

接著,可以使用 ajc 來進行編譯:

> ajc -d classes -cp C:\workspace\aspectj1.9\lib\aspectjrt.jar -sourceroots src -source 1.9 -target 1.9

在編譯成功之後,就可以直接使用 java 來運行程式:

> java -cp classes;C:\workspace\aspectj1.9\lib\aspectjrt.jar cc.openhome.Main
11月 26, 2018 4:24:28 下午 cc.openhome.aspect.LoggingAspect ajc$before$cc_openhome_aspect_LoggingAspect$1$fe9ec69c
INFO: cc.openhome.model.Hello.hello([XD])
Hello, XD

當然,手動進行編譯與執行是件麻煩事,可以透過 IDE 或 Gradle 來做這類事情,例如,Eclipse 可以安裝 AJDT,你可以在 AspectJDemo 找到以上的範例專案,就是基於 AJDT 來編寫。

在原生的 Java 語言上,其實也已經可以使用 AspectJ 的標註來進行相關設計,例如 LoggingAspect 也可以撰寫如下:

package cc.openhome.aspect;

import java.util.Arrays;
import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class LoggingAspect {
    @Before("execution(* cc.openhome.model.Hello.*(..))")
    public void before(JoinPoint joinPoint) {
        Object target = joinPoint.getTarget();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Logger.getLogger(target.getClass().getName()).info(String.format("%s.%s(%s)",
                target.getClass().getName(), methodName, Arrays.toString(args)));
    }
}

Spring 若要基於標註方式來進行 AOP 相關設計,就是基於 AspectJ,而這是之後要討論的主題。