1. 程式人生 > >Android Aspectj 從入門到實戰

Android Aspectj 從入門到實戰

文章目錄

AOP 簡介

Android Studio 想接入 AspectJ ? 看這篇就對了!從0到1 , 包會!

OOP ( Object Oriented Programming ) 面向物件程式設計思想
AOP ( Aspect Oriented Programming ) 面向切面程式設計思想


  • OOP : 面向物件即針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元模組劃分。也就是把各個獨立的功能封裝為個體或模組。

如下圖,每一個模組封裝了其特有的功能屬性,各盡其責,便於其它使用者的呼叫和複用。

在這裡插入圖片描述


  • AOP : 針對業務處理過程中特定的切面

如下圖,在特定的切面進行程式碼 「織入」(注意用詞,是織入不是hook…),新增共同邏輯( 日誌,埋點等) 且不會影響原有模組的業務功能和架構。
在這裡插入圖片描述


OOP 的精髓是把功能或問題模組化,每個模組處理自己的家務事。但在現實世界中,並不是所有問題都能完美得劃分到模組中。舉個最簡單而又常見的例子:現在想為每個模組加上日誌功能,要求模組執行時候能輸出日誌並統計當前方法耗時。

這個問題放在 OOP 思想中解決的辦法通常是設計日誌模組,並且在需要統計的地方一一手動加入日誌的 API,並且如果日誌的 API 改動,那將牽一髮而動全身。


這個時候 AOP 的用途便體現出來了。

AOP 主要用途有:日誌記錄,行為統計,安全控制,事務處理,異常處理,系統統一的認證、許可權管理等。可以使用 AOP 思想將這些程式碼從業務邏輯程式碼中劃分出來,通過對這些行為的分離,可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行為的時候不影響業務邏輯的程式碼。



Android AOP 實現原理

在這裡插入圖片描述
點選檢視大圖

在這裡插入圖片描述

上圖中 AspectJ 是 Android 實現 AOP 程式設計思想的具體工具

AspectJ 的使用核心就是它的編譯器,它就做了一件事,將 AspectJ 的程式碼在編譯期插入目標程式當中,執行時跟在其它地方沒什麼兩樣,因此要使用它最關鍵的就是使用它的編譯器去編譯程式碼 (AspectJ compile) 。
ajc 會構建目標程式與 AspectJ 程式碼的聯絡,在編譯期將 AspectJ 程式碼插入被切出的 PointCut 中,達到 AOP 的目的。

也就是在 .java 檔案編譯為.class檔案的時候,將.java 檔案做手腳,對相應切入點的程式碼進行功能程式碼「織入」。



Android AOP 基本實現方式

上面說道 : AOP是一個程式設計思想和概念,本身並沒有設定具體語言的實現,這實際上提供了非常廣闊的發展的空間。

AspectJ 是 AOP 的一個很悠久的實現,它能夠和 Java 配合起來使用。( 很穩 , 支付寶app第三方開源也有用到)
在這裡插入圖片描述

先來了解幾個 AspectJ 的 基本和主要的 關鍵詞 :

  • Aspect : Aspect 宣告類似於 Java 中的 類宣告 ,在Aspect中會包含著一些 Pointcut (切入點)以及相應的 Advice (通知) , Pointcut 和 Advice 的組合可以看做切面
  • Advice : ( 通知 ) , 定義了在 Pointcut 裡面定義的程式點具體要做的操作,它通過 before、after 和 around 來區別是在每個JoinPoint 之前、之後還是完全替代目標方法 的程式碼。 --> when

  • Pointcut : ( 切入點 ) , 告訴程式碼注入工具,在何處注入一段特定程式碼的表示式。 --> where
    下面是 Pointcut 篩選和匹配條件.(為了更精確的找到要切入地方.)
