1. 程式人生 > >AspectJ——基於註解的開發方式

AspectJ——基於註解的開發方式

基於註解的開發方式

AspectJ5版本支援了基於註解的開發方式,當然其仍然需要AspectJ自己的編譯器。

要使用基於註解的開發方式,需要為專案引入aspectjweaver.jar包,該Jar包也在AspectJ安裝目錄下的lib目錄中。aspectjweaver.jar中包含了aspectjrt.jar包中的內容,所以只需要引入aspectjweaver.jar包即可。

0.一個示例

此時我們在Test16包下做一個測試。首先建立業務類Service和測試類Main,如下:

package Test16;

public class Service {
    public
int add(int a, int b) { return a + b; } public double square(double a) { return a * a; } public String upper(String string) { return string.toUpperCase(); } }
package Test16;

public class Main {
    public static void main(String[] args) {
        Service service = new
Service(); System.out.println("service.add(1, 2) = " + service.add(1, 2)); System.out.println("service.square(6) = " + service.square(6)); System.out.println("service.upper(\"Gavin\") = " + service.upper("Gavin")); } }

此時如果執行程式,其執行結果是:

這裡寫圖片描述

接下來,我們使用註解的方式來開發一個切面AnnotationAspect

,這時候不使用aspect關鍵字了,還是使用class類,只不過該類使用註解@Aspect來標註。

package Test16;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class AnnotationAspect {
    @Pointcut("execution(* Test16.Service.add(..))")
    public void addPointcut() {

    }

    @Pointcut("call(* Test16.Service.square(double)) && args(value)")
    public void squarePointcut(double value) {

    }

    @Pointcut("call(* Test16.Service.upper(String)) && args(string)")
    public void upperPointcut(String string) {

    }

    @Before("addPointcut()")
    public void addAdvice(JoinPoint joinPoint) {
        System.out.println();
        System.out.println("joinPoint: " + joinPoint);
        System.out.println("Source Line: " + joinPoint.getSourceLocation());
    }

    @Around("squarePointcut(value)")
    public Object squareAround(double value, ProceedingJoinPoint joinPoint){
        System.out.println();
        System.out.println("接收到的引數是:" + value);
        Object result = null;
        try {
            result = joinPoint.proceed(new Object[]{value * 2});
        } catch (Throwable throwable) {

        }
        System.out.println("執行結果是:" + result);
        return result;
    }

    @AfterReturning("upperPointcut(value)")
    public void upperAfterReturning(String value) {
        System.out.println();
        System.out.println("接收到的引數是:" + value);
    }
}

我們使用@Aspect來表示該類是一個切面;使用@Pointcut來表示這個方法是一個切入點,方法名就是切入點的名字;使用@Before@After(或者@AfterReturning@AfterThrowing)和@Around來宣告這個方法是一個前置通知、後置通知和環繞通知,方法體中寫通知的程式碼。

此時執行結果如下:

這裡寫圖片描述

1.語法細節

這裡的語法細節譯自AspectJ的官方文件

1.0.簡介

支援註解開發切面的註解集合被稱為“@AspectJ”註解。在AspectJ5中,我們不僅可以使用基於註解的方式來開發切面,也可以使用基於程式碼的方式來開發切面,也可以混用它們。

1.1.宣告切面

切面可以使用org.aspectj.lang.annotation.Aspect註解宣告。下述宣告:

@Aspect
public class Foo{}

等效於:

public aspect Foo{}

