天天看點

Android AOP程式設計之AspectJ

一、AspectJ

Aspectj是一個AOP架構,也是通過對位元組碼操作,來實作AOP的,但是與ASM需要開發者直接編寫操作位元組碼的代碼之外,使用Aspectj不需要開發者直接編寫操作位元組碼代碼, 而是隻需要按照規範編寫我們需要處理的Java代碼即可;由Aspectj幫助我們将編寫的Java代碼的位元組碼織入到.class位元組碼檔案中實作位元組碼插樁操作;

二、AspectJ中的基本概念

1.切面 Aspect

我們在學習AOP的時候,知道AOP實際就是針對的是業務邏輯中的某個處理步驟,比如日志處理,登入判斷,權限擷取,資料埋點,異常處理,這些很多業務邏輯中都需要的,可以從業務邏輯中單獨抽出來的,這個在需要業務邏輯處都需要,可以從業務邏輯抽離出來的部分,我們稱之為一個切面。

我們到時候需要定義一個切面類,使用@Aspect注解,表示是切面處理類;

@Aspect
    public class TagAspect {
         ....
    }
           

2.連接配接點 JoinPoint

就是我們切面可以插入的地方,也就是我們位元組碼可以織入的地方,比如方法的調用處,方法的執行處,都是連接配接點,也即我們可以織入位元組碼的地方;

JoinPoint 說明
method call 方法調用處
method execution 方法執行處
constructor call 構造方法調用處
constructor execution 構造方法執行處
field get field get
field set 設定成員處
static initialization static塊初始化
handler 異常處理處

這些都是JointPoint

3.切點 PointCut

切點(PointCut)很簡單,我們上面說了連接配接點,切點就是具體是哪一個連接配接點,比具體如是在哪一個或哪一種方法中進行代碼的織入;

那如何篩選出具體是哪一個連接配接點,我們在切面類中定義方法,使用@Pointcut注解,表示定義的是切點,注解值是一個表達式: 連接配接點類型(簽名),表示具體是哪一個或者哪一類連接配接點;

比如連接配接點是一個方法執行處,表達式規範如下:

execution表示的是方法的執行類連接配接點;括号裡面就是方法的簽名;

@注解 :表示的是方法使用的注解類型(要寫全類名:@com.example.aspectjmodule.Tag)

?表示可選的,不是必須要寫的

<修飾符模式>: 就是方法的修飾符 public,private,protected等,也不是必填的

*表示任意類型

<傳回類型模式> :方法傳回值類型,void,int,等 , *表示任意類型 必填

方法名: 表示方法名字

test 就是表示方法名字是testDev所有方法

com.example…* 表示是com.example包下的任意方法

*表示任意方法

等等寫法

<參數模式> :方法的形參類型

(int) 一個int類型形參

(int,int) 兩個int類型形參

(. .) 任意個數,任意類型形參

@Aspect
    public class TagAspect {

         @Pointcut("execution(@execution(@com.example.aspectjmodule.Tag * *(..)))")
         public void tagPointCut(){

         }

    }
           
JoinPoint 說明 PointCut
method call 方法調用處 call(MethodSignature)
method execution 方法執行處 execution(MethodSignature)
constructor call 構造方法調用處 call(ConstructorSignature)
constructor execution 構造方法執行處 execution(ConstructorSignature)
field get field get get(FieldSignature)
field set 設定成員處 set(FieldSignature)
static initialization static塊初始化 staticinitialization(TypeSignature)
handler 異常處理處 handler(TypeSignature)

三、使用Aspectj實作位元組碼插樁

1.Aspectj的內建步驟

(1)在Android項目下建立一個Module (Android Library), 一般會将Aspectj相關的代碼放到一個單獨的Module;是以先建立一個Module,名稱為AspectjModule

(2) 在Aspectj Module的build.gradle下的buildsc添加Aspectj插件的依賴

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

   }
           

注意這個buildscript塊必須要放到build.gradle的plugins{}塊的上面,

否則會報如下錯誤:

all buildscript {} blocks must appear before any plugins {} blocks in the script
           

(3)在Module的build.gradle的dependencies下添加Aaspectj的依賴

