文章

AspectJ基础(可用ASM替代)

AspectJ基础(可用ASM替代)

AspectJ 基础

AOP?

AOP(Aspect-Oriented Programming)是一种面向切面的编程范式,是一种编程思想,旨在通过分离横切关注点,提高模块化,可以跨越对象关注点。
AOP 的典型应用是 Spring 的事务机制,日志记录。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等;主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
AspectJ 和 Spring AOP 是 AOP 的两种实现方案。

什么是 AspectJ?

AspectJ 作为 Java 中流行的 AOP(aspect-oriented programming) 编程扩展框架。

  • AspectJ 是一种 AOP 框架
  • 内部使用的是 BCEL 框架 来完成其功能
  • 调用时机是在 Java 文件编译成 .class 文件之后,生成 Dalvik 字节码之前执行
  • AspectJ 是一种编译期的用注解形式实现的 AOP

APT、AspectJ 和 Javassist 工作时机:
image.png

Android 中使用 AspectJ

AspectJ 中由哪些组件组成?

  • PointCut 切点,是一个(组)基于正则表达式的表达式;本身是一个表达式,是基于正则语法的。通常一个 PointCut 会选取程序中的某些我们感兴趣的执行点
  • joinPoint 连接点,通过 PointCut 选取出来的集合中的具体的一个执行点,我们就叫 JoinPoint
  • Advice 通知,在选取出来的 JoinPoint 上要执行的操作、逻辑:Before、After、AfterReturning、AfterThrowing、Around
  • aspect 切面,是一个抽象的概念,指应用程序不同模块的某一个领域或方面,由 PointCut 和 Advice 组成
  • Target,被 AspectJ 横切的对象,我们所说的 JoinPoint 就是 Target 的某一行,如方法开始执行的地方,方法类调用某个其他方法的代码

引入 AspectJ

AGP 低版本引入

  1. 在项目根目录的 build.gradle 里依赖 AspectJX
1
2
3
dependencies {
	classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
}

或者使用 product 目录下的 jar 包,在你的项目根目录下新建目录 plugins,把 product/gradle-android-plugin-aspectjx-2.0.10.jar 拷贝到 plugins,依赖 jar 包

1
2
3
dependencies {
	classpath fileTree(dir:'plugins', include:['*.jar'])
}
  1. 添加插件
1
2
3
apply plugin: 'android-aspectjx'
// 或
apply plugin: 'com.hujiang.android-aspectjx'
  1. 添加依赖
1
implementation 'org.aspectj:aspectjrt:1.9.7'
  1. 添加不扫描的包 跟 Android 同级
1
2
3
4
5
6
7
8
9
aspectjx {
    // 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突)
    // 否则就会起冲突:ClassNotFoundException: Didn't find class on path: DexPathList
    exclude 'androidx', 'com.google', 'com.squareup', 'com.alipay', 'com.taobao',
            'org.apache',
            'org.jetbrains.kotlin',
            "module-info", 'versions.9'
}

AGP 高版本

1
2
3
4
classpath 'io.github.wurensen:gradle-android-plugin-aspectjx:3.3.2'

// app build.gradle 
apply plugin: 'io.github.wurensen.android-aspectjx'

集成遇到的问题

高版本 AGP 报错 No such property: FD_INTERMEDIATES

