Android AOP學習之:AspectJ實踐

AOP

AOP(Aspect Oriented Programming),中文一般翻譯成面向切面編程。在Java當中咱們經常說起到的是OOP(Object Oriented Programming)面向對象編程。其實這些都只是編程中從不一樣的思考方向得出的一種編程思想、編程方法。javascript

在面向對象編程中,咱們經常說起到的是「everything is object」一切皆對象。咱們在編程過程當中,將一切抽象成對象模型,思考問題、搭建模型的時候,優先從對象的屬性和行爲職責出發,而不固執於具體實現的過程。html

但是當咱們深挖裏面的細節的時候,就會發現一些很矛盾的地方。好比,咱們要完成一個事件埋點的功能,咱們但願在原來整個系統當中,加入一些事件的埋點,監控並獲取用戶的操做行爲和操做數據。java

按照面向對象的思想,咱們會設計一個埋點管理器,而後在每一個須要埋點的地方都加上一段埋點管理器的方法調用的邏輯。咋看起來,這樣子並無什麼問題。可是咱們會發現一個埋點的功能已經侵入到了咱們系統的內部,埋點的功能方法調用處處都是。若是咱們要對埋點的功能進行撤銷、遷移或者重構的時候,都會存在不小的代價。android

那麼AOP的思想能幹什麼呢?AOP提倡的是針對同一類問題的統一處理。好比咱們前面說起到的事件埋點功能,咱們的埋點功能散落在系統的每一個角落(雖然咱們的核心邏輯能夠抽象在一個對象當中)。若是咱們將AOP與OOP二者相結合,將功能的邏輯抽象成對象(OOP,同一類問題,單一的原則),再在一個統一的地方,完成邏輯的調用(AOP,將問題的處理,也便是邏輯的調用統一)。這樣子,咱們就能夠用更加完美的結構完成系統的功能。git

上面其實已經揭示了AOP的實際使用場景:無侵入的在宿主系統中插入一些核心的代碼邏輯:日誌埋點、性能監控、動態權限控制、代碼調試等等。github

實現AOP的的核心技術其實就是代碼織入技術(code injection),對應的編程手段和工具其實有不少種,好比AspectJ、JavaAssit、ASMDex、Dynamic Proxy等等。關於這些技術的實踐的對比,能夠參考這篇文章。Practical Introduction into Code Injection with AspectJ, Javassist, and Java Proxysql

AspectJ

AspectJ其實是對AOP編程思想的一個實踐。AspectJ提供了一套全新的語法實現,徹底兼容Java(其實跟Java之間的區別,只是多了一些關鍵詞而已)。同時,還提供了純Java語言的實現,經過註解的方式,完成代碼編織的功能。所以咱們在使用AspectJ的時候有如下兩種方式:編程

  • 使用AspectJ的語言進行開發
  • 經過AspectJ提供的註解在Java語言上開發

由於最終的目的其實都是須要在字節碼文件中織入咱們本身定義的切面代碼,無論使用哪一種方式接入AspectJ,都須要使用AspectJ提供的代碼編譯工具ajc進行編譯。app

經常使用術語

在瞭解AspectJ的具體使用以前,先了解一下其中的一些基本的術語概念,這有利於咱們掌握AspectJ的使用以及AOP的編程思想。eclipse

在下面的關於AspectJ的使用相關介紹都是以註解的方式使用做爲說明的。

JoinPoints

JoinPoints(鏈接點),程序中可能做爲代碼注入目標的特定的點。在AspectJ中能夠做爲JoinPoints的地方包括:

JoinPoints 說明 示例
method call 函數調用 好比調用Log.e(),這是一處Joint point
method execution 函數執行 好比Log.e()的執行內部,是一處Joint Point
constructor call 構造函數調用 與方法的調用類型
constructor executor 構造函數執行 與方法的執行執行
field get 獲取某個變量
field set 設置某個變量
static initialization 類初始化
initialization object在構造函數中作的一些工做
handler 異常處理 對應try-catch()中,對應的catch塊內的執行

PointCuts

PointCuts(切入點),其實就是代碼注入的位置。與前面的JoinPoints不一樣的地方在於,其實PointCuts是有條件限定的JoinPoints。好比說,在一個Java源文件中,會有不少的JoinPoints,可是咱們只但願對其中帶有@debug註解的地方纔注入代碼。因此,PointCuts是經過語法標準給JoinPoints添加了篩選條件限定。

Advice

Advice(通知),其實就是注入到class文件中的代碼片。典型的 Advice 類型有 before、after 和 around,分別表示在目標方法執行以前、執行後和徹底替代目標方法執行的代碼。

Aspect

Aspect(切面),Pointcut 和 Advice 的組合看作切面。

Weaving

注入代碼(advices)到目標位置(joint points)的過程

AspectJ使用配置

在android studio的android工程中使用AspectJ的時候,咱們須要在項目的build.gradle的文件中添加一些配置:

  • 首先在項目的根目錄的build.gradle中添加以下配置:
buildscript {
    ...
    dependencies {        
        classpath 'org.aspectj:aspectjtools:1.8.6'
        ...
    }
}複製代碼
  • 單獨定一個module用於編寫aspect的切面代碼,在該module的build.gradle目錄中添加以下配置(若是咱們的切面代碼並非獨立爲一個module的能夠忽略這一步):
apply plugin: 'com.android.library'

android {
    ...
}

android.libraryVariants.all { variant ->
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        //下面的1.8是指咱們兼容的jdk的版本
        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;
            }
        }
    }
}

dependencies {
    ...
    compile 'org.aspectj:aspectjrt:1.8.6'
    ...

}複製代碼
  • 在咱們的app module的build.gradle文件中添加以下配置:
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

apply plugin: 'com.android.application'

android {
    ...
}

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.6'
    //本身定義的切面代碼的模塊
    compile project(":aspectj")
    ...
}複製代碼

其實,第二步和第三步的配置是同樣的,而且在配置當中,咱們使用了gradle的log日誌打印對象logger。所以咱們在編譯的時候,能夠得到關於代碼織入的一些異常信息。咱們能夠利用這些異常信息幫助檢查咱們的切面代碼片斷是否語法正確。要注意的是:logger的日誌輸出是在android studio的Gradle Console控制檯顯示的,並非咱們常規的logcat

經過上面的方式,咱們就完成了在android studio中的android項目工程接入AspectJ的配置工做。這個配置有點繁瑣,所以網上其實已經有人寫了相應的gradle插件,具體能夠參考:AspectJ Gradle插件

Pointcut使用語法

在前面術語當中說起到,Pointcut實際上是加了篩選條件限制的JoinPoints,而每種類型的JoinPoint都會對應有本身的篩選條件的匹配格式,Pointcut的定義就是要根據不一樣的JoinPoint聲明合適的篩選條件表達式。

直接對JoinPoint的選擇

JoinPoint類型 Pointcut語法
Method Execution(方法執行) execution(MethodSignature)
Method Call(方法調用) call(MethodSignature)
Constructor Execution(構造器執行) execution(ConstructorSignature)
Construtor Call(構造器調用) call(ConstructorSignature)
Class Initialization(類初始化) staticinitialization(TypeSignature)
Field Read(屬性讀) get(FieldSignature)
Field Set(屬性寫) set(FieldSignature)
Exception Handler(異常處理) handler(TypeSignature)
Object Initialization(對象初始化) initialization(ConstructorSignature)
Object Pre-initialization(對象預初始化) preinitialization(ConstructorSignature)
Advice Execution(advice執行) adviceexecution()
  1. 在上面表格中所說起到的MethodSignature、ConstructorSignature、TypeSignature、FieldSignature,它們的表達式均可以使用通配符進行匹配。
  2. 表格當中的execution、call、set、get、initialization、preinitialization、adviceexecution、staticinitialization這些都是屬於AspectJ當中的關鍵詞
  3. 表格當中的handler只能與advice中的before(advice的相應關鍵詞及使用參考後文)一塊兒使用
  • 經常使用通配符
通配符 意義 示例
* 表示除」.」之外的任意字符串 java.*.Date:能夠表示java.sql. Date,java.util. Date
.. 表示任意子package或者任意參數參數列表 java..*:表示java任意子包;void getName(..):表示方法參數爲任意類型任意個數
+ 表示子類 java..*Model+:表示java任意包中以Model結尾的子類
  • MethodSignature

    定義MethodSignature的條件表達式與定義一個方法類型,其結構以下:

    • 表達式:

      [@註解] [訪問權限] 返回值的類型 類全路徑名(包名+類名).函數名(參數)

    • 說明:

      1. []當中的內容表示可選項。當沒有設定的時候,表示全匹配
      2. 返回值類型、類全路徑、函數名、參數均可以使用上面的通配符進行描述。
    • 例子:

      public (..) :表示任意參數任意包下的任意函數名任意返回值的public方法

      @com.example.TestAnnotation com.example..(int) :表示com.example下被TestAnnotation註解了的帶一個int類型參數的任意名稱任意返回值的方法

  • ConstructorSignature

    Constructorsignature和Method Signature相似,只不過構造函數沒有返回值,並且函數名必須叫new.

    • 表達式:

      [@註解] [訪問權限] 類全路徑名(包名+類名).new(參數)

    • 例子:

      public *..People.new(..) :表示任意包名下面People這個類的public構造器,參數列表任意

  • FieldSignature

    與在類中定一個一個成員變量的格式相相似。

    • 表達式:

      [@註解] [訪問權限] 類型 類全路徑名.成員變量名

    • 例子:

      String com.example..People.lastName :表示com.example包下面的People這個類中名爲lastName的String類型的成員變量

  • TypeSignature

    TypeSignature其實就是用來指定一個類的。所以咱們只須要給出一個類的全路徑的表達式便可

間接對JoinPoint進行選擇

除了上面表格當中說起到的直接對Join Point選擇以外,還有一些Pointcut關鍵字是間接的對Join Point進行選擇的。以下表所示:

