AOP 之 AspectJ 全面剖析 in Android

AspectJ 是 Android 平臺上一種比較高效和簡單的實現(xiàn) AOP 技術的方案。

相類似的方案有以下幾種:

  • AspectJ: 一個 JavaTM 語言的面向切面編程的無縫擴展(適用Android)。

  • Javassist for Android :用于字節(jié)碼操作的知名 java 類庫 Javassist 的 Android 平臺移植版。

  • DexMaker :Dalvik 虛擬機上,在編譯期或者運行時生成代碼的 Java API。

  • ASMDEX :一個類似 ASM 的字節(jié)碼操作庫,運行在Android平臺,操作Dex字節(jié)碼。

AOP 是什么?

在軟件業(yè),AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術。AOP是OOP的延續(xù),是軟件開發(fā)中的一個熱點,也是Spring框架中的一個重要內容,是函數(shù)式編程的一種衍生范型。利用AOP可以對業(yè)務邏輯的各個部分進行隔離,從而使得業(yè)務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發(fā)的效率。

------以上解釋來自百度百科。

簡單的來講,AOP是一種:可以在不改變原來代碼的基礎上,通過“動態(tài)注入”代碼,來改變原來執(zhí)行結果的技術。

AOP 能做什么?

  • 日志
  • 持久化
  • 性能監(jiān)控
  • 數(shù)據(jù)校驗
  • 緩存
  • 其他更多

AspectJ 術語

  • JPoint:代碼可注入的點,比如一個方法的調用處或者方法內部、“讀、寫”變量等。
  • Pointcut:用來描述 JPoint 注入點的一段表達式,比如:調用 Animal 類 fly 方法的地方,call(* Animal.fly(..))。
  • Advice:常見的有 Before、After、Around 等,表示代碼執(zhí)行前、執(zhí)行后、替換目標代碼,也就是在 Pointcut 何處注入代碼。
  • Aspect:Pointcut 和 Advice 合在一起稱作 Aspect。

引入 AspectJ

app/build.grade加入以下配置項:

...
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.1'
    }
}

repositories {
    mavenCentral()
}

android {
    ...
}

dependencies {
    ...
    compile 'org.aspectj:aspectjrt:1.8.1'
}

final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                     "-1.5",
                     "-inpath", javaCompile.destinationDir.toString(),
                     "-aspectpath", javaCompile.classpath.asPath,
                     "-d", javaCompile.destinationDir.toString(),
                     "-classpath", javaCompile.classpath.asPath,
                     "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
           switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

AspectJ 語法

AspectJ 的使用相對來說還是有點復雜,所以我整理了一份參考手冊來方便查閱,以下是貼圖,為了方便使用,文章最后會給出 pdf 版的下載地址。

JPoint 的分類和對應的 Pointcut 如下:


1-1

Pointcut 中的 Signature 參考:

1-2

以上的 Signature 都是由一段表達式組成,且每個關鍵詞之間都有“空格”,下面是對關鍵詞的解釋:

1-3

Pointcut 語法熟悉了之后,Advice 就顯得很簡單了,它包含以下幾個:

1-4
高級用法

有了以上參考手冊,我們來看看如何使用吧~

開始使用

一、Method -> call

定義一個 Animal 類,包含一個 fly 方法:

public class Animal {
    private static final String TAG = "Animal";
    public void fly() {
        Log.e(TAG, this.toString() + "#fly");
    }
}

在調用 fly 的地方,之前插入一段代碼:

@Aspect
public class MethodAspect {
    private static final String TAG = "ConstructorAspect";

    @Pointcut("call(* android.aspectjdemo.animal.Animal.fly(..))")
    public void callMethod() {}

    @Before("callMethod()")
    public void beforeMethodCall(JoinPoint joinPoint) {
        Log.e(TAG, "before->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
    }
}

使用 @Pointcut 來注解方法,定義具體的 Pointcut ,call(MethodSignature) 關鍵字表示方法被調用,MethodSignature 的定義參考 1-2、1-3表格。

MethodSignature

調用 fly 之前插入一段代碼,所以 Advice 需要使用 @Before,@Before 的參數(shù)就是 使用@Pointcut 注解的方法名稱。

當然,你也可以合并 Pointcut 和 Advice,像這樣:

@Aspect
public class MethodAspect {
    private static final String TAG = "MethodAspect";

