0%

AspectJ介绍和示例

AspectJ介绍

介绍:一个字节码处理框架,实现AOP的工具。

原理:使用专门的编译器(ajc,可以执行ajc命令编译,也可以通过java执行)操作Class字节码,内部使用BCEL框架

由三部分组成:

  • aspectjtools:包含ajc编译器
  • aspectjweaver:织入器,包含切点表达式处理,供ajc使用。并提供了javaagent用于类加载时期织入代码
  • aspectjrt:包含@AspectJ注解

三种织入时机(参考AspectJ使用介绍):

  • compile-time:编译期织入,处理Java源文件,使用ajc编译期替代javac编译器
  • post-compile:编译后织入,处理class字节码文件,如增强三方库中的方法
  • load-time:在 JVM 进行类加载的时候进行织入,使用wearver织入器

特点:

  1. 使用简单:不需要了解 .class 字节码文件格式 ,在目标位置插入或替换为自定义代码;
  2. 成熟稳定:直接修改字节码很容易出错,导致程序无法运行。AspectJ使用了专门的编译器,基本不需要考虑生成字节码正确性的问题。
  3. 不受访问限制:final、static、private都可以修改
  4. 切入点固定:AspectJ 只能在固定的几个切入点插入,如方法调用前、方法内部、异常前后、变量修改等;
  5. 匹配规则:AspectJ 的匹配规则类似于正则表达式;也可以结合注解匹配;
  6. 使用静态织入,无法织入Android SDK。如:需要先重写生命周期方法才能被织入。
  7. 重复织入:同样的方法,父类和子类都会织入,除非匹配具体类型。
  8. 性能较低:AspectJ 插入逻辑时,会添加一些冗余代码,如果大面积使用会影响性能;
  9. 增加编译时间:编译时需要扫描匹配规则,插入代码。——可以排除不需要扫描的包,匹配规则尽可能具体,减少匹配时间。配置只在debug/release环境生效
  10. 多个切点匹配到同一个方法,需要关注通知优先级。

AspectJ基本使用

具体语法可参考AspectJ官方文档文档

两种用法:

  1. 创建aj文件,使用AspectJ的语言(语法类似java,多了一些关键字)定义切点和通知(即切面),需要使用ajc进行编译
  2. 使用纯Java语言开发,通过AspectJ进行注解,简称@AspectJ,也要通过ajc进行编译

从切点匹配角度看,有两种方式

  1. 非侵入式:通过关键字匹配目标连接点
    1. 不需要修改连接点代码
    2. 难以精确控制切入点
    3. 如果有10个方法,分别需要检查不同权限,我们需要定义10个切点和通知
    4. 如果方法名和路径变了,需要检查和修改切面
  2. 侵入式:通过自定义注解找到目标连接点。
    1. 需要在连接点处加入注解
    2. 不需要修改切面代码,即可修改切点。
    3. 如果运行时需要读取注解参数,则使用RetentionPolicy.RUNTIME,否则可以使用RetentionPolicy.CLASS

切点表达式

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

可以使用 且(&&)、或(||)、非(!)来组合切入点表达式。

切点定义有两种写法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//写法1:切点和Advice一起定义
@Before("execution(* *..*Activity.on*(..))")
public void beforLifecycle(JoinPoint joinPoint) {
//do something
}
//写法2:先定义切点,Advice关联切点,可以复用切点
@Pointcut("execution(* *..*Activity.on*(..))")
public void onLifecycle() {
}
@Before("onLifecycle")
public void beforLifecycle(JoinPoint joinPoint) {
//do something
}

直接选择JoinPoint

匹配方法Signature信息。切点和连接点对应选择条件如下

