AspectJ in Android 系列:
AspectJ in Android (一),AspectJ 基礎(chǔ)概念
AspectJ in Android (二)肥惭,AspectJ 語(yǔ)法
AspectJ in Android (三)么伯,AspectJ 兩種用法以及常見問題
上篇文章介紹了 AspectJ 的基本概念疟暖,這篇文章詳細(xì)分析 AspectJ 基于注解開發(fā)方式的語(yǔ)法。
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 相對(duì)應(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 對(duì)應(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 對(duì)象是否 instanceOf Type 或者 Id 的類型 |
target(Type or Id) | Join Point 所在的對(duì)象(例如 call 或 execution 操作符應(yīng)用的對(duì)象)是否 instanceOf Type 或者 Id 的類型 |
args(Type or Id, ...) | 方法或構(gòu)造函數(shù)參數(shù)的類型 |
if(BooleanExpression) | 滿足表達(dá)式的 Join Point准验,表達(dá)式只能使用靜態(tài)屬性、Pointcuts 或 Advice 暴露的參數(shù)廷没、thisJoinPoint 對(duì)象 |
Pointcut 表達(dá)式還可以 糊饱!、&&颠黎、|| 來組合另锋,!Pointcut 選取不符合 Pointcut 的 Join Point,Pointcut0 && Pointcut1 選取符合 Pointcut0 和 Pointcut1 的 Join Point狭归,Pointcut0 || Pointcut1 選取符合 Pointcut0 或 Pointcut1 的 Join Point夭坪。
上面 Pointcuts 的語(yǔ)法中涉及到一些 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í)表示匹配任意長(zhǎng)度任意類型勾哩,'+' 匹配其自身及子類股耽,還有一個(gè) '...'表示不定個(gè)數(shù) |
TypePattern 也可以使用 &&、|| 操作符钳幅,其他 Pointcut 更詳細(xì)的語(yǔ)法說明物蝙,見官網(wǎng)文檔 Pointcuts Language Semantics。
Pointcut 示例
execution(void void android.view.View.OnClickListener+.onClick(..)) -- OnClickListener 及其子類的 onClick 方法執(zhí)行時(shí)
call(@retrofit2.http.GET public * com.johnny.core.http..*(..)) -- 'com.johnny.core.http'開頭的包下面的所有 GET 方法調(diào)用時(shí)
call(android.support.v4.app.Fragment+.new(..)) -- support 包中的 Fragment 及其子類的構(gòu)造函數(shù)調(diào)用時(shí)
set(@Inject * *) -- 寫入所有 @Inject 注解修飾的屬性時(shí)
handler(IOException) && within(com.johnny.core.http..) -- 'com.johnny.core.http'開頭的包代碼中處理 IOException 時(shí)
execution(void setUserVisibleHint(..)) && target(android.support.v4.app.Fragment) && args(boolean) -- 執(zhí)行 Fragment 及其子類的 setUserVisibleHint(boolean) 方法時(shí)
execution(void Foo.foo(..)) && cflowbelow(execution(void Foo.foo(..))) -- 執(zhí)行 Foo.foo() 方法中再遞歸執(zhí)行 Foo.foo() 時(shí)
Pointcut 聲明
Pointcuts 可以在普通的 class 或 Aspect class 中定義敢艰,由 org.aspectj.lang.annotation.Pointcut 注解修飾的方法聲明诬乞,方法返回值只能是 void。@Pointcut 修飾的方法只能由空的方法實(shí)現(xiàn)而且不能有 throws 語(yǔ)句钠导,方法的參數(shù)和 pointcut 中的參數(shù)相對(duì)應(yīng)震嫉。
看下面這個(gè)例子:
@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 的語(yǔ)句中 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)用的所有者。簡(jiǎn)單地說就是提前,PointcutA 選取的是 methodA吗货,那么 target 就是 methodA() 這個(gè)方法的對(duì)象,而 this 就是 methodA 被調(diào)用時(shí)所在類的對(duì)象岖研。
看下面這個(gè)例子:
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 對(duì)象卿操,所以 target 為 test1警检,而該方法在 a 對(duì)象的方法中調(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 時(shí)鸽凶,不指定返回類型時(shí)匹配所有類型 |
@AfterThrowing | Join Point 為方法調(diào)用且拋出異常時(shí)币砂,不指定異常類型時(shí)匹配所有類型 |
@Around | 替代 Join Point 的代碼,如果要執(zhí)行原來代碼的話玻侥,要使用 ProceedingJoinPoint.proceed() |
注意: After 和 Before 沒有返回值决摧,但是 Around 的目標(biāo)是替代原 Join Point 的,所以它一般會(huì)有返回值凑兰,而且返回值的類型需要匹配被選中的 Join Point 的代碼掌桩。而且不能和其他 Advice 一起使用,如果在對(duì)一個(gè) Pointcut 聲明 Around 之后還聲明 Before 或者 After 則會(huì)失效姑食。
Advice 注解修改的方法必須為 public波岛,Before、After音半、AfterReturning则拷、AfterThrowing 四種類型修飾的方法返回值也必須為 void,Advice 需要使用 JoinPoint曹鸠、JoinPointStaticPart隔躲、JoinPoint.EnclosingStaticPart 時(shí),要在方法中聲明為額外的參數(shù)物延,@Around 方法可以使用 ProceedingJoinPoint宣旱,用以調(diào)用 proceed() 方法。
看下面幾個(gè)示例叛薯,進(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)鍵單位 -- 切面,我們一般會(huì)把相關(guān) Pointcut 和 Advice 放在一個(gè) Aspect 類中耗溜,在基于 AspectJ 注解開發(fā)方式中只需要在類的頭部加上 @Aspect 注解即可,@Aspect 不能修飾接口组力。
例如,定義一個(gè) LogAspect抖拴,在需要的 Join Point 上加上打印日志的 Advice燎字,這樣就形成了一個(gè) LogAspect 的切面腥椒,在編譯期會(huì)將代碼織入到相應(yīng)的方法中,但是在編碼中只需要關(guān)注 LogAspect 即可候衍。
在多個(gè)切入點(diǎn)織入 Advice 代碼時(shí)笼蛛,會(huì)涉及到 Aspect 對(duì)象實(shí)例的問題,因?yàn)?Advice 代碼是 Aspect 的方法蛉鹿。一般情況下滨砍,我們使用的都是單例的 Aspect,即所有 Advice 代碼使用的都是同一個(gè) Aspect 對(duì)象實(shí)例妖异。
Singleton Aspect
文章中代碼示例都是單例的 Aspect惋戏,這也是最常見的,定義方式為:@Aspect
或者 @Aspect()
他膳。
編譯期响逢,ajc 編譯期會(huì)給單例的切面加上靜態(tài)的 aspectOf() 方法來獲取單例實(shí)例,還有一個(gè) hasAspect() 靜態(tài)方法判斷實(shí)例是否初始化棕孙。假設(shè) FragmentAspect 有 Advice 方法 advice1()舔亭,織入切入點(diǎn)的代碼就是 FragmentAspect.aspectOf().advice1()。
Per-object, Per-cflow Aspect 等
除了單例 Aspect 外散罕,還可以根據(jù) Join Point 的相應(yīng)對(duì)象分歇、控制流、所在類型產(chǎn)生不同的實(shí)例欧漱。
定義方式為:@Aspect("perthis|pertarget|percflow|percflowbelow(Pointcut) | pertypewithin(TypePattern)")
职抡,因?yàn)椴怀R姡跃秃?jiǎn)單介紹下误甚,想進(jìn)一步了解請(qǐng)看 Aspects Language Semantics 缚甩。
Inter-type Declarations
上面提到的都是 Pointcut 和 Advice 都是在類本身結(jié)構(gòu)不變的情況下織入代碼,AspectJ 的 Inter-type Declarations 可以修改類的結(jié)構(gòu)窑邦,給類添加方法或者屬性擅威,讓類繼承多個(gè)類或者實(shí)現(xiàn)多個(gè)接口。但是基于 AspectJ 注解開發(fā)方式因?yàn)榧夹g(shù)原因冈钦,目前只能讓類實(shí)現(xiàn)多個(gè)接口郊丛,通俗的說法就是給類添加接口,也添加了接口的方法瞧筛。
給類添加接口厉熟,實(shí)際通過實(shí)現(xiàn)了該接口的代理來完成對(duì)原類型的替換,所以需要提供實(shí)現(xiàn)了該接口的實(shí)現(xiàn)完成代理中接口的具體行為较幌,不然只是增加接口揍瑟,沒有接口實(shí)現(xiàn)沒什么用處。@DeclareMixin 就是用來確定接口的默認(rèn)實(shí)現(xiàn)乍炉,綁定一個(gè)產(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 字符串
也可以將原對(duì)象作為接口默認(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纯续。
關(guān)于 AspectJ 基于注解開發(fā)方式的語(yǔ)法就講到這里随珠,下一篇文章根據(jù)實(shí)際例子介紹 AspectJ 的常見用法灭袁。
參考資料