    @Before("call(* android.aspectjdemo.animal.Animal.fly(..))")
    public void beforeMethodCall(JoinPoint joinPoint) {
        Log.e(TAG, "before->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
    }
}

最后,就是在 MethodAspect 加上 @Aspect 注解,這樣 AspectJ 在編譯時會查找被 @Aspect 注解的 class,然后 AOP 的過程會自動完成。

編譯運行之后,可以在 app/build/intermediates/classes/debug 目錄查看編譯后的 class 文件:

...
Animal animal = new Animal();
JoinPoint var5 = Factory.makeJP(ajc$tjp_0, this, animal);
MethodAspect.aspectOf().beforeMethodCall(var5);
animal.fly();
...

animal.fly() 之前插入了一段代碼,調用就是 MethodAspect#beforeMethodCall 方法,查看 logcat 輸出:

beforeMethodCall

如果使用 @After 類型的 Advice,則會在animal.fly() 之后插入:

...
Animal animal = new Animal();
Animal var6 = animal;
JoinPoint var5 = Factory.makeJP(ajc$tjp_0, this, animal);

try {
    var6.fly();
} catch (Throwable var9) {
    MethodAspect.aspectOf().afterMethodCall(var5);
    throw var9;
}

MethodAspect.aspectOf().afterMethodCall(var5);
...

我們來看一下 @Around 類型的 Advice,它會替換原先的執(zhí)行代碼:

/**
 * 不能和Before、After一起使用
 * @param joinPoint
 * @throws Throwable
 */
@Around("call(* android.aspectjdemo.animal.Animal.fly(..))")
public void aroundMethodCall(ProceedingJoinPoint joinPoint) throws Throwable {
    Log.e(TAG, "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());

    // 執(zhí)行原代碼
    joinPoint.proceed();
}

@Around 不能和 @Before、@After 一起使用,如果不小心這樣用了,你會發(fā)現(xiàn)沒有任何效果。

@Around 會替換原先執(zhí)行的代碼,但如果你仍然希望執(zhí)行原先的代碼,可以使用joinPoint.proceed()

二、Method -> execution

與 call 類似,只不過執(zhí)行點(JPoint)在方法內部,比如:我們希望在 fly 方法的Log.e(TAG, this.toString() + "#fly")這段代碼執(zhí)行前,插入一段代碼:

@Aspect
public class MethodAspect {
    private static final String TAG = "MethodAspect";

    @Before("execution(* android.aspectjdemo.animal.Animal.fly(..))")
    public void beforeMethodExecution(JoinPoint joinPoint) {
        Log.e(TAG, "before->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
   }
}

execution 關鍵字表示方法執(zhí)行內部,編譯后 class 文件如下:

public void fly() {
    JoinPoint var1 = Factory.makeJP(ajc$tjp_1, this, this);
    MethodAspect.aspectOf().beforeMethodExecution(var1);
    Log.e("Animal", this.toString() + "#fly");
}

logcat 輸出如下:

beforeMethodExecution

@After 和 @Around 這里就不做演示了,和 Method -> call 中講解的類似,只不過是在 fly 方法內部。

三、Constructor -> call & execution

Constructor 和 Method 幾乎一模一樣,最大的區(qū)別就在 Signature,如下表:


Constructor & Method -> Signature

Constructor 沒有返回值類型,且函數(shù)名只能是 new,一個示例如下:

@Aspect
public class ConstructorAspect {
    private static final String TAG = "ConstructorAspect";
    
    @Before("execution(android.aspectjdemo.animal.Animal.new(..))")
    public void beforeConstructorExecution(JoinPoint joinPoint) {
        Log.e(TAG, "before->" + joinPoint.getThis().toString() + "#" + joinPoint.getSignature().getName());
    }
}

四、Field -> get

語法概要:


FieldSignature

Animal 包含年齡age屬性、返回agegetAge方法:

public class Animal {
    private static final String TAG = "Animal";
    private int age;

    public Animal() {
        this.age = 10;
    }

    public int getAge() {
        Log.e(TAG, "getAge: ");
        return this.age;
    }
}

比如,我們希望不管怎么修改age的值,最后獲取的age都為100,那么就需要替換訪問age的代碼:

@Aspect
public class FieldAspect {
    private static final String TAG = "FieldAspect";

    @Around("get(int android.aspectjdemo.animal.Animal.age)")
    public int aroundFieldGet(ProceedingJoinPoint joinPoint) throws Throwable {
       // 執(zhí)行原代碼
       Object obj = joinPoint.proceed();
       int age = Integer.parseInt(obj.toString());
       Log.e(TAG, "age: " + age);
       return 100;
   }
}

這樣,animal.getAge()返回的值一定是100:

Animal animal = new Animal();
int age = animal.getAge();
Log.e(TAG, "true age: " + age);

logcat 輸出如下:

aroundFieldGet

同時看下編譯后的class

public int getAge() {
    Log.e(TAG, "getAge: ");
    JoinPoint var2 = Factory.makeJP(ajc$tjp_2, this, this);
    FieldAspect var10000 = FieldAspect.aspectOf();
    Object[] var3 = new Object[]{this, this, var2};
    return var10000.aroundFieldGet((new Animal$AjcClosure3(var3)).linkClosureAndJoinPoint(4112));
}

可以看到,原先的this.age已經被替換成調用FieldAspect#aroundFieldGet方法了。

五、Field -> set & withincode

與 get 對應的是 set:表示修改某個屬性,比如setAge方法中的this.age = age

public class Animal {
    private static final String TAG = "Animal";
    private int age;

    public Animal() {
        this.age = 10;
    }

    public void setAge(int age) {
        Log.e(TAG, "setAge: ");
        this.age = age;
    }
}

假如我們希望替換這段代碼,讓調用方無法改變age

@Aspect
public class FieldAspect {
    private static final String TAG = "FieldAspect";

    @Around("set(int android.aspectjdemo.animal.Animal.age)")
    public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.e(TAG, "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
    }
}

編譯后的class

public class Animal {
    private static final String TAG = "Animal";
    private int age;

    public Animal() {
        Log.e("Animal", "Animal構造函數(shù)");
        byte var1 = 10;
        JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, Conversions.intObject(var1));
        FieldAspect var10000 = FieldAspect.aspectOf();
        Object[] var4 = new Object[]{this, this, Conversions.intObject(var1), var3};
        var10000.aroundFieldSet((new Animal$AjcClosure1(var4)).linkClosureAndJoinPoint(4112));
    }

    public void setAge(int age) {
        Log.e("Animal", "setAge: ");
        JoinPoint var4 = Factory.makeJP(ajc$tjp_3, this, this, Conversions.intObject(age));
        FieldAspect var10000 = FieldAspect.aspectOf();
        Object[] var5 = new Object[]{this, this, Conversions.intObject(age), var4};
        var10000.aroundFieldSet((new Animal$AjcClosure5(var5)).linkClosureAndJoinPoint(4112));
    }
}

我們發(fā)現(xiàn),setAge方法中的this.age = age的確被替換了,但是原先構造函數(shù)初始化age的代碼:this.age = 10也被替換了。

這時候,就需要使用withincode關鍵字了。

六、withincode

語法概要:

withincode

比如,我們要排除 Animal 的構造函數(shù)修改age的 JPoint,可以這樣寫:

@Aspect
public class FieldAspect {
    private static final String TAG = "FieldAspect";

    @Around("set(int android.aspectjdemo.animal.Animal.age) && !withincode(android.aspectjdemo.animal..*.new(..))")
    public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.e(TAG, "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
    }
}

Pointcut 多個條件使用&&、||運算符連接,!表示否的意思。

再看一下編譯后的class,構造函數(shù)中的 JPoint 已經被排除:

public class Animal {
    private static final String TAG = "Animal";
    private int age;

    public Animal() {
        Log.e("Animal", "Animal構造函數(shù)");
        this.age = 10;
    }

    public void setAge(int age) {
        Log.e("Animal", "setAge: ");
        JoinPoint var4 = Factory.makeJP(ajc$tjp_3, this, this, Conversions.intObject(age));
        FieldAspect var10000 = FieldAspect.aspectOf();
        Object[] var5 = new Object[]{this, this, Conversions.intObject(age), var4};
        var10000.aroundFieldSet((new Animal$AjcClosure5(var5)).linkClosureAndJoinPoint(4112));
    }
}

七、staticinitialization

語法概要:

staticinitialization

TypeSignature 語法:


TypeSignature

JPoint 為static塊初始化內:

public class Animal {
    private static final String TAG = "Animal";

    static {
        Log.e(TAG, "static block");
    }
}

如果要在static塊初始化之前,插入代碼:

@Aspect
public class StaticInitializationAspect {
    private static final String TAG = "StaticAspect";

    @Before("staticinitialization(android.aspectjdemo.animal.Animal)")
    public void beforeStaticBlock(JoinPoint joinPoint) {
        Log.d(TAG, "beforeStaticBlock: ");
    }
}

編譯后的class

public class Animal {
    private static final String TAG = "Animal";

    static {
        ajc$preClinit();
        JoinPoint var0 = Factory.makeJP(ajc$tjp_3, (Object)null, (Object)null);
        StaticInitializationAspect.aspectOf().beforeStaticBlock(var0);
        Log.e("Animal", "static block");
    }
}

八、handler

用來匹配 catch 的異常,比如 Animal 的 hurt 方法:

public class Animal {
    public void hurt(){
        try {
            int i = 4 / 0;
        } catch (ArithmeticException e) {
            e.printStackTrace();
        }
    }
}

如果我們需要統(tǒng)計所有出現(xiàn)ArithmeticException的點,則可以使用 handler:

@Aspect
public class MethodAspect {
    private static final String TAG = "MethodAspect";
    /**
     * handler
     * 不支持@After、@Around
     */
    @Before("handler(java.lang.ArithmeticException)")
    public void handler() {
        Log.e(TAG, "handler");
    }
}

注意 handler 不支持 @After 與 @Around,且異常只支持編譯時匹配,也就是handler(java.lang.Exception)無法匹配java.lang.ArithmeticException,雖然ArithmeticException繼承自Exception。

九、Advice 之 @AfterThrowing

@AfterThrowing 屬于 @After 的變種,方法的結束包括兩種狀態(tài):正常結束異常退出。

我們經常需要收集拋出異常的方法信息,這時候可以使用 @AfterThrowing。

比如 Animal 的hurtThrows會拋出java.lang.ArithmeticException異常:

public class Animal {
    public void hurtThrows(){
        int i = 4 / 0;
    }
}

我們可以這樣收集異常:

@Aspect
public class MethodAspect {
    private static final String TAG = "MethodAspect";

    @AfterThrowing(pointcut = "call(* *..*(..))", throwing = "throwable")
    public void anyFuncThrows(Throwable throwable) {
        Log.e(TAG, "hurtThrows: ", throwable);
    }
}

call(* *..*(..))表示任意類的任意方法,被調用的 JPoint。

throwing = "throwable"描述了異常參數(shù)的名稱,也就是anyFuncThrows方法中的參數(shù)throwable。

你可以通過 Throwable 收集方法調用棧的信息,這里就不做過多講解了。

這里,需要強調幾點:
1、@AfterThrowing 不支持 Field -> get & set,一般用在 Method 和 Constructor,其他暫時沒測試過;
2、捕獲的是拋出異常的方法,即使這個方法的調用方已經處理了此異常,比如:

try {
    animal.hurtThrows();
} catch (Exception e) {}

即使這樣,MethodAspect#anyFuncThrows也會被觸發(fā)。

接下來,我們看下方法正常結束的情況。

十、Advice 之 @AfterReturning & args

這里講的正常結束,指的是有返回值的方法。

假如 Animal 有兩個getHeight方法:

public class Animal {
    public int getHeight() {
        return 0;
    }

    public int getHeight(int sex) {
        switch (sex) {
            case 0:
                return 163;
            case 1:
                return 173;
        }
        return 173;
    }
}

我們想要拿到 getHeight 的返回值,做一些其他事情(比如,數(shù)據(jù)統(tǒng)計、緩存等),可以這樣做:

@Aspect
public class MethodAspect {
    private static final String TAG = "MethodAspect";

    @AfterReturning(pointcut = "execution(* android.aspectjdemo.animal.Animal.getHeight(..))", returning = "height")
    public void getHeight(int height) {
        Log.d(TAG, "getHeight: " + height);
    }
}

如果你調用animal.getHeight(),此方法會得到0;
如果你調用animal.getHeight(0),此方法會得到163。

如果你只對getHeight(int sex)感興趣,有兩種做法:
1、Pointcut 中表示任意參數(shù)的 .. 改為 int

@AfterReturning(pointcut = "execution(* android.aspectjdemo.animal.Animal.getHeight(int))", returning = "height")

2、 && args(int)

@AfterReturning(pointcut = "execution(* android.aspectjdemo.animal.Animal.getHeight(..)) && args(int)", returning = "height")
args

舉個高級栗子

這里介紹一個在實際項目中,經常碰到的一個問題:Android M 6.0+ 之后危險權限需要動態(tài)申請。

如果有一些老的項目需要適配,一般做法是去修改原有的代碼,比如我們有一個啟動相機拍照的方法:

public void camera() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(getExternalCacheDir() + "photo.jpg")));
    startActivity(intent);
}