dependencies {
       api 'org.aspectj:aspectjrt:1.9.7'
}
           

(4)在Aspectj Module的build.gradle下添加如下建構腳本

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

android.libraryVariants.all { variant ->

    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 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;
            }
        }
    }
}  
           

(5) 将Aspectj Module 添加為app 主Module的的依賴Moduleapp Module的

app 的build.gradle中dependencies添加如下

dependencies {
    implementation project(path: ':AspectjModule')
}    
           

(6)在app主Module中也要添加Aspectj的插件依賴,以及上面的建構腳本,但是app主Module的建構腳本稍有不同:

app主module 也即Application Module

用的是project.android.applicationVariants.all

AspectjModule也即Android Libdary Module使用的是android.libraryVariants.all

app主Module建構腳本如下

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

project.android.applicationVariants.all { variant ->

    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 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;
            }
        }
    }
}
           

這裡要注意的是,不僅僅是app主Module,其他隻要要使用Aspectj的Module,也是要添加Aspectj的插件依賴,以及建構腳本的;

上面就是內建Aspectj的步驟

2.用Aspectj實作位元組碼插樁

內建了Aspectj之後,下面我們來使用Aspectj實作一個功能;

比如我們希望在某些方法開始執行,和結束執行添加列印日志;

Aspectj的相關代碼,我們就在AspectjModule中去編寫;

(1)首先我們定義一個注解

@Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Tag {

     String value();

   }
           

(2)定義一個Aspectj處理類,Aspectj相關處理代碼會在這個類中

@Aspect
     public class TagAspect {

     }
           

Aspectj處理類要是用@Aspect注解

(3)定義PointCut(切點)

定義切點:

定義一個方法,名字任意,使用@PointCut注解,@PointCut注解值是篩選具體的連接配接點的表達式

@Pointcut("execution(@com.example.aspectjmodule.Tag * *(..))")
    public void tagPointCut() {

    }
           

這個切點的定義表示是所有使用了com.example.aspectjmodule.Tag這個注解的方法

(4)定義代碼織入的地方,并編寫織入代碼

也是要定義一個方法,方法名任意,然後我們這裡給方法使用了@Around注解

注解的值就是切點的方法名加形參 tagPointCut()

@Before 前置通知 切點方法執行之前通知

@After 後置通知 切點方法執行之後通知

@Around 環繞通知 可以實作前置後置通知

@AfterReturning 傳回通知 在切點方法傳回之後執行

@AfterThrowing 異常通知 切點抛出異常時執行

@Around("tagPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        
        //拿到方法的簽名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //類名
        String className = methodSignature.getDeclaringType().getSimpleName();
        //方法名
        String methodName = methodSignature.getName();

        Tag tagAnnotation = methodSignature.getMethod().getAnnotation(Tag.class);

        String value = tagAnnotation.value();

        Log.e(value, "begin execute"); //方法執行前添加列印代碼

        Object proceed = null;
        try {
            proceed = joinPoint.proceed(); 
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        Log.e(value, "end execute"); //方法執行後添加列印代碼

        return proceed;
    }
           

我們在方法執行的前後都添加了列印日志

(5)我們需要添加列印的方法使用Tag注解

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        test();
    }

    @Tag("TAG")
    public void test() {
        Log.e(TAG, "test: ");
    }

}    
           

然後運作程式,運作程式之後,logcat中日志

begin execute

test: 

end execute
           

在test方法執行前後成功添加了列印日志

以上就是使用AspectJ實作位元組碼插樁的基本流程;

注意:我們在使用Java編寫的Android項目中可以這樣使用AspectJ,但是如果是Kotlin編寫的Android項目,這樣使用AspectJ是無效的,但是我們使用gradle_plugin_android_aspectjx這個第三方庫,一個基于AspectJ并在此基礎上擴充出來可應用于Android開發平台的AOP架構,可作用于java源碼,class檔案及jar包,同時支援kotlin的應用;

Android AOP程式設計之AspectJ

歡迎掃碼訂閱公衆号,公衆号主要分享原創或轉載移動開發技術文章,和廣大移動開發者一起學習成長!

繼續閱讀