AOP入門介紹

概念

AOP(Aspect Oriented Programming)簡而言之就是面向切面編程。它所要實現的目標就是解耦,提供代碼的靈活性和可擴展性。

與OOP的區別

OOP(Object Oriented Programming)面對對象編程。
這其實是兩種不同的設計思想:

  • OOP是把同一對象的屬性放到一個對象里,把同一類別的類放到一個模塊里;這里同一的評判標準可以按照業務來劃分,也可以通過行為來劃分
  • AOP則是提取相同的功能和方法,針對這樣的橫切面來歸類。

用圖來彌補下言語的匱乏:

OOP

AOP

AOP的應用領域

在Android開發過程中,我們時長遇到需要統計事件,性能監測,權限檢查等需求。而這些需求是獨立業務開發之外的。在業務開發過程中,開發人員不想被這些需求打擾而中斷了業務邏輯的梳理。這個時候,就可以用到AOP的思想來解決問題。

  • 打印日志:獨立的日志模塊,在業務開發后期嵌入到各個業務模塊中,在代碼里不存在日志相關代碼。
  • 性能監測:時長需要對生命周期函數,view繪制函數監測其運行時長來監測性能。但如果在代碼開發階段考慮就要,實現每個生命周期函數,在繪制view的函數中加入時間統計代碼,這樣不僅會導致冗余代碼還會影響方法本身的性能。
  • 權限檢查:使方法功能單一,剝離權限檢查部分。

其實,總結起來,最終的目的就是為了解耦,盡量將業務無關的,且同一類方法,功能中需要做的重復動作,提取出來。

AspectJ

AOP的實現有很多中,AspectJ只是其中一種,在Java中用得比較多。AspectJ可以說是一種語言,它完全兼容Java,使用原生的Java來開發的話,只需要加上AspectJ的注解就可以。因此兩種方式:

  • 通過AspectJ的關鍵字來實現
  • 原生java+AspectJ的相關注解來開發

但是無論是通過何種方式實現,其編譯都必須要通過AspectJ的編譯工具ajc來編譯。

AspectJ的語法

在介紹AspectJ的語法之前,先介紹幾個概念,也是AspectJ中的關鍵字,了解他們的含義,對于開發至關重要。

  • aspect(切面) 針對切面的模塊。也就說獨立于業務,需要被插入的部分。
  • joinpoint(連接點) 顧名思義就是連接切面模塊和業務模塊的地方;也可以理解為就是業務模塊中需要被嵌入代碼的地方。
  • pointcut 這個理解起來跟joinpoint應該是一個意思,只不過它可以添加一些附加條件
  • advice(處理邏輯) 說邏輯處理有點牽強,它表示的意思應該是被插入的代碼,以及插入的時機,如:Before,After,Around等。

常用的切入點

切入點一般是通過joinpoint和advice的組合來實現的,常用的可以看下表:

joinpoint advice 切入點
execution before 方法執行之前,切入點在方法內
execution after 方法執行之后,切入點在方法內
execution around 方法執行前后,可以替換原方法,切入點在方法內
call before 方法調用之前,切入點在方法外
call after 方法調用之后,切入點在方法外
call around 方法調用前后,可以替換原方法,切入點在方法外

PS:以上是常用的一些切入點,還有通過cflow來切入每一行字節碼。這個控制較難,控制不好會產生StackOverFlow,這個以后再說。
PSS: Advice的各個類型是可以組合使用的,但是切記Around與After是不可以同時使用的,會發生重復調用的問題。

JoinPoint的匹配規則

通過call 和 execution 我們可以知道切入點的時機是在方法調用還是在方法執行。但是如何才能找到方法呢,這就需要一定的匹配規則去找到需要切入的方法。

舉個例子:
cn.test.fwl.Test.main() 這樣的表達式,可以指定到包名為<cn.test.fwl>,類名為<Test>中無參數的main方法;那么如果我們需要匹配到這個類里所有的main方法,又或者我們需要匹配到這個包里所有類的main方法,再或者我們需要匹配到包含main字符的方法該如何來寫表達式呢?

