AspectJ 在APM上的應用(二)

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。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容