Android Aspectj 从入门到实战

版权声明:本文为博主原创文章,欢迎转载,转载需标明出处。 https://blog.csdn.net/qian520ao/article/details/85308948

AOP 简介

Android Studio 想接入 AspectJ ? 看这篇就对了!从0到1 , 包会!

OOP ( Object Oriented Programming ) 面向对象编程思想
AOP ( Aspect Oriented Programming ) 面向切面编程思想


  • OOP : 面向对象即针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元模块划分。也就是把各个独立的功能封装为个体或模块。

如下图,每一个模块封装了其特有的功能属性,各尽其责,便于其它使用者的调用和复用。

在这里插入图片描述


  • AOP : 针对业务处理过程中特定的切面

如下图,在特定的切面进行代码 「织入」(注意用词,是织入不是hook…),添加共同逻辑( 日志,埋点等) 且不会影响原有模块的业务功能和架构。
在这里插入图片描述


OOP 的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单而又常见的例子:现在想为每个模块加上日志功能,要求模块运行时候能输出日志并统计当前方法耗时。

这个问题放在 OOP 思想中解决的办法通常是设计日志模块,并且在需要统计的地方一一手动加入日志的 API,并且如果日志的 API 改动,那将牵一发而动全身。


这个时候 AOP 的用途便体现出来了。

AOP 主要用途有:日志记录,行为统计,安全控制,事务处理,异常处理,系统统一的认证、权限管理等。可以使用 AOP 思想将这些代码从业务逻辑代码中划分出来,通过对这些行为的分离,可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。



Android AOP 实现原理

在这里插入图片描述
点击查看大图

在这里插入图片描述

上图中 AspectJ 是 Android 实现 AOP 编程思想的具体工具

AspectJ 的使用核心就是它的编译器,它就做了一件事,将 AspectJ 的代码在编译期插入目标程序当中,运行时跟在其它地方没什么两样,因此要使用它最关键的就是使用它的编译器去编译代码 (AspectJ compile) 。
ajc 会构建目标程序与 AspectJ 代码的联系,在编译期将 AspectJ 代码插入被切出的 PointCut 中,达到 AOP 的目的。

也就是在 .java 文件编译为.class文件的时候,将.java 文件做手脚,对相应切入点的代码进行功能代码「织入」。



Android AOP 基本实现方式

上面说道 : AOP是一个编程思想和概念,本身并没有设定具体语言的实现,这实际上提供了非常广阔的发展的空间。

AspectJ 是 AOP 的一个很悠久的实现,它能够和 Java 配合起来使用。( 很稳 , 支付宝app第三方开源也有用到)
在这里插入图片描述

先来了解几个 AspectJ 的 基本和主要的 关键词 :

  • Aspect : Aspect 声明类似于 Java 中的 类声明 ,在Aspect中会包含着一些 Pointcut (切入点)以及相应的 Advice (通知) , Pointcut 和 Advice 的组合可以看做切面
  • Advice : ( 通知 ) , 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个JoinPoint 之前、之后还是完全替代目标方法 的代码。 --> when

  • Pointcut : ( 切入点 ) , 告诉代码注入工具,在何处注入一段特定代码的表达式。 --> where
    下面是 Pointcut 筛选和匹配条件.(为了更精确的找到要切入地方.)
筛选条件 说明 示例
within(TypePattern) 筛选执行的包名路径 within(com.sample.aop.*),在aop包名内的JPoint.
withincode(Method) 筛选执行的方法. withinCode(* A.aopMethod(…)),在A类的aopMethod涉及的JPoint
target(类全限定名) target一般用在call的情况,匹配任意标注了的目标类(指明拦截的方法属于那个类) target(A)就会搜索到由A类调用testMethod的地方
this(类全限定名) 与target雷同,区分点在于:this指方法是在哪个类中被调用的 B类中调用A.testMethod,指定的类为B
args() 对入参进行条件匹配 args(int,…),表示第一个参数是int,后面参数个数和类型不限
其它高级用法

  • JoinPoint : ( 连接点) , 表示在程序中明确定义的点,例如,典型的 方法函数调用类成员的访问 以及对 异常处理程序块 的执行等等,这些都是 JPoint 。(如Log.e()这个函数 , e()可以看作是个 JPoint ,而且调用e()的函数也可以认为是一个 JPoint ) , 也就是所有可以注入代码的地方。

