AspectJ 語法
上篇文章介紹了 AspectJ 的基本概念,這篇文章詳細分析 AspectJ 基于注解開發方式的語法。
Join Point
Join Point 表示連接點,即 AOP 可織入代碼的點,下表列出了 AspectJ 的所有連接點:
Join Point | 說明 |
---|---|
Method call | 方法被調用 |
Method execution | 方法執行 |
Constructor call | 構造函數被調用 |
Constructor execution | 構造函數執行 |
Field get | 讀取屬性 |
Field set | 寫入屬性 |
Pre-initialization | 與構造函數有關,很少用到 |
Initialization | 與構造函數有關,很少用到 |
Static initialization | static 塊初始化 |
Handler | 異常處理 |
Advice execution | 所有 Advice 執行 |
Pointcuts
Pointcuts 是具體的切入點,可以確定具體織入代碼的地方,基本的 Pointcuts 是和 Join Point 相對應的。
Join Point | Pointcuts syntax |
---|---|
Method call | call(MethodPattern) |
Method execution | execution(MethodPattern) |
Constructor call | call(ConstructorPattern) |
Constructor execution | execution(ConstructorPattern) |
Field get | get(FieldPattern) |
Field set | set(FieldPattern) |
Pre-initialization | initialization(ConstructorPattern) |
Initialization | preinitialization(ConstructorPattern) |
Static initialization | staticinitialization(TypePattern) |
Handler | handler(TypePattern) |
Advice execution | adviceexcution() |
除了上面與 Join Point 對應的選擇外,Pointcuts 還有其他選擇方法:
Pointcuts synatx | 說明 |
---|---|
within(TypePattern) | 符合 TypePattern 的代碼中的 Join Point |
withincode(MethodPattern) | 在某些方法中的 Join Point |
withincode(ConstructorPattern) | 在某些構造函數中的 Join Point |
cflow(Pointcut) | Pointcut 選擇出的切入點 P 的控制流中的所有 Join Point,包括 P 本身 |
cflowbelow(Pointcut) | Pointcut 選擇出的切入點 P 的控制流中的所有 Join Point,不包括 P 本身 |
this(Type or Id) | Join Point 所屬的 this 對象是否 instanceOf Type 或者 Id 的類型 |
target(Type or Id) | Join Point 所在的對象(例如 call 或 execution 操作符應用的對象)是否 instanceOf Type 或者 Id 的類型 |
args(Type or Id, …) | 方法或構造函數參數的類型 |
if(BooleanExpression) | 滿足表達式的 Join Point,表達式只能使用靜態屬性、Pointcuts 或 Advice 暴露的參數、thisJoinPoint 對象 |
Pointcut 表達式還可以 !、&&、|| 來組合,!Pointcut 選取不符合 Pointcut 的 Join Point,Pointcut0 && Pointcut1 選取符合 Pointcut0 和 Pointcut1 的 Join Point,Pointcut0 || Pointcut1 選取符合 Pointcut0 或 Pointcut1 的 Join Point。
上面 Pointcuts 的語法中涉及到一些 Pattern,下面是這些 Pattern 的規則,[]
里的內容是可選的:
Pattern | 規則 |
---|---|
MethodPattern | [!][@Annotation] [public,protected,private][static] [final] 返回值類型 [類名.]方法名(參數類型列表) [throws 異常類型] |
ConstructorPattern | [!][@Annotation] [public,protected,private][final] [類名.]new(參數類型列表) [throws 異常類型] |
FieldPattern | [!][@Annotation] [public,protected,private][static] [final] 屬性類型 [類名.]屬性名 |
TypePattern | 其他 Pattern 涉及到的類型規則也是一樣,可以使用 ‘!’、’‘、’..’、’+’,’!’ 表示取反,’‘ 匹配除 . 外的所有字符串,’*’ 單獨使用事表示匹配任意類型,’..’ 匹配任意字符串,’..’ 單獨使用時表示匹配任意長度任意類型,’+’ 匹配其自身及子類,還有一個 ‘…’表示不定個數 |
TypePattern 也可以使用 &&、|| 操作符,其他 Pointcut 更詳細的語法說明,見官網文檔 Pointcuts Language Semantics。
Pointcut 示例
execution(void void android.view.View.OnClickListener+.onClick(..)) – OnClickListener 及其子類的 onClick 方法執行時
call(@retrofit2.http.GET public com.johnny.core.http..(..)) – ‘com.johnny.core.http’開頭的包下面的所有 GET 方法調用時
call(android.support.v4.app.Fragment+.new(..)) – support 包中的 Fragment 及其子類的構造函數調用時
set(@Inject ) – 寫入所有 @Inject 注解修飾的屬性時
handler(IOException) && within(com.johnny.core.http..) – ‘com.johnny.core.http’開頭的包代碼中處理 IOException 時
execution(void setUserVisibleHint(..)) && target(android.support.v4.app.Fragment) && args(boolean) – 執行 Fragment 及其子類的 setUserVisibleHint(boolean) 方法時
execution(void Foo.foo(..)) && cflowbelow(execution(void Foo.foo(..))) – 執行 Foo.foo() 方法中再遞歸執行 Foo.foo() 時
Pointcut 聲明
Pointcuts 可以在普通的 class 或 Aspect class 中定義,由 org.aspectj.lang.annotation.Pointcut 注解修飾的方法聲明,方法返回值只能是 void。@Pointcut 修飾的方法只能由空的方法實現而且不能有 throws 語句,方法的參數和 pointcut 中的參數相對應。
看下面這個例子:
@Aspect
class Test {
@Pointcut("execution(void Foo.foo(..)")
public void executFoo() {}
@Pointcut("executFoo() && cflowbelow(executFoo()) && target(foo) && args(i)")
public void loopExecutFoo(Foo foo, int i) {}
}
if() 表達式
在基于 AspectJ 注解的開發方式中,if(...)
表達式的用法與其他的選擇操作符不同,在 @Pointcut 的語句中 if 表達式只能是if()
、if(true)
或if(false)
,而且 @Pointcut 方法必須為 public static boolean,方法體內就是 if 表達式的內容,可以使用暴露的參數、靜態屬性、JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart。
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;
}
if() 表達式使用的比較少,大致了解下就可以了。
target() 與 this()
target() 與 this() 很容易混淆,target() 是指 Pointcut 選取的 Join Point 的所有者;this() 是指 Pointcut 選取的 Join Point 的調用的所有者。簡單地說就是,PointcutA 選取的是 methodA,那么 target 就是 methodA() 這個方法的對象,而 this 就是 methodA 被調用時所在類的對象。
看下面這個例子:
class Test {
public void test() {...}
}
class A {
...
test1.test(); // test() 在 a 的某方法中調用
...
}
@Aspect
class TestAspect {
@Pointcut("call(void Test.test()) && target(Test)")
public test1() {}
@Pointcut("call(void Test.test()) && this(A)")
public test2() {}
}
上面代碼中 test1.test()
方法屬于 test1 對象,所以 target 為 test1,而該方法在 a 對象的方法中調用,所以 this 為 a。
Advice
Advice 是在切入點上織入的代碼,在 AspectJ 中有五種類型:Before、After、AfterReturning、AfterThrowing、Around。
Advice | 說明 |
---|---|
@Before | 在執行 Join Point 之前 |
@After | 在執行 Join Point 之后,包括正常的 return 和 throw 異常 |
@AfterReturning | Join Point 為方法調用且正常 return 時,不指定返回類型時匹配所有類型 |
@AfterThrowing | Join Point 為方法調用且拋出異常時,不指定異常類型時匹配所有類型 |
@Around | 替代 Join Point 的代碼,如果要執行原來代碼的話,要使用 ProceedingJoinPoint.proceed() |
注意: After 和 Before 沒有返回值,但是 Around 的目標是替代原 Join Point 的,所以它一般會有返回值,而且返回值的類型需要匹配被選中的 Join Point 的代碼。而且不能和其他 Advice 一起使用,如果在對一個 Pointcut 聲明 Around 之后還聲明 Before 或者 After 則會失效。
Advice 注解修改的方法必須為 public,Before、After、AfterReturning、AfterThrowing 四種類型修飾的方法返回值也必須為 void,Advice 需要使用 JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart 時,要在方法中聲明為額外的參數,@Around 方法可以使用 ProceedingJoinPoint,用以調用 proceed() 方法。
看下面幾個示例,進一步了解 Advice 用法:
@Before("call(* *.*(..)) && this(foo)")
public void callFromFoo(Foo foo) {
Log.d(TAG, "call from Foo:" + foo);
}
@AfterReturning(pointcut="call(Foo+.new(..))", returning="f")
public void itsAFoo(Foo f, JoinPoint thisJoinPoint) {
// ...
}
@Around("call(* setAge(..)) && args(i)")
public Object twiceAsOld(int i, ProceedingJoinPoint thisJoinPoint) {
return thisJoinPoint.proceed(new Object[]{i * 2}); // 原來參數乘以 2
}
注:Handler Pointcut 不能使用 After 和 Around。
Aspect
Aspect 就是 AOP 中的關鍵單位 – 切面,我們一般會把相關 Pointcut 和 Advice 放在一個 Aspect 類中,在基于 AspectJ 注解開發方式中只需要在類的頭部加上 @Aspect 注解即可,@Aspect 不能修飾接口。
例如,定義一個 LogAspect,在需要的 Join Point 上加上打印日志的 Advice,這樣就形成了一個 LogAspect 的切面,在編譯期會將代碼織入到相應的方法中,但是在編碼中只需要關注 LogAspect 即可。
在多個切入點織入 Advice 代碼時,會涉及到 Aspect 對象實例的問題,因為 Advice 代碼是 Aspect 的方法。一般情況下,我們使用的都是單例的 Aspect,即所有 Advice 代碼使用的都是同一個 Aspect 對象實例。
Singleton Aspect
文章中代碼示例都是單例的 Aspect,這也是最常見的,定義方式為:@Aspect
或者 @Aspect()
。
編譯期,ajc 編譯期會給單例的切面加上靜態的 aspectOf() 方法來獲取單例實例,還有一個 hasAspect() 靜態方法判斷實例是否初始化。假設 FragmentAspect 有 Advice 方法 advice1(),織入切入點的代碼就是 FragmentAspect.aspectOf().advice1()。
Per-object, Per-cflow Aspect 等
除了單例 Aspect 外,還可以根據 Join Point 的相應對象、控制流、所在類型產生不同的實例。
定義方式為:@Aspect("perthis|pertarget|percflow|percflowbelow(Pointcut) | pertypewithin(TypePattern)")
,因為不常見,所以就簡單介紹下,想進一步了解請看 Aspects Language Semantics 。
Inter-type Declarations
上面提到的都是 Pointcut 和 Advice 都是在類本身結構不變的情況下織入代碼,AspectJ 的 Inter-type Declarations 可以修改類的結構,給類添加方法或者屬性,讓類繼承多個類或者實現多個接口。但是基于 AspectJ 注解開發方式因為技術原因,目前只能讓類實現多個接口,通俗的說法就是給類添加接口,也添加了接口的方法。
給類添加接口,實際通過實現了該接口的代理來完成對原類型的替換,所以需要提供實現了該接口的實現完成代理中接口的具體行為,不然只是增加接口,沒有接口實現沒什么用處。@DeclareMixin 就是用來確定接口的默認實現,綁定一個產生該接口的默認實現的工廠方法,以該接口為返回類型。
看下面代碼,給 Fragment 添加 Title 接口:
public interface Title {
String getTitle();
}
public class TitleImpl implements Title {
@Override
public String getTitle() {
return "Test";
}
}
@Aspect
public class FragmentAspect {
@DeclareMixin("android.support.v4.app.Fragment")
public static Title createDelegate() {
return new TitleImpl();
}
}
上面代碼可以給 Fragment 添加了 Title 接口,如果@DeclareMixin("android.support.v4.app.*")
的話,則給 app 下所有類添加 Title 接口,之后通過正常的類型轉換來訪問 Title 接口:
String title = ((Title) fragment).getTitle(); // 返回 Test 字符串
也可以將原對象作為接口默認實現的參數,這樣就可以根據 fragment 的屬性返回不同的 title :
public class TitleImpl implements Title {
private final String title;
public TitleImpl(Fragment fragment) {
title = fragment.getClass().getSimpleName();
}
@Override
public String getTitle() {
return title;
}
}
@Aspect
public class FragmentAspect {
@DeclareMixin("android.support.v4.app.Fragment")
public static Title createDelegate(Fragment fragment) {
return new TitleImpl(fragment);
}
}
上面代碼返回 fragment 的類名作為 title。