Android中AOP實踐之三AspectJ解析篇

介紹

AspectJ是Java的一個簡單實用的面向方面的擴展。通過幾個新的構造,AspectJ提供了對一系列橫切關注的模塊化實現(xiàn)的支持。

在現(xiàn)有的Java開發(fā)項目中采用AspectJ可能是一個簡單而且增量的任務。一條路徑是從開發(fā)方面開始,繼續(xù)使用生產(chǎn)方面,然后在使用AspectJ建立經(jīng)驗之后再使用方面。采用也可以遵循其他途徑。例如,一些開發(fā)人員將從馬上使用生產(chǎn)方面受益。其他人可能幾乎可以立即編寫干凈的可重用方面。

AspectJ支持基于名稱和基于屬性的橫切。使用基于名稱的橫切的方面傾向于影響少數(shù)其他類。但是,盡管規(guī)模較小,但與普通的Java實現(xiàn)相比,它們通常可以消除顯著的復雜性。使用基于屬性的橫切的方面可以具有小規(guī)模或大規(guī)模。

使用AspectJ會導致橫切關注的干凈模塊化的實現(xiàn)。當作為AspectJ方面編寫時,橫切關注的結構是明確的且易于理解的。方面也是高度模塊化的,使得開發(fā)橫切功能的即插即用實現(xiàn)成為可能。

基礎知識

Join point 連接點

Join Points 定義 解釋
method call 調用方法 一般在執(zhí)行某個方法前會先調用該方法
method execution 執(zhí)行方法 這個點是已經(jīng)執(zhí)行到了方法的內(nèi)部
constructor call 調用構造方法 同調用方法
constructor execution 執(zhí)行構造方法 同執(zhí)行方法
field get 獲取參數(shù) 比如獲取某個變量的值,get()
field set 設置參數(shù) 比如設置某個變量的值,int num = 3
pre-initialization 預初始化 在第一次初始化前會預初始化
initialization 初始化 初始化類的時候會執(zhí)行
static initialization 靜態(tài)初始化 靜態(tài)塊或靜態(tài)類初始化的時候會執(zhí)行
handler 異常處理
advice execution 通知執(zhí)行

是指程序中可能作為代碼注入目標的特定的點,例如一個方法調用或者方法入口。
程序中連接點有很多,下面做一個表格一一指出:

Join Points 定義 解釋
method call 調用方法 一般在執(zhí)行某個方法前會先調用該方法
method execution 執(zhí)行方法 這個點是已經(jīng)執(zhí)行到了方法的內(nèi)部
constructor call 調用構造方法 同調用方法
constructor execution 執(zhí)行構造方法 同執(zhí)行方法
field get 獲取參數(shù) 比如獲取某個變量的值,get()
field set 設置參數(shù) 比如設置某個變量的值,int num = 3
pre-initialization 預初始化 在第一次初始化前會預初始化
initialization 初始化 初始化類的時候會執(zhí)行
static initialization 靜態(tài)初始化 靜態(tài)塊或靜態(tài)類初始化的時候會執(zhí)行
handler 異常處理
advice execution 通知執(zhí)行

//這里可以用一個例子來演示一下所有的連接點

Pointcut 切入點

一個程序中會有很多JPoint連接點,但不一定我們都要去關注。那么我們可以選擇我們需要的點來作為切入點。

我們利用Pointcut的功能來篩選出對我們有用的點作為切入,pointcut有一套專門的語法,只要搞懂他后面就不愁了。

一個例子

    @Pointcut("within(@com.jie.aoptest.aop.DebugLog *)")
    public void withinAnnotatedClass() {}

    @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
    public void methodInsideAnnotatedType() {}

    @Pointcut("execution(@com.jie.aoptest.aop.DebugLog * *(..)) || methodInsideAnnotatedType()")
    public void method() {}

這個例子表明切入點在DebugLog類中所有執(zhí)行方法的點,這里用到了within和execution兩個指示符,within用于匹配指定類型內(nèi)的方法執(zhí)行,而exection則用于匹配方法執(zhí)行的連接點。有synthetic標記的field和method是class內(nèi)部使用的,正常的源代碼里不會出現(xiàn)synthetic field。

切入點指示符