可以说如果 AspectJ 规定中没有这样的 JPoint,那么我们是无法利用AspectJ 来实现功能需求.

织入时机 说明 示例
call 函数调用 比如调用Log.e() , 这是一处JPoint
execution 函数调执行 execution是某个函数执行的内部

例如 A 类中,调用 Pointcut.Method() ,
call 截取的是 在A类中调用该处函数的地方.
execution 截取的则是 Pointcut 内 Method() 执行的方法…

Call(Before)
Pointcut{
    Pointcut Method
}
Call(After)
Pointcut{
  execution(Before)
    Pointcut Method
  execution(After)
}


Demo

实体类 , get/setName方法
下面这个例子通过 AOP 修改getName()返回参数,在调用setName()方法加上打印日志

public class AopDemo {
    public static class innerB {
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}

声明使用 Aspect 的类, 加上@Aspect 注解即可,类里定义了切入点和通知,即组成了切面.

@Pointcut(" call(* getName() ) ")

  • call 表示方法函数调用即要截取的地方
  • 第一个位置表示方法调用的返回值,*表示返回值为任意类型
  • getName表示方法名,也可以使用该方法的全限定名,getName()的括号()代表这个方法的参数,可以指定类型,或者(…)(int,…)这样来代表任意类型和个数的参数。
  • 同时在call()后面可以 加入&&、||、!来进行条件组合,匹配或过滤关键 JPoint
@Aspect
public class DemoAspect {
    @Pointcut("call(* setName(String))")
    public void demo2() {}

    @Around("demo2()")
    public Object arounddemo1(ProceedingJoinPoint joinPoint) throws Throwable {
        Object target = joinPoint.getTarget();

        Object proceed = joinPoint.proceed();
        if (target instanceof AopDemo.innerB) {
            Log.e("log", "call setName");
//            ((AopDemo.innerB) target).setName("haha"); // 可以在方法执行之后搞点事情.
        }
        // joinPoint.proceed()代表执行原始的方法,在这之前之后都可以进行各种逻辑处理。
        return proceed;
    }


    @Pointcut("call(* getName())")
    public void demo1() {}

    @Around("demo1()")
    public String arounddemo1() {
        return "hoho";
    }
}

方法调用出 , 例如在MainActivity里调用实体类的get/setName方法

// MainActivity里调用方法
           AopDemo.innerB innerB = new AopDemo.innerB();
           innerB.setName("ok");
           Toast.makeText(this, "str==" + innerB.getName(), Toast.LENGTH_SHORT).show();

innerB.setName(“ok”); 代码执行后便会加上 Log.e(“log”, “call setName”);
innerB.getName(); 返回的不是"ok",而是我们代码织入的"hoho"


打开当前MainActivity的.class文件,看看 AspectJ 到底做了什么骚操作
// MainActivity.class AspectJ 代码织入前
            innerB innerB = new innerB();
            innerB.setName("ok");
            Toast.makeText(this, "str==" + innerB.getName(), 0).show();
// MainActivity.class AspectJ 代码织入后
            innerB innerB = new innerB();
            String var7 = "ok";
            JoinPoint var9 = Factory.makeJP(ajc$tjp_0, this, innerB, var7);
            var10000 = DemoAspect.aspectOf();
            Object[] var10 = new Object[]{this, innerB, var7, var9};
            var10000.arounddemo1((new MainActivity$AjcClosure1(var10)).linkClosureAndJoinPoint(4112));
            