Failed to apply plugin ‘android-aspectjx’. No such property: FD_INTERMEDIATES for class: com.android.builder.model.AndroidProject`

解决:
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx/issues/332
修改支持高版本 AGP 的 aspectj 插件https://github.com/wurensen/gradle_plugin_android_aspectjx

1
2
3
4
classpath 'io.github.wurensen:gradle-android-plugin-aspectjx:3.3.2'

// app build.gradle 
apply plugin: 'io.github.wurensen.android-aspectjx'

各种报错,需要 exclude

如:

Caused by: org.aspectj.weaver.BCException: Whilst processing type ‘Lcom/appsflyer/internal/AFa1wSDK$4;’ - cannot cast the outer type to a reference type. Signature=Lcom/appsflyer/internal/AFa1wSDK; toString()=com.appsflyer.internal.AFa1wSDK class=AFa1wSDK

解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apply plugin: 'io.github.wurensen.android-aspectjx'
aspectjx {
    // 移除kotlin相关,编译错误和提升速度
    exclude 'kotlin.jvm', 'kotlin.internal', 'kotlin'
    exclude 'kotlinx.coroutines.internal', 'kotlinx.coroutines.android'
    // 排除所有package路径中包含`android.support`的class文件及库(jar文件)
    exclude 'android.support'
    exclude 'com.appsflyer'
    // Caused by: org.aspectj.weaver.BCException: Whilst processing type 'Lcom/appsflyer/internal/AFa1wSDK$4;' - cannot cast the outer type to a reference type.  Signature=Lcom/appsflyer/internal/AFa1wSDK; toString()=com.appsflyer.internal.AFa1wSDK class=AFa1wSDK
    exclude 'com.google'
    // Caused by: java.lang.IllegalStateException: Expecting .,<, or ;, but found - while unpacking <T::Lcom/google/android/gms/internal/firebase-auth-api/zzabd<*>;>Ljava/lang/Object;
    exclude 'leakcanary'
    exclude 'com.hujiang'
    exclude 'com.google.android.material', 'androidx.appcompat', 'androidx.fragment', 'androidx.emoji2', 'androidx.viewpager', 'androidx.paging'
}

AspectJ API

注解

@Before

在方法执行之前要插入的代码。

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
    String value();
    String argNames() default "";
}

@After

在方法执行之后要插入的代码。

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {
    String value();
    String argNames() default "";
}

@AfterReturning

在方法执行后,返回一个结果再执行;如果没有结果,用此修饰符修饰是不会执行的
注意的是方法参数必须和注解中的值一致。

1
2
3
4
5
6
7
8
9
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterReturning {
    String value() default "";
    String pointcut() default "";
    String returning() default "";
    String argNames() default "";

}

@AfterThrowing

在方法执行过程中抛出异常后执行:也就是方法执行过程中,如果抛出异常后,才会执行此切面方法。

1
2
3
4
5
6
7
8
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterThrowing {
    String value() default "";
    String pointcut() default "";
    String throwing() default "";
    String argNames() default "";
}

参数和注解里的值必须一致。这里崩溃同样会发生,不会因为切面操作而直接 catch 住,只是在抛出异常之前会打印出异常信息而已

@Around

在方法执行前后和抛出异常时执行。

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {
    String value();
    String argNames() default "";
}

示例

  • AOP 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Aspect
class DemoAspect {
    @After("execution(* $ASPECTJ_HELPER_CLASS.test*(..))")
//    @Before("execution(* $ASPECTJ_HELPER_CLASS.test*(..))")
    fun testAspectBefore(point: JoinPoint) {
        Log.d(
            TAG,
            "--->>> ${point.signature.name}-before \n signature=${point.signature}, point=$point,\n shortStr=${point.toShortString()}"
        )
    }

    @AfterReturning(
        value = "execution(* $ASPECTJ_HELPER_CLASS.afterReturning*(..))",
        returning = "num1"
    )
    // 方法参数必须和注解中的值一致
    fun testAspectAfterReturning(num1: Int, point: JoinPoint) {
        Log.i(TAG, "--->>> afterReturning-num:$num1 $point")
    }


    @AfterThrowing(
        value = "execution(* $ASPECTJ_HELPER_CLASS.afterThrowing*(..))",
        throwing = "exception"
    )
    fun testAspectAfterThrowing(exception: Exception) {
        exception.printStackTrace()
        Log.w(TAG, "--->>> afterThrowing-exception:" + exception.message)
    }

    @Around("execution(* $ASPECTJ_HELPER_CLASS.aroundTest(..))")
    @Throws(
        Throwable::class
    )
    fun testAspectAround(point: ProceedingJoinPoint) {
        Log.e(TAG, "--->>> ${point.signature.name}-before ")
        point.proceed()
        Log.e(TAG, "--->>> ${point.signature.name}-after ")
    }

    companion object {
        const val TAG = "DemoAspect"
        private const val ASPECTJ_HELPER_CLASS =
            "me.hacket.assistant.samples.进阶.aop.aspectj.AspectJHelper"
    }
}
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AspectJHelper {

    public String testAspectJMethod() {
        Log.e("DemoAspect", "testAspectJMethod-invoke");
        return "hacket";
    }

    public int afterReturningTest() {
        Log.e(DemoAspect.TAG, "AfterReturning-invoke");
        return 10;
    }


    public void afterThrowingTest() {
        View v = null;
        v.setVisibility(View.VISIBLE);
    }

    public void aroundTest() {
        Log.e(DemoAspect.TAG, "AroundTest-invoke");
        int x = 0;
        int y = 1 / x;
    }
}

@Pointcut

@Before("execution(* me.hacket.assistant.samples.进阶.aop.aspectj.AspectJHelper.test*(..))")
我们分成几个部分依次来看:

  • @Before:Advice,具体的插入点
  • execution:处理 Join Point 的类型,例如 call、execution、withincode
    • call、execution 类似,都是插入代码的意思;区别就是 execution 是在被切入的方法中,call 是在调用被切入的方法前或者后。

// 对于 call 来说: call(Before) Pointcut { Pointcut Method } call(After) // 对于 execution 来说: Pointcut { execution(Before) Pointcut Method execution(After) }

  • withcode 通常来进行一些切入点条件的过滤,作更加精确的切入控制

withcode

假如我们想要切 test 方法,但是只希望在 test2 中调用 test 才执行切面方法,就需要用到 withincode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AspectJHelper {
    public void callTest1_2() {
        test1Hacket();
        test2Hacket();
    }

    public void testHacket() {
        Log.e(DemoAspect.TAG, "testHacket");
    }

    public void test1Hacket() {
        testHacket();
    }

    public void test2Hacket() {
        testHacket();
    }
} 
callTest1_2();
  • AOP 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在test()方法内
@Pointcut(value = "withincode(* $ASPECTJ_HELPER_CLASS.test2Hacket(..))")
fun invoke2() {
    Log.e(TAG, "-->> invoke2()")
}

// 调用test()方法的时候
@Pointcut(value = "call(* $ASPECTJ_HELPER_CLASS.testHacket(..))")
operator fun invoke() {
    Log.e(TAG, "-->> invoke()")
}

// 同时满足前面的条件,即在test2()方法内调用test()方法的时候才切入
@Pointcut("invoke() && invoke2()")
fun invokeOnlyIn2() {
    Log.w(TAG, "-->> invoke() && invoke2()")
}

@Before("invokeOnlyIn2()")
fun beforeInvokeOnlyIn2(joinPoint: JoinPoint) {
    val key = joinPoint.signature.toString()
    Log.d(TAG, "beforeInvokeOnlyIn2: $key")
}

输出:

testHacket beforeInvokeOnlyIn2: void me.hacket.assistant.samples.进阶.aop.aspectj.AspectJHelper.testHacket() testHacket

MethodPattern

这个是最重要的表达式,大致为:@注解和访问权限,返回值的类型和包名.函数名 (参数)
如:@After("execution(* me.hacket.assistant.samples.进阶.aop.aspectj.AspectJHelper.test*(..))")

  • @注解和访问权限

public/private/protect,以及 static/final:属于可选项。如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么 public,private,protect 及 static、final 的函数都会进行搜索。

  • 返回值类型

就是普通的函数的返回值类型。如果不限定类型的话,就用 * 通配符表示。

  • 包名.函数名

用于查找匹配的函数。可以使用通配符,包括和 以及 + 号。其中号用于匹配除 . 号之外的任意字符,而 则表示任意子 package,+ 号表示子类
* com.hujiang.library.demo.DemoActivity.test*(…)

切入点

匹配通配符含义

  • *:匹配任何数量字符
  • ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数
  • +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边

案例:

  • public * *(..) :任何公共方法。
  • * com..*.*(..):com 包以及所有子包下所有类的任何方法。
  • * com..Manager.*(..):com 包以及所有子包下的 Manager 类的任何方法。
  • * com.alan..*Manager.find*(..):com.alan 包以及所有子包下的以 Manager 结尾的类的以 find 开头的任何方法。

AspectJ 应用

统计 Application 中所有方法的耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
public class ApplicationAop {

    @Around("call (* com.json.chao.application.BaseApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();
    String name = signature.toShortString();
    long time = System.currentTimeMillis();
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    Log.i(TAG, name + " cost" +     (System.currentTimeMillis() - time));
    }
}
  1. 当 Action 为 Before、After 时,方法入参为 JoinPoint。当 Action 为 Around 时,方法入参为 ProceedingPoint。
  2. 而 Around 和 Before、After 的最大区别就是 ProceedingPoint 不同于 JoinPoint,其提供了 proceed 方法执行目标方法。

对 App 中所有的方法进行 Systrace 函数插桩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
public class SystraceTraceAspectj {

    private static final String TAG = "SystraceTraceAspectj";

    @Before("execution(* **(..))")
    public void before(JoinPoint joinPoint) {
        TraceCompat.beginSection(joinPoint.getSignature().toString());
    }

    @After("execution(* **(..))")
    public void after() {
        TraceCompat.endSection();
    }
}

遇到的问题

app:externalNativeBuildCleanDebug clean 循环引用

image.png
解决:删除这段代码,具体原因未知

1
2
3
4
cmake {
    path "src/main/cpp/CMakeLists.txt"
    version "3.10.2"
}

Cannot cast object TransformTask$2$1

1
Cannot cast object 'com.android.build.gradle.internal.pipeline.TransformTask$2$1@4436acbe' with class 'com.android.build.gradle.internal.pipeline.TransformTask$2$1' to class 'com.android.build.gradle.internal.pipeline.TransformTask'

解决:
在 root project 的 build.gralde 里面加入,升级到 2.0.10 版本

1
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'

在 app 的 build.gradle 里面加入

1
2
3
4
5
6
apply plugin: 'android-aspectjx'
api 'org.aspectj:aspectjrt:1.9.5'
aspectjx {
    enabled true
    exclude 'com.google','com.taobao'
}

参考:as 4.0 编译失败

java.util.zip.ZipException: zip file is empty

exclude 对应冲突的包

1
2
3
4
5
6
7
8
9
10
11
aspectjx {
    // 关闭AspectJX功能
    enabled true
    // 排除所有package路径中包含`android.support`的class文件及库(jar文件)
//    exclude 'android.support', 'androidx', 'com.google', 'com.appsflyer', 'com.android'
    exclude 'versions.9'
//    exclude 'com.squareup'
//    exclude 'leakcanary'
//    exclude 'com.taobao'
//    exclude 'com.ut'
}

升级到 2.0.1 后报错,java.lang.ClassNotFoundException

applying to join point that doesn’t return void: method-call(java.lang.String me.hacket.assistant.base.app.BaseApplication.getCurrentProcessName(android.content.Context))
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx/issues/82

本文由作者按照 CC BY 4.0 进行授权