指示符 說明
execution 用于匹配方法執(zhí)行的連接點
within 用于匹配指定類型內(nèi)的方法執(zhí)行
this: 用于匹配當前AOP代理對象類型的執(zhí)行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
target 用于匹配當前目標對象類型的執(zhí)行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配
args 用于匹配當前執(zhí)行的方法傳入的參數(shù)為指定類型的執(zhí)行方法
@within 用于匹配所以持有指定注解類型內(nèi)的方法
@target 用于匹配當前目標對象類型的執(zhí)行方法,其中目標對象持有指定的注解
@args 用于匹配當前執(zhí)行的方法傳入的參數(shù)持有指定注解的執(zhí)行
@annotation 用于匹配當前執(zhí)行方法持有指定注解的方法

常用指示符

指示符 說明
execution 用于匹配方法執(zhí)行的連接點
within 用于匹配指定類型內(nèi)的方法執(zhí)行
this: 用于匹配當前AOP代理對象類型的執(zhí)行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
target 用于匹配當前目標對象類型的執(zhí)行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配
args 用于匹配當前執(zhí)行的方法傳入的參數(shù)為指定類型的執(zhí)行方法
@within 用于匹配所以持有指定注解類型內(nèi)的方法
@target 用于匹配當前目標對象類型的執(zhí)行方法,其中目標對象持有指定的注解
@args 用于匹配當前執(zhí)行的方法傳入的參數(shù)持有指定注解的執(zhí)行
@annotation 用于匹配當前執(zhí)行方法持有指定注解的方法

AspectJ切入點支持的切入點指示符還有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode,感興趣的可以了解,就不一一說明了。

類型匹配語法

先來看一下AspectJ類型匹配的通配符

  1. *:匹配任何數(shù)量字符;
  2. ..:匹配任何數(shù)量字符的重復,如在類型模式中匹配任何數(shù)量子包;而在方法參數(shù)模式中匹配任何數(shù)量參數(shù)。
  3. +:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。

接下來看一下具體匹配表達式類型

  1. 注解:可選,方法上持有的注解,如@Deprecated;
  2. 修飾符:可選,如public、protected;
  3. 返回值類型:必填,可以是任何類型模式;“*”表示所有類型;
  4. 類型聲明:可選,可以是任何類型模式;
  5. 方法名:必填,可以使用“*”進行模式匹配;
  6. 參數(shù)列表:“()”表示方法沒有任何參數(shù);“(..)”表示匹配接受任意個參數(shù)的方法,“(..,java.lang.String)”表示匹配接受java.lang.String類型的參數(shù)結束,且其前邊可以接受有任意個參數(shù)的方法;“(java.lang.String,..)” 表示匹配接受java.lang.String類型的參數(shù)開始,且其后邊可以接受任意個參數(shù)的方法;“(*,java.lang.String)” 表示匹配接受java.lang.String類型的參數(shù)結束,且其前邊接受有一個任意類型參數(shù)的方法;
  7. 異常列表:可選,以“throws 異常全限定名列表”聲明,異常全限定名列表如有多個以“,”分割,如throws java.lang.IllegalArgumentException, java.lang.ArrayIndexOutOfBoundsException。

組合切入點表達式

AspectJ使用 且(&&)、或(||)、非(!)來組合切入點表達式。

常用場景舉例

Advice通知參數(shù)

前面已經(jīng)介紹了Join point 連接點和 Pointcut 切入點,如果基本掌握了的話那么恭喜你內(nèi)功已經(jīng)修煉7成了。

我們成功設置好切入點后需要獲取通知來執(zhí)行要切入的代碼片段,這里的通知相當于鉤子/回調方法,在程序執(zhí)行到JPoint時候會調起通知,接下來就介紹一下獲取通知的方式。

Advice通知有三種類型

類型 說明
before() 是指在JPoint之前可以執(zhí)行一些操作
after() 是指在JPoint之后可以執(zhí)行一些操作
around() 環(huán)繞JPoint執(zhí)行操作,它包含了前后兩個過程,使用這種類型需要手動調用procees方法來執(zhí)行原操作

來看一個例子

我們來用檢測是否登錄做一個例子
這個是檢查登錄的注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface CheckLogin {
}

先用before和after兩個類型來做一個測試

    @Pointcut("execution(@com.jie.aoptest.aop.CheckLogin * *(..))")
    public void methodAnnotated() {
    }

    @Before("methodAnnotated()")
    public void beforeMethod(ProceedingJoinPoint joinPoint) {
        Log.d("aspect", "beforeMethod");
        Log.d("login", "請您登錄");
        Toast.makeText(App.getAppContext().getCurActivity(), "請您登錄", Toast.LENGTH_SHORT).show();
    }

    @After("methodAnnotated()")
    public void afterMethod(ProceedingJoinPoint joinPoint) {
        Log.d("aspect", "afterMethod");
    }