通配符
AspectJ中提供了一些通配符來方便我們找到滿足規則的方法。

通配符 含義
* 匹配除了[.]之外的所有字符,用在路徑中表示任意包名字符串,用在類名中標識任意類名字符串,方法中表示任意方法名字符串
.. 表示任意的子package,或者任意的參數
+ 表示子類

舉例:

  • java.*.Date : 可以表示java.sql.Date,也可以表示java.util.Date
  • Test* : 表示以Test開頭的任意字符串
  • java..* : 表示java包中的任意類
  • java..*Model+ : 表示java包中以Model結尾的類的子類
  • test(..) : 表示方法名為test,任意的參數,沒有參數,有一個,兩個都可以匹配
  • test(int,char) : 表示方法名為test,有且僅有兩個參數,類型為int,char
  • test(String,..) : 表示方法名為test,至少有一個參數,第一個類型為String,其他任意
  • test(String ...) : 表示方法名為test,參數個數不定,但必須都是String類型,這里的[...] 不是通配符,而是java中不定參數的意思。

JoinPoint的約束

除了上面的匹配規則外,AspectJ還提供了一些其他方法來更加精確的選擇JoinPoint,比如某個類中的JoinPoint或者某個函數執行流程中的JoinPoint。

關鍵詞 說明 舉個栗子
within(pattern) pattern可以通過通配符表示,代表某個包或者類 滿足pattern適配的JoinpPoint。比如說within(Test)就標識在Test類中(包括內部類)所有的JoinPoint。
withinCode(Constructor Signature/Method Signature) 表示某個構造函數或其他函數執行過程中涉及到的 JoinPoint 比如:withinCode(* Test.testMethod(..))表示testMethod涉及的JoinPoint; withinCode(*.Test.new(..))表示Test構造函數涉及的JoinPoint
cflow(pointcuts) cflow的條件是一個pointcut,表示某個流程中涉及的JoinPoint 比如cflow(call Test.testMethod):表示調用Test.testMethod函數時所包含的JoinPoint,包括testMethod的call這個JoinPoint本身
cflowbelow(pointcuts) 比如:cflowbelow(call Test.testMethod):表示調用Test.testMethod函數時所包含的JoinPont,不包含testMethod的call這個JoinPont本身
this(Type) JoinPoint的this對象是Type類型。包括其子類 JPoint所在的這個類的類型是Type標示的類型或是其子類,則和它相關的JPoint將全部被選中。比如:Animal中的Move方法,則Bird,cat中的Move方法都會被選中
target(Type) JoinPoint的target對象是Type類型 target一般用在call的情況。call一個函數,這個函數可能定義在其他類。比如Bird的move方法在調用時被選中,那么其他的Move的方法則不會。
args(Type) 用來對JoinPoint的參數進行條件約束 比如args(int,..),表示第一個參數是int,后面參數個數和類型不限

Advice的注意點

關于Advice前面已經說過了,他其實就是被嵌入的部分,而嵌入的時機,也在切入點的表格里提到過。這里主要講下注意點:

  • After:表示函數執行或者調用完成后運行被嵌入的代碼部分。但是函數可能執行結束可能有兩種退出方式:一個正常的Return,或者拋出異常,因此After也做了區分: after():return(type)
    after():throwing(Throwable)
  • Around: 除了之前說Around和After不能同時使用之外,Around因為是可以替代原函數執行的,因此,要特別注意被嵌入的代碼的返回值一定要和原來的方法一致。

環境配置

Eclipse
Android現在很少有用Eclipse開發的了,但是Eclipse的插件卻是對AspectJ開發支持最友好的。基礎的AOP實例,打算用Eclipse來開發AspectJ對Java的橫切,因此,這里也介紹下,Eclipse的搭建。
Help -> Install New Software

Eclipse插件安裝

然后一直下一步就好

Android Studio
首先在工程目錄中導入相關的編譯工具:

buuildscript{
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.1'
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

然后在Aspect的module的build.gradle中添加依賴庫:

compile 'org.aspectj:aspectjrt:1.8.9'

添加aspect編譯的腳本:

def variants = android.libraryVariants
variants.all{ variant ->
    LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
    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
            )
        ]

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

        def log = project.logger
        for(IMessage msg: handler.getMessages(null,true)){
            switch(msg.getKind()){
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error msg.message, msg.thrown
                    break;
                case IMessage.WARNING:
                    log.warn msg.message, msg.thrown
                    break;
                case IMessage.INFO:
                    log.info msg.message,msg.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug msg.message,msg.thrown
                    break;
            }
        }
    }
}

最后在app的module里添加對aspect module的依賴,同時添加上對aspectJ編譯的腳本:

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 msg: handler.getMessages(null,true)){
            switch (msg.getKind()){
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error msg.message,msg.thrown
                    break;
                case IMessage.WARNING:
                    log.warn msg.message, msg.thrown
                    break;
                case IMessage.INFO:
                    log.info msg.message,msg.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug msg.message,msg.thrown
                    break;
            }
        }
    }
}

舉個栗子

先通過Eclipse上創建AJ的工程來熟悉下AspectJ的相關語法。
首先,創建一個AspectJ的工程,在已經完成AJDT的插件的前提下,在新建工程的時候,就可以看到可以創建AspectJ Project這樣的工程,如圖:

創建工程

之后創建兩個不同的包,來區分java文件和aj文件
創建Test1.java文件

public class Test1 {
    public static void main(String[] args) {
        test();
    }
    public static void test(){
        System.out.println("this is test method!");
    }
}

現在我們要在test()方法執行打印之前,插入我們的操作(這里也插入一句打印)
注意我們這里創建文件的時候,不再是java文件,而是.aj的文件
創建AspectJ.aj文件

public aspect AspectJ{
    public pointcut aspect1(): execution(* test(..));

    before():aspect1(){
        System.out.println("this is before test method: execution");
    }
}

完成這個文件之后,就會發現之前Test1.javatest()這個方法里上多了箭頭的標志。這就表明插入成功了。
可以運行看下結果:

運行結果

AspectJ.aj中的注入的打印已經被打印出來了。那么被注入之后的Test1.class是樣的:

class file

可以看到在打印System.out.println("this is test method!");之前被插入了一段代碼,而這段正是before():aspect1()方法中所執行的內容。


上面以executionbefore的組合舉了一個簡單的例子,主要是闡述了下如何創建Aspecj的工程,以及相應的文件。下面的例子會包含call,executionbefore,after的兩兩組合。
Test.java

public class Test1 {


    public static void main(String[] args) {
        testBeforeExecution();
        testBeforeCall();
        testAfterExecution();
        testAfterCall();
        testAfterReturn();
        testAfterThrowable();

    }


    public static void testBeforeExecution(){
        System.out.println("this is test before-execution!");
    }

    public static void testBeforeCall(){
        System.out.println("this is test before-call");
    }

    public static void testAfterExecution(){
        System.out.println("this is test after-execution");
    }

    public static void testAfterCall(){
        System.out.println("this is test after-call");
    }

    public static String testAfterReturn(){
        String a = "test parameter";
        System.out.println("this is test after-return");
        return a;
    }

    public static String testAfterThrowable(){
        String a = null;
        System.out.println("this is test after-throwable");
        a.equals("test");
        return a;
    }

}

AspectJ.aj

public aspect AspectJ{

    public pointcut aspect1(): execution(* testBeforeExecution(..));
    public pointcut aspect2(): call(* testBeforeCall(..));
    public pointcut aspect3(): execution(* testAfterExecution(..));
    public pointcut aspect4(): call(* testAfterCall(..));
    public pointcut aspect5(): execution(* testAfterReturn(..));
    public pointcut aspect6(): execution(* testAfterThrowable(..));



    before():aspect1(){
        System.out.println("this is before test : execution");
    }

    before():aspect2(){
        System.out.println("this is before test: call");
    }

    after():aspect3(){
        System.out.println("this is after test:execution");
    }

    after():aspect4(){
        System.out.println("this is after test: call");
    }

    after() returning(String s):aspect5(){
        System.out.println("this is after test : return->"+s);
    }

    after() throwing(Exception e):aspect6(){
        System.out.println("this is after test: throwable->"+e.getMessage());
    }
}