可能需要這么改:

Utils.requestPermisson(this, Manifest.permission.CAMERA).callback(new Callback(){
    public void onGranted(){
        camera();
    }
    public void onDenied() {}
});

如果你封裝了請求權限工具類,這樣改看起來也沒什么問題,無非就是把所有類似的地方都加上這個段申請權限的代碼。

如果沒有封裝,只能是更痛苦。如果使用 AspectJ,可以通過一行注解,解決所有需要需要申請權限的方法。

一、定義注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MPermisson {
    String value();
}

value表示要申請的權限名稱,比如Manifest.permission.CAMERA。

二、編寫 Aspect

@Aspect
public class PermissonAspect {
    @Around("execution(@android.aspectjdemo.MPermisson * *(..)) && @annotation(permisson)")
    public void checkPermisson(final ProceedingJoinPoint joinPoint, MPermisson permisson) throws Throwable {
        // 權限
        String permissonStr = permisson.value();
        // 正常需要使用維護的棧頂Activity作為上下文,這里為了演示需要
        MainActivity mainActivity = (MainActivity) joinPoint.getThis();          // 權限申請

        Utils.requestPermisson(mainActivity, Manifest.permission.CAMERA).callback(new Callback(){
            public void onGranted(){
                try {
                    // 繼續(xù)執(zhí)行原方法
                    joinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }
            public void onDenied() {}
      });
   }
}

@annotation(permisson)用來表示permisson參數(shù)是注解類型。

三、使用 @MPermisson

在需要申請權限的方法上加上@MPermisson注解,其它代碼不用做修改:

@MPermisson(value = Manifest.permission.CAMERA)
public void camera() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(getExternalCacheDir() + "photo.jpg")));
    startActivity(intent);
}

如果你項目中有其他地方,也需要申請權限,只需要在涉及到權限的方法上加上@MPermisson(value = "你的權限")即可。

寫在最后

以上介紹的內容,基本上已經涵蓋了 AspectJ 所有常用的方法。

剩余的幾個用法,比如adviceexecution()、within(TypePattern)、cflow(pointcuts)、cflowbelow(pointcuts)、this(Type)、target(Type)等,讀者可以自行嘗試以下,篇幅原因不做細談。

或者參考文章最后給出的 Demo 源碼,這個 Demo 包含了本文演示的所有示例。
Demo 地址:AspectJDemo
AspectJ 參考手冊:AspectJ.pdf


春節(jié)將至,希望已經回家的、沒回家的和正在回家路上的你都能過一個開開心心的好年。

最后,預祝大家在新的一年里:身體健康,龍馬精神,事業(yè)有成,步步高升

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容