Android AOP编程之AspectJ

Posted 殇神马

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 getfield get
field set设置成员处
static initializationstatic块初始化
handler异常处理处

这些都是JointPoint

3.切点 PointCut

切点(PointCut)很简单,我们上面说了连接点,切点就是具体是哪一个连接点,比具体如是在哪一个或哪一种方法中进行代码的织入;
那如何筛选出具体是哪一个连接点,我们在切面类中定义方法,使用@Pointcut注解,表示定义的是切点,注解值是一个表达式: 连接点类型(签名),表示具体是哪一个或者哪一类连接点;
比如连接点是一个方法执行处,表达式规范如下:

execution(@注解? <修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)

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 getfield getget(FieldSignature)
field set设置成员处set(FieldSignature)
static initializationstatic块初始化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的主要内容,如果未能解决你的问题,请参考以下文章

Android AOP编程之AspectJ

Android基于AOP的非侵入式监控之——AspectJ实战

AOP之AspectJ在android中的解读

AOP之AspectJ在android中的解读

AOP之AspectJ在Android实现无侵入埋点实践

AOP之AspectJ - 代码注入