可以看到運行結果:

運行結果

同時也可以看到編譯后的class文件:

class file


接下來再舉個關于Around的用法的例子:
Test2.java

public class Test2 {

    public static void main(String[] args) {
        testAroundCall();
        testAroundExecution();
        testAroundReplace();
    
        System.out.println(testAroundRetrun());

    }


    public static void testAroundCall(){
        System.out.println("this is testAroundCall method");
    }

    public static void testAroundExecution(){
        System.out.println("this is testAroundExecution method");
    }

    public static void testAroundReplace(){
        System.out.println("this is testAroundReplace");
    }


    public static String testAroundRetrun(){
        String a = "the return value";
        System.out.println("this is  test around return");
    
        return a;
    
    }

}

AspectJ1.aj

public aspect AspectJ1{

    public pointcut test1():execution(* testAroundCall(..));
    public pointcut test2():call(* testAroundExecution(..));
    public pointcut test3():call(* testAroundReplace(..));
    public pointcut test4():call(* testAroundRetrun(..));


    void around():test1(){
        System.out.println("around-execution test before");
        proceed();
        System.out.println("around-execution test after");
    
    }

    void around():test2(){
        System.out.println("around-call test before");
        proceed();
        System.out.println("around-call test after");
    }

    void around():test3(){
        System.out.println("do replace ... ");
    
    }


    String around():test4(){
        String a = "string in aspect";
        System.out.println("replace return value");
        proceed();
        return a;
    }
}

運行結果如下:

運行結果

class 文件

class file


以上都是AspectJ語言寫的,那么如果使用純Java的方式該如何來實現呢,看下面的例子:
Aspectj2.java

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspectj2 {



    @Pointcut("execution(* testBeforeExecution(..))")
    public void test1(){
    
    }

    @Pointcut("call(* testBeforeCall(..))")
    public void test2(){
    
    }

    @Pointcut("execution(* testAfterExecution(..))")
    public void test3(){
    
    }

    @Pointcut("call(* testAfterCall(..))")
    public void test4(){
        
    }

    @Pointcut("execution(* testAfterReturn(..))")
    public void test5(){
    
    }

    @Pointcut("execution(* testAfterThrowable(..))")
    public void test6(){
    
    }



    @Before("test1()")
    public void execute1(){
        System.out.println("before-execution aspectj");
    }

    @Before("test2()")
    public void execute2(){
        System.out.println("before-call aspectj");
    }

    @After("test3()")
    public void execute3(){
        System.out.println("after-execution aspectj");
    }

    @After("test4()")
    public void execute4(){
        System.out.println("after-call aspectj");
    }

    @AfterReturning("test5()")
    public void execute5(){
        System.out.println("after-return aspectj");
    }

    @AfterThrowing("test6()")
    public void execute6(){
        System.out.println("after-throw aspectj");
    }

}

特別提醒下:類的注釋@Aspect千萬不能少,在這入坑了好幾次

運行結果如下:

運行結果

再看下編譯后的文件:

class file

栗子就先吃這么多~~~后面會再補一篇關于帶參數,返回值處理的栗子。

AspectJ在Android中的應用

后續會在github上傳一個關于權限檢查的庫,有時間也會寫個文檔介紹下這個庫。

總結

AOP的知識接觸得還不多,寫了些demo和Android的庫,總結下來,重點還是在JoinPoint的適配,如何才能精確得適配到自己想要的切入點,還需要將JoinPoint和Advice結合多加練習。
Eclipse上對的AJDT的插件對Aspect的語法還有錯誤檢查,但是Android Studio上還沒有,所以寫的時候,要特別仔細。

TODO

  • 帶參數,返回值的栗子
  • 權限檢查的工程和分析文檔
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容