            Toast.makeText(this, "str==" + DemoAspect.aspectOf().arounddemo1(), 0).show();

如果将 Pointcut 的 call 方法改为 execution , 修改的则是 innerB.class 文件.

也就是说 AspectJ 实现 AOP 编程思想的方法就是在 .java 文件编译为 .class 期间,使用 ajc (AspectJ compile) 编译器 , 将需要 织入 的代码插到特定的 Pointcut 中

以上便是 AspectJ 基本使用方式,要挑战高级用法,前往 AspectJ 开发手册.



自定义PointCut

例如我们要在代码中进行权限检查,如果没有权限则不执行方法(或者执行权限调用方法,权限申请成功后再执行目标方法)

  • 1.创建自定义注解行为
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomPointCut {
    String[] permissionName();
}
  • 2.方法调用,在MainActivity调用注解方法
    @CustomPointCut(permissionName = {"PHONE", "STATUS"})
    public void customMethod() {
        Toast.makeText(this, "customMethod call", Toast.LENGTH_SHORT).show();
    }
  • 3.声明使用 Aspect 的类
@Aspect
public class DemoAspect {
	// 具体使用的时候,CustomPointCut 要改为具体的全限定名.
    @Pointcut("execution(@com.xxx.CustomPointCut * *..*.*(..))")
    public void customMethod() {
    }
    
    @Around("customMethod()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 类名
        String className = methodSignature.getDeclaringType().getSimpleName();
        // 方法名
        String methodName = methodSignature.getName();
        // 功能名
        CustomPointCut behaviorTrace = methodSignature.getMethod().getAnnotation(CustomPointCut.class);
        String[] value = behaviorTrace.permissionName();
        // value -- > phone,status

        long start = System.currentTimeMillis();
        
        // 也可以不执行joinPoint.proceed(),根据业务需求没有权限/登录不调用目标方法
        Object result = joinPoint.proceed();// result 为目标方法调用后的返回值
        
        long duration = System.currentTimeMillis() - start;//可以统计方法耗时.

        return result;//返回值,可以任性修改.
    }
}

这样只要代码中加入 @CustomPointCut 注解,便可以统一处理权限操作. 登录判断也可以用此方法来统一处理。代码精简,一步到位.

下面看一下 AspectJ 在编译时期做了哪些处理

// MainActivity.class
    @CustomPointCut(
        permissionName = {"PHONE", "STATUS"}
    )
    public void customMethod() {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        DemoAspect var10000 = DemoAspect.aspectOf();
        Object[] var2 = new Object[]{this, var1};
        var10000.aroundMethod((new MainActivity$AjcClosure1(var2)).linkClosureAndJoinPoint(69648));
    }



Android AspectJ 接入实战



1.添加aop模块,配置依赖

建议新建一个aop相关模块 , 方便 aop 作为Demo 项目的调试,后期可以轻松依赖进自己的工程项目。


首先创建一个 lib-aop 模块,作为 library 方式引入到 Demo 项目中
关键有以下两点 :

  1. 依赖 ajc 编译脚本,我们将脚本内容写 aspectj-configure-lib.gradle 文件中,aop 模块引用该脚本
  2. 添加 aspectjrt 依赖,添加的方式有两种,如果是多人协作建议下载 jar 包配置到本地依赖.

aspectj 相关jar包 : maven仓库地址.
示例项目中所用 jar 包 : aspectjrt-1.8.13.jar. ( 放置 lib-aop 模块内 libs 文件夹下)

// aop 模块内 build.gradle
apply plugin: 'com.android.library'
apply from: '../lib-aop/aspectj-configure-lib.gradle' // ajc 编译所需gradle脚本

android {
    compileSdkVersion 27
    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    
}

dependencies {
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support:design:27.1.1'
    implementation 'com.android.support:support-v4:27.1.1'

    //依赖方式 1. 使用本地jar包
    api fileTree(include: ['*.jar'], dir: 'libs') // 作用范围一定得是 api !!!

    //依赖方式 2. 配置 maven 地址
    // api 'org.aspectj:aspectjrt:1.8.13'
}

