1. 程式人生 > >AspectJ教程--AOP面向切面程式設計框架(Android)

AspectJ教程--AOP面向切面程式設計框架(Android)

AOP的概念很久前就接觸過了,不過沒有真正寫過專案,甚至Demo都沒有,今天把這點缺陷補上。
推薦兩篇文章(本文部分圖片引自這兩篇文章):
1. 【翻譯】Android中的AOP程式設計
2. 【深入理解Android之AOP】

1. 本篇文章總覽

這裡寫圖片描述

2. 什麼是AOP

2.1 定義

AOP是Aspect Oriented Program的首字母縮寫,譯為:面向切面程式設計。類似的OOP,譯為:面向物件程式設計。

  • OOP:面向物件思想簡單理解就是,需要把各功能封裝為獨立模組,然後把他們簡單拼裝成為產品。Android系統的各個模組封裝就遵循OOP(下圖)。
    這裡寫圖片描述
  • AOP:在這些獨立的模組間,在特定的切入點進行hook,將共同的邏輯新增到模組中而不影響原有模組的獨立性。下圖,在不同的模組中加入日誌、快取、效能檢測功能,並不影響原有的架構。
    這裡寫圖片描述

2.2 相關術語

術語名稱 術語解釋
Cross-cutting concerns(橫切關注點) 多個模組可能新增相同附屬功能的點
Advice(通知) 注入到class檔案中的程式碼。典型的 Advice 型別有 before、after 和 around,分別表示在目標方法執行之前、執行後和完全替代目標方法執行的程式碼。 除了在方法中注入程式碼,也可能會對程式碼做其他修改,比如在一個class中增加欄位或者介面。
join Point(連線點) 所有可以注入程式碼的地方
PointCut(切入點) 告訴AOP框架,我應該在哪個join point注入一段程式碼
Aspect(切面) 由PointCut和Advice組成的公共邏輯成為切面,切面邏輯只需開發一次,多處呼叫
Weaving(織入) 注入程式碼到目標位置

2.3 AOP使用場景

  • 日誌相關
  • 持久化操作
  • 效能監控
  • 資料校驗
  • 快取
  • 等…

3 OOP和AOP實現具體需求

統計三個模組耗時。

3.1 OOP實現轉向AOP實現圖例

這裡寫圖片描述

3.2 OOP實現

3.2.1 編寫登入模組

簡寫程式碼如下:

/**
 * OOP 登入模組
 * Created by Administrator on 2017/10/13.
 */

public class LoginUtils {
    private static final String TAG = "OOP";

    public static boolean Login(String userName, String passWord){
        long start=System.currentTimeMillis();
        long end;
        StringBuffer stringBuffer = new StringBuffer();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if ("張三".equals(userName) && "123456".equals(passWord)){
            end = System.currentTimeMillis();
            stringBuffer.append("登入成功,耗時:")
            .append(end - start);
            Log.e(TAG,stringBuffer.toString());
            return true;
        }else{
            end = System.currentTimeMillis();
            stringBuffer.append("登入失敗,耗時:")
                    .append(end - start);
            Log.e(TAG,stringBuffer.toString());
            return false;
        }
    }
}

3.2.2 編寫檔案上傳模組

簡寫程式碼如下

/**
 * OOP 檔案上傳模組
 * Created by Administrator on 2017/10/13.
 */

public class UploadFileUtils {
    private static final String TAG = "OOP";

    public static boolean upload(String url, String path){
        long start=System.currentTimeMillis();
        long end;
        StringBuffer stringBuffer1 = new StringBuffer();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("從本地上傳")
                .append(path)
                .append("到")
                .append(url);
        Log.e(TAG,stringBuffer.toString());

        end = System.currentTimeMillis();
        stringBuffer1.append("檔案上傳成功,耗時:")
                .append(end - start);
        Log.e(TAG,stringBuffer1.toString());

        return true;
    }
}

3.2.3 依次執行登入、轉賬、本地上傳

日誌如下:

10-12 13:28:17.300 14605-14605/com.aspectjdemo E/OOP: 登入成功,耗時:2000
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 從111112賬戶轉出100.0到222221
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 轉賬成功,耗時:2001
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 從本地上傳/sd/example.png到www.baidu.com
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 檔案上傳成功,耗時:2001

3.2.4 找出弊端