1.2.切入點(Pointcuts

切入點可以通過在一個方法上使用註解org.aspectj.lang.annotation.Pointcut來指定,這個方法必須返回void,方法的引數與切入點的引數一致,方法的修飾符與切入點的修飾符一致。

一般情況下,使用@Pointcut註解的方法的方法體必須是空的,並且沒有任何throws語句。如果切入點綁定了形式引數(使用args()target()this()@args()@target()@this()@annotation()),那麼它們必須也是方法的形式引數。

if()切入點比較特殊,我們在之後介紹。

這裡是一些切入點的例子,它們同時使用程式碼方式和@AspectJ方式。

@Pointcut("call(* *.*(..))")
void anyCall(){}

等效於:

pointcut anyCall(): call(* *.*(..));

當要繫結引數的時候,只需要將引數作為被註解的方法的引數即可:

@Pointcut("call(* *.*(int)) && args(i) && target(callee)")
void anyCall(int i, Fool callee){}

它等效於:

pointcut anyCall(int i, Foo callee): call(* *.*(int)) && args(i) && target(callee);

1.2.0.if()切入點表示式

在基於程式碼的方式中,我們可以使用if(...)切入點來定義一個條件切入點表示式,它會在執行的時候對每一個候選的連線點進行評估。if(...)表示式的條件可以是任何有效的Java邏輯表示式,並且可以使用任何暴露出來的變數,比如連線點變數thisJoinPointthisJoinPointStaticPartthisJoinPointEnclosingStaticPart

當使用註解的方式的時候,因為我們無法在註解值中寫一個完整的Java表示式,所以這裡的語法有些微不同,但是與之前具有相同的語義和執行時的行為。

if()切入點表示式可以在一個@Pointcut註解中宣告,但是其條件必須是空的,被註解的方法必須是public static的,並且返回boolean型別,方法體包含被評估的條件。比如下例:

@Pointcut("call(* *.*(int)) && args(i) && if()")
public static boolean someCallWithIfTest(int i){
    return i > 0;
}

它等效於:

pointcut someCallWithIfTest(int i): call(* *.*(int)) && args(i) && if(i > 0);

下面也是一個有效的格式:

static int COUNT = 0;

@Pointcut("call(* *.*(int)) && args(i) && if()")
public static boolean someCallWithIfTest(int i, JoinPoint jp, JoinPoint.EnclosingStaticPart esjp) {
    // any legal Java expression...
    return i > 0
        && jp.getSignature().getName.startsWith("doo")
        && esjp.getSignature().getName().startsWith("test")
        && COUNT++ < 10;
}

@Before("someCallWithIfTest(anInt, jp, enc)") 
public void beforeAdviceWithRuntimeTest(int anInt, JoinPoint jp, JoinPoint.EnclosingStaticPart enc) {
//...
}

// 注意下面的寫法是不對的
/*
@Before("call(* *.*(int)) && args(i) && if()")
public void advice(int i) {
// 所以你想在這裡寫一個通知還是一個if表示式
}
*/

1.3.通知

使用註解的方式,一個通知被寫成一個普通的Java方法,並且使用BeforeAfterAfterReturningAfterThrowing或者Around註解。除了Around註解的環繞通知以外,所有的方法必須返回void。方法必須是public的。

下述例子使用兩種不同的方式表示了一個簡單的前置通知:

@Before("call(* org.aspectprogrammer..*(..)) && this(Foo)")
public void callFromFoo(){
    System.out.println("Call From Foo")
}

它等效於:

before(): call(* org.aspectprogrammer..*(..)) && this(Foo){
    System.out.println("Call From Foo");
}

如果通知需要知道是哪一個具體的Foo物件做出的這個方法呼叫,只需要為通知宣告加上一個引數即可:

before(Foo foo):  call(* org.aspectprogrammer..*(..)) && this(foo){
    System.out.println("Call From Foo" + foo);
}

這可以寫成:

@Before("call(* org.aspectprogrammer..*(..)) && this(foo)")
public void callFromFoo(Foo foo) {
    System.out.println("Call from Foo: " + foo);
}

如果通知需要訪問thisJoinPointthisJoinPointStaticPartthisEnclosingJoinPointStaticPart,它們需要被宣告為一個額外的方法引數:

@Before("call(* org.aspectprogrammer..*(..)) && this(foo)")
public void callFromFoo(JoinPoint thisJoinPoint, Foo foo){
    System.out.println("Call from Foo: " + foo + " at " + thisJoinPoint);
}

這等效於:

 before(Foo foo) : call(* org.aspectprogrammer..*(..)) && this(foo) {
    System.out.println("Call from Foo: " + foo + " at " + thisJoinPoint);
}

同時需要三個引數的通知可以宣告為:

@Before("call(* org.aspectprogrammer..*(..)) && this(Foo)")
public void callFromFoo(JoinPoint thisJoinPoint,
                        JoinPoint.StaticPart thisJoinPointStaticPart,
                        JoinPoint.EnclosingStaticPart thisEnclosingJoinPointStaticPart) {
    // ...
}

後置通知After()與前置通知Before()的宣告方式是一樣,並且成功的後置通知AfterReturning在不需要獲取返回值、異常的後置通知AfterThrowing在不需要獲取丟擲的異常的時候,它們的宣告方式也是和前置通知一樣的。

如果要使用成功的後置通知獲取返回值,只需要將返回值宣告為方法的引數,並且在註解中將它繫結到returning屬性上:

@AfterReturning("criticalOperation()")
public void phew(){
    System.out.println("phew");
}

@AfterReturning(pointcut="call(Foo+.new(..))", returning="f")
public void itsAFoo(Foo f){
    System.out.println("It's a Foo: " + f);
}

這等效於:

after() returning: criticalOperation(){
    System.out.println("phew");
}

after() returning(Foo f) : call(Foo+.new(..)){
    System.out.println("It's a Foo: " + f);
}

(需要注意的是,需要在切入點表示式之前使用“pointcut=”字首)

異常的後置通知AfterThrowing與此類似,當需要獲取丟擲的異常物件的時候,使用throwing屬性來繫結引數即可。

對於環繞通知來說,我們需要來解決proceed()的問題。如果在方法體內直接呼叫proceed()方法:

@Around("call(* org.aspectprogrammer..*(..))")
public Object doNothing(){
    return proceed();
}

我們會得到一個No Such method的編譯錯誤。為此,AspectJ5定義了JoinPoint的一個子介面ProceedingJoinPoint

public interface ProceedingJoinPoint extends JoinPoint{
    public Object proceed(Object[] args);
}

那麼上面的環繞通知可以這樣寫:

@Around("call(* org.aspectprogrammer..*(..))")
public Object doNothing(ProceedingJoinPoint thisJoinPoint){
    return thisJoinPoint.proceed();
}

這是一個在proceed()呼叫中使用引數的例子:

@Aspect
public class ProceedAspect{
    @Pointcut("call(* setAge(..)) && args(i)")
    void setAge(int i){}

    @Around("setAge(i)")
    public Object twiceAsOld(ProceedingJoinPoint thisJoinPoint, int i){
        return thisJoinPoint.proceed(new Object[]{i * 2});
    }
}

它等效於:

public aspect ProceedAspect{
    pointcut setAge(int i): call(* setAge(..)) && args(i);

    Object around(int i): setAge(i){
        return proceed(i * 2);
    }
}