Joint Point Pointcuts 表达式
Method call call(MethodSignature)
Method execution execution(MethodSignature)
Constructor call call(ConstructorSignature)
Constructor execution execution(ConstructorSignature)
Class initialization staticinitialization(TypeSignature)
Field get get(FieldSignature)
Field set set(FieldSignature)
Exception Handler,try-catch中的catch代码块 handler(TypeSignature)
Object initialization initialization(ConstructorSignature)
Object pre-initialization preinitialization(ConstructorSignature)

![](2021-12-02-AspectJ介绍和示例/AspectJ切点类型.png)

间接选择JoinPoint

除了上面与 Join Point 对应的选择外,Pointcuts 还有其他选择方法。如选择某个类中所有的JPoint、某个方法执行过程中包含的JPoint、满足某些条件的JPoint等

Pointcuts 表达式 说明
within(TypePattern) TypePattern标识类或者包,表示在某个包或者类中的所有JPoint
withincode(MethodPattern|ConstructorPattern) 在方法/构造方法执行过程中涉及到的JPoint
cflow(Pointcut) call flow,调用切点方法时所包含的JPoint,包括切点本身
cflowbelow(Pointcut) 调用切点方法时所包含的JPoint,不包括切点本身
this(Type) JPoint 所属的 this 对象是否是Type类型
target(Type) JPoint 所在的对象(例如 call 或 execution 操作符应用的对象)是否是Type类型
args(Type, …) JPoint方法或构造函数参数的类型
if(BooleanExpression) 满足表达式的 Join Point,表达式只能使用静态属性、Pointcuts 或 Advice 暴露的参数、thisJoinPoint 对象

![](2021-12-02-AspectJ介绍和示例/间接选择JPoint.png)

this和target区别

  • this:指织入代码所属类的实例对象(织入代码的地方)
  • target:指切入点方法的所有者(方法定义的地方)

切点为call情况下,织入代码的地方(方法调用的地方)和方法的所有者(方法定义的地方)不一样

切点为execution的情况下,this=target

call和execution区别

可以反编译对比

  • call:代表调用方法的位置,插入在函数体外面。
  • execution:代表方法执行的位置,插入在函数体内部。

Before、After和Around区别

可以反编译对比

  1. Before和After通知:在匹配到的JoinPoint前后插入代码
  2. Around通知:使用代理+闭包进行替换。将原方法体放到一个闭包(AroundClosure)中,通过调用ProceedingJoinPoint.proceed方法执行原逻辑。(可以调用set$AroundClosure替换闭包,即修改原逻辑)

高级用法

  1. 类加载时Hook
  2. 对项目中的三方库进行Hook
  3. 增加接口实现、添加成员变量等

Android中引入AspectJ

大部分资料和框架(如Hugo、AspectJX等)都比较老,AspectJ可能不够主流,更多用在Spring中。因此可能存在版本兼容问题(如Gradle版本、JDK版本 Lambda、R8等)。

AspectJ需要使用ajc编译器处理,因此要配置加入Android构建流程中

方式一

  1. 根目录build.gradle添加AspectJ Gradle插件
1
2
3
//根目录build.gradle
classpath 'org.aspectj:aspectjtools:1.8.9' //包含ajc编译器
classpath 'org.aspectj:aspectjweaver:1.8.9' //包含切点表达式处理,供ajc使用。并提供了javaagent用于类加载时期织入代码
  1. 模块build.gradle添加ajc编译
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
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return
}
JavaCompile javaCompile = variant.javaCompileProvider.get()
javaCompile.doLast {
//在Java编译完之后执行
print("——————————ajc start——————————")
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
}
}
}
}
  1. 添加@AspectJ依赖库:implementation 'org.aspectj:aspectjrt:1.8.9',包含@AspectJ注解
  2. 使用注解定义切面

可以查看AspectJ构建时的打印,会提示匹配规则语法错误,或者未匹配到切点等,用于排查错误

方式二

自定义Gradle插件,将ajc编译脚本进行封装,使用apply plugin即可插入构建流程

插件编写可以参考:

基于AspectJ实现的框架:(可以参考AOP应用场景和实现)

  • SAF-AOP:依赖沪江AspectJX。提供子线程切换,Log打印,方法Hook、捕获异常、追踪方法耗时、动态申请权限等功能

配置只在Debug环境生效

  • 如果是三方库,可以使用debugImplementation依赖
  • 如果是自己写编译脚本,可以判断buildType,不执行ajc编译

AspectJ示例

只列出切面方法,测试代码均在AspectJActivity中。

代码已上传至仓库

打印生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
public class LifecycleAspect {
private static final String TAG = "LifecycleAspect";

//定义切点:匹配Activity中on开头的方法
@Pointcut("execution(* *..*Activity.on*(..))")
public void onLifecycle() {
}

//在切点之前执行
@Before("onLifecycle()")
public void beforLifecycle(JoinPoint joinPoint) {
Log.e(TAG, "[" + joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName());
}
}

存在问题:

  1. 由于AspectJ使用静态织入,无法检测到Android SDK中的方法,因此只会对Activity中重写的生命周期生效
  2. on开头的不一定是生命周期方法
  3. 重复织入:如果定义BaseActivity并且继承的话,会打印两遍生命周期

解决方法:

  1. 定义BaseActivity,以 BaseActivity 作为切入点。如:execution(* BaseActivity.on**(..))
  2. 通过Application.ActivityLifecycleCallbacks监听生命周期的变化。

对APP中所有方法进行Systrace函数插桩,用于分析性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
public class DebugTraceAspect {
//Around和Before、After匹配到同一个目标的时候,Around切点需要放到前面
//匹配所有方法
@Before("execution(* **(..))")
public void beginTrace(JoinPoint joinPoint) {
Log.e(TAG, "beginTrace: " + "[" + joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName());
Trace.beginSection(joinPoint.getSignature().toString());
}

@After("execution(* **(..))")
public void endTrace(JoinPoint joinPoint) {
Log.e(TAG, "endTrace: " + "[" + joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName());
Trace.endSection();
}
}

监测和捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Aspect
public class ExceptionAspect {
private static final String TAG = "ExceptionAspect";

//原理:匹配代码中的try-catch块,且catch的类型要一样,否则无法匹配。
//比如切点表达式使用Exception,如果代码中catch的是NullPointerException,则无法匹配到。
//或者使用通配符
@Pointcut("handler(java.lang.*Exception) && args(e)")
public void onException(Exception e) {
}

@Before(value = "onException(e)", argNames = "e")
public void handleExceptionBefore(JoinPoint joinPoint, Exception e) {
Log.e(TAG, "handleExceptionBefore: " + "[" + joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName() + " " + e.getClass().getName());
}

//原理:在目标位置加try-catch,catch之后先执行方法,再throw抛异常。并不会阻止程序崩溃
@AfterThrowing(value = "execution(* *(..))", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
Log.e(TAG, "afterThrowing: ");
}
//如果要捕获异常,可以使用注解,在指定方法的Around中加try-catch
}

参数校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//1. 校验方法参数是否为空,为空则不执行
@Aspect
public class CheckArgsAspect {
private static final String TAG = "CheckArgsAspect";

//匹配有一个String类型参数的方法,可以使用..匹配多参数
//Advice方法中参数名称需要和args表达式中名称一样
@Around("execution(* *(String)) && args(arg)")
public void checkArgs(ProceedingJoinPoint joinPoint, String arg) {
Log.e(TAG, "checkArgs: " + arg + " [" + joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName());
if (arg != null && !arg.isEmpty()) {
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} else {
Toast.makeText(((Context) joinPoint.getThis()), "参数为空", Toast.LENGTH_SHORT).show();
}
}
}

