天天看点

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

欢迎扫码订阅公众号,公众号主要分享原创或转载移动开发技术文章,和广大移动开发者一起学习成长!

继续阅读