來看一下打印日志是這樣的

11-12 06:30:52.476 10388-10388/com.jie.aoptest D/aspect: beforeMethod
11-12 06:30:52.477 10388-10388/com.jie.aoptest D/login: 請您登錄
11-12 06:30:52.486 10388-10388/com.jie.aoptest D/aspect: afterMethod

然后我們用around做一個測試

    @Pointcut("execution(@com.jie.aoptest.aop.CheckLogin * *(..))")
    public void methodAnnotated() {
    }

    @Around("methodAnnotated()")
    public void aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.d("aspect", "aroundMethod");
        Log.d("login", "請您登錄");
        Toast.makeText(App.getAppContext().getCurActivity(), "請您登錄", Toast.LENGTH_SHORT).show();
        joinPoint.proceed();
        Log.d("aspect", "aroundMethod");
    }

打印日志是這樣的

11-12 06:35:09.696 10512-10512/com.jie.aoptest D/aspect: aroundMethod
11-12 06:35:09.696 10512-10512/com.jie.aoptest D/login: 請您登錄
11-12 06:35:09.700 10512-10512/com.jie.aoptest D/aspect: aroundMethod

兩種方式都可以,但要注意一點around和after兩種類型是有沖突的,around和before可以共存,所以還是建議兩種方式,一種before和after配合使用,一種around單獨使用。

參數(shù)的獲取

方法參數(shù)的獲取

方法參數(shù)的獲取很簡單,可以通過joinPoint.getArgs()來獲取參數(shù),舉個例子:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        safe("haha", 20, true);
        ...
    }
    
    @Safe
    private void safe(String a, int b, boolean c) {
        Log.d("aop", "獲取參數(shù)")
    }

通知方法

    @Around("execution(!synthetic * *(..)) && methodAnnotated()")
    public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
        for (Object arg : joinPoint.getArgs()) {
            Log.d("arg", arg.toString());
        }
        joinPoint.proceed(joinPoint.getArgs());
    }

日志打印

11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: haha
11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: 20
11-12 06:56:08.062 29915-29915/com.jie.aoptest D/arg: true

注解參數(shù)的獲取

直接上代碼例子
首先需要在注解上聲明參數(shù)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
    //聲明參數(shù)
    String declaredPermission();
}

然后看一下Activity中的調用方法,注意這里在注解后設置參數(shù)值

    @CheckPermission(declaredPermission="android.permission.READ_PHONE_STATE")
    private void checkPhoneState(){
        Log.d("CheckPermission","Read Phone State succeed");
    }

看一下切片類的寫法,注意這里在切點上要用@annotation來獲取注解對象,然后我們在aroundMethod方法中多了一個checkPermission對象,最后從這個對象中拿到注解參數(shù)

@Aspect
public class CheckPermissionAspect {

    @Pointcut("execution(@com.jie.aoptest.aop.CheckPermission * *(..)) && @annotation(checkPermission)")
    public void checkPermission(CheckPermission checkPermission){};

    @Around("checkPermission(checkPermission)")
    public void aroundMethod(JoinPoint joinPoint, CheckPermission checkPermission){
        //從注解信息中獲取聲明的權限。
        String neededPermission = checkPermission.declaredPermission();
        Log.d("CheckPermissionAspect", joinPoint.toShortString());
        Log.d("CheckPermissionAspect", "\tneeded permission is " + neededPermission);
    }
}

最后來看一下輸出日志,證明我們已經(jīng)成功拿到注解參數(shù)了

11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermissionAspect: execution(MainActivity.checkPhoneState())
11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermissionAspect:     needed permission is android.permission.READ_PHONE_STATE
11-12 08:00:21.203 24559-24559/com.jie.aoptest D/CheckPermission: Read Phone State succeed

總結

AspectJ解析基本就到這里了,掌握了它就可以全面的用AOP思想去解決問題了,核心還是在解決問題的思路。我也是在邊學習邊整理從而寫出這篇文檔,這里講解了一些AspectJ的基礎用法,高級用法大家可以從參考文獻的書中去慢慢探索。

參考文獻

  1. 深入理解Android之AOP 博主寫的細致入微,我也從中有所參考。
  2. Manning.AspectJ.in.Action第二版
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容