按照上面的實現方式,弊端有以下幾個:

  • 程式碼冗餘
  • 邏輯不清晰
  • 重構不方便

3.3 AOP實現

3.3.1 AspectJ實現AOP

 AspectJ是一個非侵入式的AOP框架,下一章專門介紹。此處只寫Android Studio的實現方式,Eclipse實現方式不太一樣。

3.3.1.1 第一步 配置AspectJ

在app的gradle檔案中新增如下程式碼,作用:使用ajc代替javac編譯java程式碼。具體說明見:【翻譯】Android中的AOP程式設計

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.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

apply plugin: 'com.android.application'

repositories {
    mavenCentral()
}

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.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;
            }
        }
    }
}
dependencies {
    compile 'org.aspectj:aspectjrt:1.8.11'
}
3.3.1.2 第二步 自定義註解
/**
 * 自定義註解
 * Created by Administrator on 2017/10/13.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeTrace {
    String value();
}
3.3.1.3 第三步 實現各模組,並加上@TimerTrace註解

拿登入模組來舉例,該模組中已經不包含耗時統計的邏輯。

/**
 * AOP 登入模組
 * Created by Administrator on 2017/10/13.
 */

public class AOPLoginUtils {
    private static final String TAG = "OOP";

    @TimeTrace(value = "登入")
    public static boolean Login(String userName, String passWord){

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if ("張三".equals(userName) && "123456".equals(passWord)){
            return true;
        }else{
            return false;
        }
    }
}
3.3.1.4 第四步 用@Aspect標註切面類
@Aspect
public class TimeTraceAspect {

}
3.3.1.5 第五步 在切面類中定義PointCut(切入點)
// 語法:execution(@註解 訪問許可權 返回值的型別 包名.函式名(引數))
// 表示:使用TimeTrace註解的任意型別返回值任意方法名(任意引數)
@Pointcut("execution(@com.aspectjdemo.aop.TimeTrace * *(..))")
public void myPointCut(){

}
3.3.1.6 第六步 在切面類中定義Advance(通知)

具體注入的程式碼

// Advance比較常用的有:Before():方法執行前,After():方法執行後,Around():代替原有邏輯
@Around("myPointCut()")
public Object dealPoint(ProceedingJoinPoint point) throws Throwable {
    // 方法執行前先記錄時間
    long start=System.currentTimeMillis();
    MethodSignature methodSignature = (MethodSignature) point.getSignature();
    // 獲取註解
    TimeTrace annotation = methodSignature.getMethod().getAnnotation(TimeTrace.class);
    String value = annotation.value();

    // 執行原方法體
    Object proceed = point.proceed();

    // 方法執行完成後,記錄時間,列印日誌
    long end = System.currentTimeMillis();
    StringBuffer stringBuffer = new StringBuffer();
    if (proceed instanceof Boolean){
        // 返回的是boolean
        if ((Boolean)proceed){
            stringBuffer.append(value)
                    .append("成功,耗時:")
                    .append(end - start);
        }else{
            stringBuffer.append(value)
                    .append("失敗,耗時:")
                    .append(end - start);
        }
    }
    Log.e(TAG,stringBuffer.toString());
    return proceed;
}
3.3.1.6 第六步 依次執行登入、轉賬、本地上傳
10-12 13:25:44.106 12332-12332/com.aspectjdemo E/AOP: 登入成功,耗時:2001
10-12 13:25:46.136 12332-12332/com.aspectjdemo E/OOP: 從111112賬戶轉出100.0到222221
10-12 13:25:46.137 12332-12332/com.aspectjdemo E/AOP: 轉賬成功,耗時:2002
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/OOP: 從本地上傳/sd/example.png到www.baidu.com
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/AOP: 檔案上傳成功,耗時:2001
3.3.1.7 總結優點
  • 減少程式碼冗餘
  • 程式碼邏輯更清晰
  • 方便擴充套件、重構

3.3.2 其他方式實現AOP

不是本文重點,不深入,其實是我還沒了解其他方式(~ ̄▽ ̄)~,稍微羅列一下。

  • 原始的AOP模式
  • 動態代理實現AOP
  • 等…

4 AspectJ詳解

建議這一部分直接去看這個文章,這個文章,這部分很詳細,很多語法,各種說明:【深入理解Android之AOP】

4.1 AspectJ介紹
4.2 語法

本節只講解AspectJ的註解語法

4.2.1 Join Points介紹