篩選條件 說明 示例
within(TypePattern) 篩選執行的包名路徑 within(com.sample.aop.*),在aop包名內的JPoint.
withincode(Method) 篩選執行的方法. withinCode(* A.aopMethod(…)),在A類的aopMethod涉及的JPoint
target(類全限定名) target一般用在call的情況,匹配任意標註了的目標類(指明攔截的方法屬於那個類) target(A)就會搜尋到由A類呼叫testMethod的地方
this(類全限定名) 與target雷同,區分點在於:this指方法是在哪個類中被呼叫的 B類中呼叫A.testMethod,指定的類為B
args() 對入參進行條件匹配 args(int,…),表示第一個引數是int,後面引數個數和型別不限
其它高階用法

  • JoinPoint : ( 連線點) , 表示在程式中明確定義的點,例如,典型的 方法函式呼叫類成員的訪問 以及對 異常處理程式塊 的執行等等,這些都是 JPoint 。(如Log.e()這個函式 , e()可以看作是個 JPoint ,而且呼叫e()的函式也可以認為是一個 JPoint ) , 也就是所有可以注入程式碼的地方。

可以說如果 AspectJ 規定中沒有這樣的 JPoint,那麼我們是無法利用AspectJ 來實現功能需求.

織入時機 說明 示例
call 函式呼叫 比如呼叫Log.e() , 這是一處JPoint
execution 函式調執行 execution是某個函式執行的內部

例如 A 類中,呼叫 Pointcut.Method() ,
call 擷取的是 在A類中呼叫該處函式的地方.
execution 擷取的則是 Pointcut 內 Method() 執行的方法…

Call(Before)
Pointcut{
    Pointcut Method
}
Call(After)
Pointcut{
  execution(Before)
    Pointcut Method
  execution(After)
}


Demo

實體類 , get/setName方法
下面這個例子通過 AOP 修改getName()返回引數,在呼叫setName()方法加上列印日誌

public class AopDemo {
    public static class innerB {
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}

宣告使用 Aspect 的類, 加上@Aspect 註解即可,類裡定義了切入點和通知,即組成了切面.

@Pointcut(" call(* getName() ) ")

  • call 表示方法函式呼叫即要擷取的地方
  • 第一個位置表示方法呼叫的返回值,*表示返回值為任意型別
  • getName表示方法名,也可以使用該方法的全限定名,getName()的括號()代表這個方法的引數,可以指定型別,或者(…)(int,…)這樣來代表任意型別和個數的引數。
  • 同時在call()後面可以 加入&&、||、!來進行條件組合,匹配或過濾關鍵 JPoint
@Aspect
public class DemoAspect {
    @Pointcut("call(* setName(String))")
    public void demo2() {}

    @Around("demo2()")
    public Object arounddemo1(ProceedingJoinPoint joinPoint) throws Throwable {
        Object target = joinPoint.getTarget();

        Object proceed = joinPoint.proceed();
        if (target instanceof AopDemo.innerB) {
            Log.e("log", "call setName");
//            ((AopDemo.innerB) target).setName("haha"); // 可以在方法執行之後搞點事情.
        }
        // joinPoint.proceed()代表執行原始的方法,在這之前之後都可以進行各種邏輯處理。
        return proceed;
    }


    @Pointcut("call(* getName())")
    public void demo1() {}

    @Around("demo1()")
    public String arounddemo1() {
        return "hoho";
    }
}

方法調用出 , 例如在MainActivity裡呼叫實體類的get/setName方法

// MainActivity裡呼叫方法
           AopDemo.innerB innerB = new AopDemo.innerB();
           innerB.setName("ok");
           Toast.makeText(this, "str==" + innerB.getName(), Toast.LENGTH_SHORT).show();

innerB.setName(“ok”); 程式碼執行後便會加上 Log.e(“log”, “call setName”);
innerB.getName(); 返回的不是"ok",而是我們程式碼織入的"hoho"


開啟當前MainActivity的.class檔案,看看 AspectJ 到底做了什麼騷操作
// MainActivity.class AspectJ 程式碼織入前
            innerB innerB = new innerB();
            innerB.setName("ok");
            Toast.makeText(this, "str==" + innerB.getName(), 0).show();
// MainActivity.class AspectJ 程式碼織入後
            innerB innerB = new innerB();
            String var7 = "ok";
            JoinPoint var9 = Factory.makeJP(ajc$tjp_0, this, innerB, var7);
            var10000 = DemoAspect.aspectOf();
            Object[] var10 = new Object[]{this, innerB, var7, var9};
            var10000.arounddemo1((new MainActivity$AjcClosure1(var10)).linkClosureAndJoinPoint(4112));
            