  • 踩坑1 :aspectjrt 依赖的作用范围一定得是 api ,否则其它模块死活织入不了代码


2.配置 ajc 脚本

建议 在 lib-aop 模块 内新建 aspectj-configure-lib.gradle 文件,脚本内容为以下代码

// aspectj-configure-lib.gradle
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.13'
        classpath 'org.aspectj:aspectjweaver:1.8.13'
    }
}

repositories {
    mavenCentral()
}


android.libraryVariants.all { variant ->
    if (variant.buildType.isDebuggable()) {
//        return;   //开放后debug模式不生效
    }
    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", 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
            }
        }
    }
}
  • 踩坑2 :aspectj-configure-lib.gradle 只适用于lib 模块!
    在 aspectj-configure-lib.gradle 的第 21行中配置到 : android.libraryVariants.all,这只适用于 类型为 library 的 module 享用
    application 的 module 只需要将这行配置改为 android.applicationVariants.all,即可.


3.配置 app 的脚本

在 Demo 工程中的 app 目录下,配置 build.gradle 脚本文件

//  build.gradle 
apply plugin: 'com.android.application'

// ajc 编译所需gradle脚本,application适用
apply from: '../lib-aop/aspectj-configure-app.gradle'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.demo.aop"
        minSdkVersion 17
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'

    implementation project(":lib-aop")// 依赖 aop 模块,这里对作用范围没有限制.
}


上文说到 aspectj-configure-app.gradle 与 aspectj-configure-lib.gradle 的不同点只在于 第22行,

// aspectj-configure-app.gradle
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.13'
        classpath 'org.aspectj:aspectjweaver:1.8.13'
    }
}

repositories {
    mavenCentral()
}



final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    if (variant.buildType.isDebuggable()) {
//        return;   //开放后debug模式不生效
    }
    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
            }
        }
    }
}



4.细节提醒

如此一来,aspectj 便配置完成了,另外,下面几点细节提醒 :

  • lib-aop 模块内需配置 proguard-rules.pro 混淆相关,将 lib-aop 内相关代码的包名添加进配置中,防止混淆.
-dontwarn com.xxx.aop.**
-keep class com.xxx.aop.**{*;}

  • 其它模块如果需要在编译时期使用 aspectj 代码织入功能,需要加入gradle脚本 ajc 配置
   app		 : apply from : '../lib-aop/aspectj-configure-app.gradle'
   library	 : apply from : '../lib-aop/aspectj-configure-lib.gradle'

并且依赖 lib-aop 模块(如果有公用base模块,作用范围可以用 api ,其它模块就可以不用再次添加依赖了。)

implementation project(":lib-aop")// 依赖 aop 模块

  • 踩坑3 :lib-aop 不能打成 aar !
    编写 aspect 相关的模块不能打成 aar ,需要模块项目引用,否则编译打包后会找不到类java.lang.NoClassDefFoundError: Failed resolution of: xxx/xxx/具体类

  • 踩坑4 :J神的 hugo 插件 debug 时期 ajc 编译无效 , 因为在 ajc 编译脚本 gradle 中,如果是 Debuggable , return … ( 这一点估计坑了许多人. )
    if (variant.buildType.isDebuggable()) {
	       return;
    }

  • 踩坑5 :AS编译时期 IOException ,或文件被占用.
java.lang.RuntimeException: java.io.IOException: Failed to delete C:\Users\..\build\intermediates\intermediate-jars\debug\classes.jar

关闭任务管理器中 java.exe 进程 ,再次编译即可.



参考

先理清概念 : Android AOP面向切面编程AspectJ.

再深入了解 阿拉神农的 :深入理解Android之AOP. 大多数博客都参考该篇文章

最后android配置 aspectj : AndroidStudio 配置 AspectJ 环境实现AOP.

猜你喜欢

转载自blog.csdn.net/qian520ao/article/details/85308948