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邏輯表示式,並且可以使用任何暴露出來的變數,比如連線點變數thisJoinPoint
,thisJoinPointStaticPart
和thisJoinPointEnclosingStaticPart
。
當使用註解的方式的時候,因為我們無法在註解值中寫一個完整的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方法,並且使用Before
、After
、AfterReturning
、AfterThrowing
或者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);
}
如果通知需要訪問thisJoinPoint
、thisJoinPointStaticPart
和thisEnclosingJoinPointStaticPart
,它們需要被宣告為一個額外的方法引數:
@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);
}
}