            Toast.makeText(this, "str==" + DemoAspect.aspectOf().arounddemo1(), 0).show();

如果將 Pointcut 的 call 方法改為 execution , 修改的則是 innerB.class 檔案.

也就是說 AspectJ 實現 AOP 程式設計思想的方法就是在 .java 檔案編譯為 .class 期間,使用 ajc (AspectJ compile) 編譯器 , 將需要 織入 的程式碼插到特定的 Pointcut 中

以上便是 AspectJ 基本使用方式,要挑戰高階用法,前往 AspectJ 開發手冊.



自定義PointCut

例如我們要在程式碼中進行許可權檢查,如果沒有許可權則不執行方法(或者執行許可權呼叫方法,許可權申請成功後再執行目標方法)

  • 1.建立自定義註解行為
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomPointCut {
    String[] permissionName();
}
  • 2.方法呼叫,在MainActivity呼叫註解方法
    @CustomPointCut(permissionName = {"PHONE", "STATUS"})
    public void customMethod() {
        Toast.makeText(this, "customMethod call", Toast.LENGTH_SHORT).show();
    }
  • 3.宣告使用 Aspect 的類
@Aspect
public class DemoAspect {
	// 具體使用的時候,CustomPointCut 要改為具體的全限定名.
    @Pointcut("execution(@com.xxx.CustomPointCut * *..*.*(..))")
    public void customMethod() {
    }
    
    @Around("customMethod()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 類名
        String className = methodSignature.getDeclaringType().getSimpleName();
        // 方法名
        String methodName = methodSignature.getName();
        // 功能名
        CustomPointCut behaviorTrace = methodSignature.getMethod().getAnnotation(CustomPointCut.class);
        String[] value = behaviorTrace.permissionName();
        // value -- > phone,status

        long start = System.currentTimeMillis();
        
        // 也可以不執行joinPoint.proceed(),根據業務需求沒有許可權/登入不呼叫目標方法
        Object result = joinPoint.proceed();// result 為目標方法呼叫後的返回值
        
        long duration = System.currentTimeMillis() - start;//可以統計方法耗時.

        return result;//返回值,可以任性修改.
    }
}

這樣只要程式碼中加入 @CustomPointCut 註解,便可以統一處理許可權操作. 登入判斷也可以用此方法來統一處理。程式碼精簡,一步到位.

下面看一下 AspectJ 在編譯時期做了哪些處理

// MainActivity.class
    @CustomPointCut(
        permissionName = {"PHONE", "STATUS"}
    )
    public void customMethod() {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        DemoAspect var10000 = DemoAspect.aspectOf();
        Object[] var2 = new Object[]{this, var1};
        var10000.aroundMethod((new MainActivity$AjcClosure1(var2)).linkClosureAndJoinPoint(69648));
    }



Android AspectJ 接入實戰



1.新增aop模組,配置依賴

建議新建一個aop相關模組 , 方便 aop 作為Demo 專案的除錯,後期可以輕鬆依賴進自己的工程專案。


首先建立一個 lib-aop 模組,作為 library 方式引入到 Demo 專案中
關鍵有以下兩點 :

  1. 依賴 ajc 編譯指令碼,我們將指令碼內容寫 aspectj-configure-lib.gradle 檔案中,aop 模組引用該指令碼
  2. 新增 aspectjrt 依賴,新增的方式有兩種,如果是多人協作建議下載 jar 包配置到本地依賴.

aspectj 相關jar包 : maven倉庫地址.
示例專案中所用 jar 包 : aspectjrt-1.8.13.jar. ( 放置 lib-aop 模組內 libs 資料夾下)

// aop 模組內 build.gradle
apply plugin: 'com.android.library'
apply from: '../lib-aop/aspectj-configure-lib.gradle' // ajc 編譯所需gradle指令碼

android {
    compileSdkVersion 27
    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    
}

dependencies {
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support:design:27.1.1'
    implementation 'com.android.support:support-v4:27.1.1'

    //依賴方式 1. 使用本地jar包
    api fileTree(include: ['*.jar'], dir: 'libs') // 作用範圍一定得是 api !!!

    //依賴方式 2. 配置 maven 地址
    // api 'org.aspectj:aspectjrt:1.8.13'
}