登录校验

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
//1. 目标:对@CheckLogin注解声明的方法进行登录校验
//2. 定义@CheckLogin注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface CheckLogin {
}
//3. 定义切面
@Aspect
public class CheckLoginAspect {
private static final String TAG = "CheckLoginAspect";

@Around("execution(@CheckLogin * *(..))")
public void checkLogin(ProceedingJoinPoint joinPoint) {
//Aspect中只处理简单的逻辑,复杂的功能交给专业模块处理,Aspect只负责拦截收集信息
if (Tools.checkLogin()) {
Log.e(TAG, "checkLogin: 已登录");
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} else {
//或者直接跳转登录页面
Log.e(TAG, "checkLogin: 未登录");
Toast.makeText(((Context) joinPoint.getThis()), "请登录...", Toast.LENGTH_SHORT).show();
}
}
}

统计特定方法耗时

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
//1. 目标:打印@DebugTrace注解方法的耗时
//2. 定义DebugTrace注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface DebugTrace {
}
//3. 定义切面
@Aspect
public class DebugTraceAspect {
private static final String TAG = "DebugTraceAspect";

@Around("execution(@DebugTrace * *(..))")
public void trace(ProceedingJoinPoint joinPoint) {
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
long duration = System.currentTimeMillis() - time;
Log.e(TAG, "["+joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName() + "方法耗时: " + duration);
}
}
//输出示例
//E/DebugTraceAspect: [MainActivity.java:34] clickEvent方法耗时: 1000

权限检查和动态申请

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
//1. 目标:@CheckPermissions注解声明的方法需要检查权限
//2. 定义CheckPermission注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckPermissions {
String[] value();
}
//3. 定义切面
@Aspect
public class CheckPermissionsAspect {
private static final String TAG = "CheckPermissionAspect";

@RequiresApi(api = Build.VERSION_CODES.M)
@Around("execution(@CheckPermissions * *(..)) && @annotation(annotation)")
public void checkPermission(ProceedingJoinPoint joinPoint, CheckPermissions annotation) {
//复杂逻辑可以抽出去,如使用PermissionUtil
Log.e(TAG, "checkPermission: " + "[" + joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName() + annotation);
String[] permissions = annotation.value();
if (permissions.length > 0) {
Activity activity = (Activity) joinPoint.getTarget();
List<String> deniedPermission = new ArrayList<>();
for (int i = 0; i < permissions.length; i++) {
if (activity.checkSelfPermission(permissions[i]) == PackageManager.PERMISSION_DENIED) {
deniedPermission.add(permissions[i]);
}
}
if (!deniedPermission.isEmpty()) {
//动态申请权限,需要在onRequestPermissionsResult中检查是否授权成功。也可以通过切入的方式检查
activity.requestPermissions(permissions, 0x1);
} else {
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}
}

自动findViewById

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1. 确定目标:@BindView注解声明的变量使用的时候自动findViewById
//2. 定义BindView注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
//3. 定义切面
@Aspect
class BindViewAspect {
//切点为@BindView注解声明的变量的get方法,传入annotation参数
@Around("get(@BindView * *) && @annotation(annotation)")
public View bindViewById(JoinPoint joinPoint, BindView annotation) {
return ((Activity) joinPoint.getTarget()).findViewById(annotation.value());
}
}

点击事件节流

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
//1. 目标:使用@ThrottleClick注解声明的方法一段时间内只会触发一次
//2. 定义@ThrottleClick注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ThrottleClick {
//支持设置间隔时间
int duration() default 500;
}
//3. 定义切面
@Aspect
public class ThrottleAspect {
//保存上一次点击时间
private long lastTime = 0L;

@Around("execution(@ThrottleClick * *(..)) && @annotation(annotation)")
public void handleThrottle(ProceedingJoinPoint joinPoint, ThrottleClick annotation) {
int duration = annotation.duration();
//超过间隔时间才可触发点击事件
if (System.currentTimeMillis() - lastTime > duration) {
try {
joinPoint.proceed();
lastTime = System.currentTimeMillis();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}

欢迎关注我的其它发布渠道