AspectJ 語法
上篇文章介紹了 AspectJ 的基本概念,這篇文章詳細(xì)分析 AspectJ 基于注解開發(fā)方式的語法劫映。
Join Point
Join Point 表示連接點(diǎn)祖今,即 AOP 可織入代碼的點(diǎn)膏斤,下表列出了 AspectJ 的所有連接點(diǎn):
Join Point | 說明 |
---|---|
Method call | 方法被調(diào)用 |
Method execution | 方法執(zhí)行 |
Constructor call | 構(gòu)造函數(shù)被調(diào)用 |
Constructor execution | 構(gòu)造函數(shù)執(zhí)行 |
Field get | 讀取屬性 |
Field set | 寫入屬性 |
Pre-initialization | 與構(gòu)造函數(shù)有關(guān)敞映,很少用到 |
Initialization | 與構(gòu)造函數(shù)有關(guān)冕末,很少用到 |
Static initialization | static 塊初始化 |
Handler | 異常處理 |
Advice execution | 所有 Advice 執(zhí)行 |
Pointcuts
Pointcuts 是具體的切入點(diǎn)攻询,可以確定具體織入代碼的地方钧栖,基本的 Pointcuts 是和 Join Point 相對應(yīng)的桐经。
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 對應(yīng)的選擇外誓沸,Pointcuts 還有其他選擇方法:
Pointcuts synatx | 說明 |
---|---|
within(TypePattern) | 符合 TypePattern 的代碼中的 Join Point |
withincode(MethodPattern) | 在某些方法中的 Join Point |
withincode(ConstructorPattern) | 在某些構(gòu)造函數(shù)中的 Join Point |
cflow(Pointcut) | Pointcut 選擇出的切入點(diǎn) P 的控制流中的所有 Join Point痊焊,包括 P 本身 |
cflowbelow(Pointcut) | Pointcut 選擇出的切入點(diǎn) P 的控制流中的所有 Join Point蕾管,不包括 P 本身 |
this(Type or Id) | Join Point 所屬的 this 對象是否 instanceOf Type 或者 Id 的類型 |
target(Type or Id) | Join Point 所在的對象(例如 call 或 execution 操作符應(yīng)用的對象)是否 instanceOf Type 或者 Id 的類型 |
args(Type or Id, …) | 方法或構(gòu)造函數(shù)參數(shù)的類型 |
if(BooleanExpression) | 滿足表達(dá)式的 Join Point,表達(dá)式只能使用靜態(tài)屬性菩暗、Pointcuts 或 Advice 暴露的參數(shù)掰曾、thisJoinPoint 對象 |
Pointcut 表達(dá)式還可以 !停团、&&旷坦、|| 來組合,!Pointcut 選取不符合 Pointcut 的 Join Point佑稠,Pointcut0 && Pointcut1 選取符合 Pointcut0 和 Pointcut1 的 Join Point秒梅,Pointcut0 || Pointcut1 選取符合 Pointcut0 或 Pointcut1 的 Join Point。
上面 Pointcuts 的語法中涉及到一些 Pattern舌胶,下面是這些 Pattern 的規(guī)則捆蜀,[]
里的內(nèi)容是可選的:
Pattern | 規(guī)則 |
---|---|
MethodPattern | [!][@Annotation] [public,protected,private][static] [final] 返回值類型 [類名.]方法名(參數(shù)類型列表) [throws 異常類型] |
ConstructorPattern | [!][@Annotation] [public,protected,private][final] [類名.]new(參數(shù)類型列表) [throws 異常類型] |
FieldPattern | [!][@Annotation] [public,protected,private][static] [final] 屬性類型 [類名.]屬性名 |
TypePattern | 其他 Pattern 涉及到的類型規(guī)則也是一樣,可以使用 ‘!’幔嫂、’‘辆它、’..’、’+’履恩,’!’ 表示取反锰茉,’‘ 匹配除 . 外的所有字符串,’*’ 單獨(dú)使用事表示匹配任意類型切心,’..’ 匹配任意字符串飒筑,’..’ 單獨(dú)使用時表示匹配任意長度任意類型片吊,’+’ 匹配其自身及子類,還有一個 ‘…’表示不定個數(shù) |
TypePattern 也可以使用 &&协屡、|| 操作符定鸟,其他 Pointcut 更詳細(xì)的語法說明,見官網(wǎng)文檔 Pointcuts Language Semantics著瓶。
Pointcut 示例
execution(void void android.view.View.OnClickListener+.onClick(..)) – OnClickListener 及其子類的 onClick 方法執(zhí)行時
call(@retrofit2.http.GET public com.johnny.core.http..(..)) – ‘com.johnny.core.http’開頭的包下面的所有 GET 方法調(diào)用時
call(android.support.v4.app.Fragment+.new(..)) – support 包中的 Fragment 及其子類的構(gòu)造函數(shù)調(diào)用時
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) – 執(zhí)行 Fragment 及其子類的 setUserVisibleHint(boolean) 方法時
execution(void Foo.foo(..)) && cflowbelow(execution(void Foo.foo(..))) – 執(zhí)行 Foo.foo() 方法中再遞歸執(zhí)行 Foo.foo() 時
Pointcut 聲明
Pointcuts 可以在普通的 class 或 Aspect class 中定義,由 org.aspectj.lang.annotation.Pointcut 注解修飾的方法聲明啼县,方法返回值只能是 void材原。@Pointcut 修飾的方法只能由空的方法實(shí)現(xiàn)而且不能有 throws 語句,方法的參數(shù)和 pointcut 中的參數(shù)相對應(yīng)季眷。
看下面這個例子:
@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() 表達(dá)式
在基于 AspectJ 注解的開發(fā)方式中余蟹,if(...)
表達(dá)式的用法與其他的選擇操作符不同,在 @Pointcut 的語句中 if 表達(dá)式只能是if()
子刮、if(true)
或if(false)
威酒,而且 @Pointcut 方法必須為 public static boolean,方法體內(nèi)就是 if 表達(dá)式的內(nèi)容挺峡,可以使用暴露的參數(shù)葵孤、靜態(tài)屬性、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() 表達(dá)式使用的比較少狭姨,大致了解下就可以了宰啦。
target() 與 this()
target() 與 this() 很容易混淆,target() 是指 Pointcut 選取的 Join Point 的所有者饼拍;this() 是指 Pointcut 選取的 Join Point 的調(diào)用的所有者赡模。簡單地說就是,PointcutA 選取的是 methodA师抄,那么 target 就是 methodA() 這個方法的對象漓柑,而 this 就是 methodA 被調(diào)用時所在類的對象。
看下面這個例子:
class Test {
public void test() {...}
}
class A {
...
test1.test(); // test() 在 a 的某方法中調(diào)用
...
}
@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 對象的方法中調(diào)用,所以 this 為 a挤安。
Advice
Advice 是在切入點(diǎn)上織入的代碼谚殊,在 AspectJ 中有五種類型:Before、After蛤铜、AfterReturning嫩絮、AfterThrowing丛肢、Around。
Advice | 說明 |
---|---|
@Before | 在執(zhí)行 Join Point 之前 |
@After | 在執(zhí)行 Join Point 之后剿干,包括正常的 return 和 throw 異常 |
@AfterReturning | Join Point 為方法調(diào)用且正常 return 時蜂怎,不指定返回類型時匹配所有類型 |
@AfterThrowing | Join Point 為方法調(diào)用且拋出異常時,不指定異常類型時匹配所有類型 |
@Around | 替代 Join Point 的代碼置尔,如果要執(zhí)行原來代碼的話杠步,要使用 ProceedingJoinPoint.proceed() |
注意: After 和 Before 沒有返回值,但是 Around 的目標(biāo)是替代原 Join Point 的榜轿,所以它一般會有返回值幽歼,而且返回值的類型需要匹配被選中的 Join Point 的代碼。而且不能和其他 Advice 一起使用谬盐,如果在對一個 Pointcut 聲明 Around 之后還聲明 Before 或者 After 則會失效甸私。
Advice 注解修改的方法必須為 public,Before飞傀、After皇型、AfterReturning高帖、AfterThrowing 四種類型修飾的方法返回值也必須為 void诱篷,Advice 需要使用 JoinPoint、JoinPointStaticPart胀溺、JoinPoint.EnclosingStaticPart 時幢痘,要在方法中聲明為額外的參數(shù)寡键,@Around 方法可以使用 ProceedingJoinPoint,用以調(diào)用 proceed() 方法雪隧。
看下面幾個示例西轩,進(jìn)一步了解 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}); // 原來參數(shù)乘以 2
}
注:Handler Pointcut 不能使用 After 和 Around。
Aspect
Aspect 就是 AOP 中的關(guān)鍵單位 – 切面脑沿,我們一般會把相關(guān) Pointcut 和 Advice 放在一個 Aspect 類中藕畔,在基于 AspectJ 注解開發(fā)方式中只需要在類的頭部加上 @Aspect 注解即可,@Aspect 不能修飾接口。
例如庄拇,定義一個 LogAspect注服,在需要的 Join Point 上加上打印日志的 Advice,這樣就形成了一個 LogAspect 的切面措近,在編譯期會將代碼織入到相應(yīng)的方法中溶弟,但是在編碼中只需要關(guān)注 LogAspect 即可。
在多個切入點(diǎn)織入 Advice 代碼時瞭郑,會涉及到 Aspect 對象實(shí)例的問題辜御,因?yàn)?Advice 代碼是 Aspect 的方法。一般情況下屈张,我們使用的都是單例的 Aspect擒权,即所有 Advice 代碼使用的都是同一個 Aspect 對象實(shí)例袱巨。
Singleton Aspect
文章中代碼示例都是單例的 Aspect,這也是最常見的碳抄,定義方式為:@Aspect
或者 @Aspect()
愉老。
編譯期,ajc 編譯期會給單例的切面加上靜態(tài)的 aspectOf() 方法來獲取單例實(shí)例剖效,還有一個 hasAspect() 靜態(tài)方法判斷實(shí)例是否初始化嫉入。假設(shè) FragmentAspect 有 Advice 方法 advice1(),織入切入點(diǎn)的代碼就是 FragmentAspect.aspectOf().advice1()璧尸。
Per-object, Per-cflow Aspect 等
除了單例 Aspect 外咒林,還可以根據(jù) Join Point 的相應(yīng)對象、控制流逗宁、所在類型產(chǎn)生不同的實(shí)例。
定義方式為:@Aspect("perthis|pertarget|percflow|percflowbelow(Pointcut) | pertypewithin(TypePattern)")
梦湘,因?yàn)椴怀R娤箍牛跃秃唵谓榻B下,想進(jìn)一步了解請看 Aspects Language Semantics 捌议。
Inter-type Declarations
上面提到的都是 Pointcut 和 Advice 都是在類本身結(jié)構(gòu)不變的情況下織入代碼哼拔,AspectJ 的 Inter-type Declarations 可以修改類的結(jié)構(gòu),給類添加方法或者屬性瓣颅,讓類繼承多個類或者實(shí)現(xiàn)多個接口倦逐。但是基于 AspectJ 注解開發(fā)方式因?yàn)榧夹g(shù)原因,目前只能讓類實(shí)現(xiàn)多個接口宫补,通俗的說法就是給類添加接口檬姥,也添加了接口的方法。
給類添加接口粉怕,實(shí)際通過實(shí)現(xiàn)了該接口的代理來完成對原類型的替換健民,所以需要提供實(shí)現(xiàn)了該接口的實(shí)現(xiàn)完成代理中接口的具體行為,不然只是增加接口贫贝,沒有接口實(shí)現(xiàn)沒什么用處秉犹。@DeclareMixin 就是用來確定接口的默認(rèn)實(shí)現(xiàn),綁定一個產(chǎn)生該接口的默認(rèn)實(shí)現(xiàn)的工廠方法稚晚,以該接口為返回類型崇堵。
看下面代碼,給 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 接口,之后通過正常的類型轉(zhuǎn)換來訪問 Title 接口:
String title = ((Title) fragment).getTitle(); // 返回 Test 字符串
也可以將原對象作為接口默認(rèn)實(shí)現(xiàn)的參數(shù)也搓,這樣就可以根據(jù) 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棍辕。