在Aspect的術語章節講過,join Point(連線點):所有可以注入程式碼的地方,在AspectJ中是有規定的,只有在下表的幾個地方才認為是join Ponit。


Join Points

說明

示例

method call

函式呼叫

比如呼叫Log.e(),這是一處JPoint

method execution

函式執行

比如Log.e()的執行內部,是一處JPoint。注意它和method call的區別。method call是呼叫某個函式的地方。而execution是某個函式執行的內部。

constructor call

建構函式呼叫

和method call類似

constructor execution

建構函式執行

和method execution類似

field get

獲取某個變數

比如讀取DemoActivity.debug成員

field set

設定某個變數

比如設定DemoActivity.debug成員

pre-initialization

Object在建構函式中做得一些工作。

很少使用,詳情見下面的例子

initialization

Object在建構函式中做得工作

詳情見下面的例子

static initialization

類初始化

比如類的static{}

handler

異常處理

比如try catch(xxx)中,對應catch內的執行

advice execution

這個是AspectJ的內容,稍後再說


4.2.2 Pointcuts介紹

在Aspect的術語章節講過,PointCut(切入點):告訴AOP框架,我應該在哪個join point注入一段程式碼。那麼Pointcuts就是篩選出來的符合條件的所有切入點。

1、 直接選擇Join Point

Join Point Ponitcut語法 示例
Method execution execution(MethodSignature) 在Activtiy的所有生命週期執行前,注入程式碼:@Before(“execution(* android.app.Activity.on**(..))”)
Method call call(MethodSignature) 在呼叫指定方法後,注入程式碼:@Before(“execution(* android.app.Activity.on**(..))”)
constructor call call(ConstructorSignature) 在呼叫指定構造方法後,注入程式碼:@Before(“call(com.aspectjdemo.aopexample.UIUtils.new())”)
constructor execution execution(ConstructorSignature) 在執行指定構造方法後,注入程式碼:@After(“call(com.aspectjdemo.aopexample.UIUtils.new())”)
field get get(FieldSignature) 在呼叫指定欄位get方法後,注入程式碼:@After(“get(String com.aspectjdemo.aopexample.AspectJActivity.userName)”)
field set set(FieldSignature) 在呼叫指定欄位get方法前,注入程式碼:@Before(“set(String com.aspectjdemo.aopexample.AspectJActivity.userName)”)
Object initialization initialization(ConstructorSignature) 在指定的物件初始化後,注入程式碼:@After(“initialization(com.aspectjdemo.aopexample.UIUtils.new())”)

MethodSignature匹配規則:

@註解 訪問許可權 返回值的型別 包名.函式名(引數)
1. @註解和訪問許可權(public/private/protect,以及static/final)屬於可選項。如果不設定它們,則預設都會選擇。以訪問許可權為例,如果沒有設定訪問許可權作為條件,那麼public,private,protect及static、final的函式都會進行搜尋。
2. 返回值型別就是普通的函式的返回值型別。如果不限定型別的話,就用*萬用字元表示
3. 包名.函式名用於查詢匹配的函式。可以使用萬用字元,包括*和..以及+號。其中*號用於匹配除.號之外的任意字元,而..則表示任意子package,+號表示子類。
比如:
java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase,也可以表示TestDervied
java..*:表示java任意子類
java..*Model+:表示Java任意package中名字以Model結尾的子類,比如TabelModel,TreeModel

4. 最後來看函式的引數。引數匹配比較簡單,主要是引數型別,比如:
(int, char):表示引數只有兩個,並且第一個引數型別是int,第二個引數型別是char
(String, ..):表示至少有一個引數。並且第一個引數型別是String,後面引數型別不限。在引數匹配中,
..代表任意引數個數和型別
(Object …):表示不定個數的引數,且型別都是Object,這裡的…不是萬用字元,而是Java中代表不定引數的意思

ConstructorSignature匹配規則:

Constructorsignature和Method Signature類似,只不過建構函式沒有返回值,而且函式名必須叫new。比如:
public *..TestDerived.new(..):
public:選擇public訪問許可權
*..代表任意包名
TestDerived.new:代表TestDerived的建構函式
(..):代表引數個數和型別都是任意

FieldSignature匹配規則:

Field Signature標準格式:
@註解 訪問許可權 型別 類名.成員變數名
其中,@註解和訪問許可權是可選的型別:成員變數型別,*代表任意型別類名.成員變數名:成員變數名可以是*,代表任意成員變數
比如,
set(*.base):表示設定所有包名下base變數時的JPoint

2、 間接選擇Join Point

這一類,在demo中沒有示例,有興趣的自己在Demo中新增檢視效果。


關鍵詞

說明

示例

within(TypePattern)

TypePattern標示package或者類。TypePatter可以使用萬用字元

表示某個Package或者類中的所有JPoint。比如

within(Test):Test類中(包括內部類)所有JPoint。圖2所示的例子就是用這個方法。

withincode(Constructor Signature|Method Signature)

表示某個建構函式或其他函式執行過程中涉及到的JPoint

比如

withinCode(* TestDerived.testMethod(..))

表示testMethod涉及的JPoint

withinCode( *.Test.new(..))

表示Test建構函式涉及的JPoint

cflow(pointcuts)

cflow是call flow的意思

cflow的條件是一個pointcut

比如

cflow(call TestDerived.testMethod):表示呼叫TestDerived.testMethod函式時所包含的JPoint,包括testMethod的call這個JPoint本身

cflowbelow(pointcuts)

cflow是call flow的意思。

比如

cflowblow(call TestDerived.testMethod):表示呼叫TestDerived.testMethod函式時所包含的JPoint,包括testMethod的call這個JPoint本身

this(Type)

JPoint的this物件是Type型別。

(其實就是判斷Type是不是某種型別,即是否滿足instanceof Type的條件)

JPoint是程式碼段(不論是函式,異常處理,static block),從語法上說,它都屬於一個類。如果這個類的型別是Type標示的型別,則和它相關的JPoint將全部被選中。

圖2示例的testMethod是TestDerived類。所以

this(TestDerived)將會選中這個testMethod JPoint

target(Type)

JPoint的target物件是Type型別

和this相對的是target。不過target一般用在call的情況。call一個函式,這個函式可能定義在其他類。比如testMethod是TestDerived類定義的。那麼

target(TestDerived)就會搜尋到呼叫testMethod的地方。但是不包括testMethod的execution JPoint

args(TypeSignature)

用來對JPoint的引數進行條件搜尋的

比如args(int,..),表示第一個引數是int,後面引數個數和型別不限的JPoint。


4.2.3 advice介紹

前面例子中已經用過了,具體看下面表中說明即可。


關鍵詞

說明

示例

before()

before advice

表示在JPoint執行之前,需要乾的事情

after()

after advice

表示JPoint自己執行完了後,需要乾的事情。

after():returning(返回值型別)

after():throwing(異常型別)

returning和throwing後面都可以指定具體的型別,如果不指定的話則匹配的時候不限定型別

假設JPoint是一個函式呼叫的話,那麼函式呼叫執行完有兩種方式退出,一個是正常的return,另外一個是拋異常。

注意,after()預設包括returning和throwing兩種情況

返回值型別 around()

before和around是指JPoint執行前或執行後備觸發,而around就替代了原JPoint

around是替代了原JPoint,如果要執行原JPoint的話,需要呼叫proceed


4.3 實現步驟

PS:這一節已經很詳細的在 3.3.1中寫了,還有程式碼示例,往上看。

4.4 AspectJ原理
4.4.1 找到AS下的class檔案

路徑如下:app->build->intermediates->classes->debug->com包下即是我們使用ajc編譯後的class程式碼。

4.4.2 Around原理

使用Around處理後,編譯出來的class檔案

 @TimeTrace("登入")
public static boolean Login(String userName, String passWord) {
    JoinPoint var5 = Factory.makeJP(ajc$tjp_0, (Object)null, (Object)null, userName, passWord);
    TimeTraceAspect var10000 = TimeTraceAspect.aspectOf();
    Object[] var6 = new Object[]{userName, passWord, var5};
    return Conversions.booleanValue(var10000.dealPoint((new AOPLoginUtils$AjcClosure1(var6)).linkClosureAndJoinPoint(65536)));
}

可以看出來,在這個方法的開頭和結尾,都被注入了一些程式碼,成為我們最終執行到手機上的class檔案。

5. 總結

本文只是對AspectJ做了一個入門的介紹,很多高階的用法都未加入進來,在實際專案使用時再進行挖掘吧。

如果有對應的AOP使用場景,建議使用AspectJ,你會感覺到很爽的。