Pointcut語法 說明 示例
within(TypeSignature) 表示在某個類中全部的Join Point within(com.example.Test):表示在com.example.Test類當中的所有Join Point
withincode(ConstructorSignature/MethodSignature) 表示在某個函數/構造函數當中的Join Point withincode( ..Test(..)):表示在任意包下面的Test函數的全部Join Point
args(TypeSignature) 對Join Point的參數進行條件篩選 args(int,..):表示第一個參數是int,後面參數不限的Join Point

除了上面幾個以外,其實還有target、this、cflow、cflowbelow。由於對這幾個掌握不是很清楚,這裏不詳細說明。有興趣的能夠參考這篇文章的說明:深刻理解Android之AOP

組合Pointcut進行選擇

Pointcut之間可使用「&& | !」這些邏輯符號進行拼接,將兩個Pointcut進行拼接,完成一個最終的對JoinPoint的選擇操做。(其實就是將上面的間接選擇JoinPoint表中關鍵字定義的Pointcut與直接選擇JoinPoint表關鍵字定義的Pointcut進行拼接)

Advice語法使用

AspectJ提供的Advice類型以下表所示:

Advice語法 說明
before 在選擇的JoinPoint的前面插入切片代碼
after 在選擇的JoinPoint的後面插入切片代碼
around around會替代原來的JoinPoint(咱們能夠徹底修改一個方法的實現),若是須要調用原來的JoinPoint的話,能夠調用proceed()方法
AfterThrowing 在選擇的JoinPoint異常拋出的時候插入切片的代碼
AfterReturning 在選擇的JoinPoint返回以前插入切片的代碼

AspectJ實踐

如下關於AspectJ的實踐都是使用AspectJ提供的Java註解的方式來實現。

直接使用Pointcut

定義一個People類,裏面包含一個靜態代碼塊

public class People {
    ...
    static {
        int a = 10;
    }
    ...
}複製代碼

接下來定義一個切片,裏面包含一個Advice往靜態代碼塊當中插入一句日誌打印

// 這裏使用@Aspect註解,表示這個類是一個切片代碼類。
// 每個定義了切片代碼的類都應該添加上這個註解

@Aspect
public class TestAspect {

    public static final String TAG = "TestAspect";

    //@After,表示使用After類型的advice,裏面的value其實就是一個poincut

    @After(value = "staticinitialization(*..People)")
    public void afterStaticInitial(){
        Log.d(TAG,"the static block is initial");
    }
}複製代碼

最後,在apk當中的dex文件的People的class文件中會多出下面這樣的一段代碼:

static {
    TestAspect.aspectOf().afterStaticInitial();
}複製代碼

自定義Pointcut && 組合Pointcut

咱們可使用AspectJ提供的「@Pointcut」註解完成自定義的Pointcut。下面經過「在MainActivity這個類裏面完成異常捕捉的切片代碼」這個例子來演示自定義Pointcut和組合Pointcut的使用

public class MainActivity extends Activity {

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        test("this is tag for test");
    }

public void test(String test) {
    try {
        throw new IllegalArgumentException("self throw exception");
        } catch (Exception e) {

        }
    }
}複製代碼
@Aspect
public class TestAspect {

    @Pointcut(value = "handler(Exception)")
    public void handleException(){

    }

    @Pointcut(value = "within(*..MainActivity)")
    public void codeInMain(){

    }

    // 這裏經過&&操做符,將兩個Pointcut進行了組合
    // 表達的意思其實就是:在MainActivity當中的catch代碼塊

    @Before(value = "codeInMain() && handleException()")
    public void catchException(JoinPoint joinPoint){
        Log.d(TAG,"this is a try catch block");
    }
}複製代碼

最後編譯後的MainActivity當中test方法變成了:

public void test(String test) {
        try {
            throw new IllegalArgumentException("self throw exception");
        } catch (Object e) {
            TestAspect.aspectOf().catchException(Factory.makeJP(ajc$tjp_0, (Object) this, null, e));
        }
    }複製代碼

使用總結

通過上面兩個簡單的小例子基本上可以明白了在android studio的android項目中接入AspectJ的流程,這裏簡單總結一下:

  1. 環境搭建(主要是配置代碼編譯使用ajc編譯器,而且添加gralde的logger日誌輸出,方便調試)
  2. 使用@Aspect註解,標示某個類做爲咱們的切片類
  3. 使用@Pointcut註解定義pointcut。這一步實際上是可選的。可是爲了提升代碼的可讀性,能夠經過合理拆分粒度,定義切點,並經過邏輯操做符進行組合,達到強大的切點描述
  4. 根據實際須要,經過註解定義advice的代碼,這些註解包括:@Before,@After,@AfterThrowing,@AfterReturning,@Around.

參考文獻

  1. 深刻理解Android之AOP
  2. @AspectJ cheat sheet
  3. Practical Introduction into Code Injection with AspectJ, Javassist, and Java Proxy
相關文章
相關標籤/搜索