  • 踩坑1 :aspectjrt 依賴的作用範圍一定得是 api ,否則其它模組死活織入不了程式碼


2.配置 ajc 指令碼

建議 在 lib-aop 模組 內新建 aspectj-configure-lib.gradle 檔案,指令碼內容為以下程式碼

// aspectj-configure-lib.gradle
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.13'
        classpath 'org.aspectj:aspectjweaver:1.8.13'
    }
}

repositories {
    mavenCentral()
}


android.libraryVariants.all { variant ->
    if (variant.buildType.isDebuggable()) {
//        return;   //開放後debug模式不生效
    }
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", android.bootClasspath.join(
                File.pathSeparator)]

        MessageHandler handler = new MessageHandler(true)
        new Main().run(args, handler)

        def log = project.logger
        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:
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break
            }
        }
    }
}
  • 踩坑2 :aspectj-configure-lib.gradle 只適用於lib 模組!
    在 aspectj-configure-lib.gradle 的第 21行中配置到 : android.libraryVariants.all,這隻適用於 型別為 library 的 module 享用
    application 的 module 只需要將這行配置改為 android.applicationVariants.all,即可.


3.配置 app 的指令碼

在 Demo 工程中的 app 目錄下,配置 build.gradle 指令碼檔案

//  build.gradle 
apply plugin: 'com.android.application'

// ajc 編譯所需gradle指令碼,application適用
apply from: '../lib-aop/aspectj-configure-app.gradle'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.demo.aop"
        minSdkVersion 17
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'

    implementation project(":lib-aop")// 依賴 aop 模組,這裡對作用範圍沒有限制.
}


上文說到 aspectj-configure-app.gradle 與 aspectj-configure-lib.gradle 的不同點只在於 第22行,

// aspectj-configure-app.gradle
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.13'
        classpath 'org.aspectj:aspectjweaver:1.8.13'
    }
}

repositories {
    mavenCentral()
}



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

variants.all { variant ->
    if (variant.buildType.isDebuggable()) {
//        return;   //開放後debug模式不生效
    }
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-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
            }
        }
    }
}



4.細節提醒

如此一來,aspectj 便配置完成了,另外,下面幾點細節提醒 :

  • lib-aop 模組內需配置 proguard-rules.pro 混淆相關,將 lib-aop 內相關程式碼的包名新增進配置中,防止混淆.
-dontwarn com.xxx.aop.**
-keep class com.xxx.aop.**{*;}

  • 其它模組如果需要在編譯時期使用 aspectj 程式碼織入功能,需要加入gradle指令碼 ajc 配置
   app		 : apply from : '../lib-aop/aspectj-configure-app.gradle'
   library	 : apply from : '../lib-aop/aspectj-configure-lib.gradle'

並且依賴 lib-aop 模組(如果有公用base模組,作用範圍可以用 api ,其它模組就可以不用再次新增依賴了。)

implementation project(":lib-aop")// 依賴 aop 模組

  • 踩坑3 :lib-aop 不能打成 aar !
    編寫 aspect 相關的模組不能打成 aar ,需要模組專案引用,否則編譯打包後會找不到類java.lang.NoClassDefFoundError: Failed resolution of: xxx/xxx/具體類

  • 踩坑4 :J神的 hugo 外掛 debug 時期 ajc 編譯無效 , 因為在 ajc 編譯指令碼 gradle 中,如果是 Debuggable , return … ( 這一點估計坑了許多人. )
    if (variant.buildType.isDebuggable()) {
	       return;
    }

  • 踩坑5 :AS編譯時期 IOException ,或檔案被佔用.
java.lang.RuntimeException: java.io.IOException: Failed to delete C:\Users\..\build\intermediates\intermediate-jars\debug\classes.jar

關閉工作管理員中 java.exe 程序 ,再次編譯即可.



參考

先理清概念 : Android AOP面向切面程式設計AspectJ.

再深入瞭解 阿拉神農的 :深入理解Android之AOP. 大多數部落格都參考該篇文章

最後android配置 aspectj : AndroidStudio 配置 AspectJ 環境實現AOP.