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织入器
特点:
- 使用简单:不需要了解 .class 字节码文件格式 ,在目标位置插入或替换为自定义代码;
- 成熟稳定:直接修改字节码很容易出错,导致程序无法运行。AspectJ使用了专门的编译器,基本不需要考虑生成字节码正确性的问题。
- 不受访问限制:final、static、private都可以修改
- 切入点固定:AspectJ 只能在固定的几个切入点插入,如方法调用前、方法内部、异常前后、变量修改等;
- 匹配规则:AspectJ 的匹配规则类似于正则表达式;也可以结合注解匹配;
- 使用静态织入,无法织入Android SDK。如:需要先重写生命周期方法才能被织入。
- 重复织入:同样的方法,父类和子类都会织入,除非匹配具体类型。
- 性能较低:AspectJ 插入逻辑时,会添加一些冗余代码,如果大面积使用会影响性能;
- 增加编译时间:编译时需要扫描匹配规则,插入代码。——可以排除不需要扫描的包,匹配规则尽可能具体,减少匹配时间。配置只在debug/release环境生效
- 多个切点匹配到同一个方法,需要关注通知优先级。
AspectJ基本使用
具体语法可参考AspectJ官方文档、文档
两种用法:
- 创建aj文件,使用AspectJ的语言(语法类似java,多了一些关键字)定义切点和通知(即切面),需要使用ajc进行编译
- 使用纯Java语言开发,通过AspectJ进行注解,简称@AspectJ,也要通过ajc进行编译
从切点匹配角度看,有两种方式
- 非侵入式:通过关键字匹配目标连接点
- 不需要修改连接点代码
- 难以精确控制切入点
- 如果有10个方法,分别需要检查不同权限,我们需要定义10个切点和通知
- 如果方法名和路径变了,需要检查和修改切面
- 侵入式:通过自定义注解找到目标连接点。
- 需要在连接点处加入注解
- 不需要修改切面代码,即可修改切点。
- 如果运行时需要读取注解参数,则使用
RetentionPolicy.RUNTIME
,否则可以使用RetentionPolicy.CLASS
。
切点表达式
- *:匹配任何数量字符;
- ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
- +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
可以使用 且(&&)、或(||)、非(!)来组合切入点表达式。
切点定义有两种写法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Before("execution(* *..*Activity.on*(..))") public void beforLifecycle(JoinPoint joinPoint) { }
@Pointcut("execution(* *..*Activity.on*(..))") public void onLifecycle() { } @Before("onLifecycle") public void beforLifecycle(JoinPoint joinPoint) { }
|
直接选择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区别
可以反编译对比
- Before和After通知:在匹配到的JoinPoint前后插入代码
- Around通知:使用代理+闭包进行替换。将原方法体放到一个闭包(
AroundClosure
)中,通过调用ProceedingJoinPoint.proceed
方法执行原逻辑。(可以调用set$AroundClosure替换闭包,即修改原逻辑)
高级用法
- 类加载时Hook
- 对项目中的三方库进行Hook
- 增加接口实现、添加成员变量等
Android中引入AspectJ
大部分资料和框架(如Hugo、AspectJX等)都比较老,AspectJ可能不够主流,更多用在Spring中。因此可能存在版本兼容问题(如Gradle版本、JDK版本 Lambda、R8等)。
AspectJ需要使用ajc编译器处理,因此要配置加入Android构建流程中
方式一
- 根目录
build.gradle
添加AspectJ Gradle插件
1 2 3
| classpath 'org.aspectj:aspectjtools:1.8.9' classpath 'org.aspectj:aspectjweaver:1.8.9'
|
- 模块
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 { 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 } } } }
|
- 添加@AspectJ依赖库:
implementation 'org.aspectj:aspectjrt:1.8.9'
,包含@AspectJ注解
- 使用注解定义切面
可以查看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";
@Pointcut("execution(* *..*Activity.on*(..))") public void onLifecycle() { }
@Before("onLifecycle()") public void beforLifecycle(JoinPoint joinPoint) { Log.e(TAG, "[" + joinPoint.getSourceLocation() + "] " + joinPoint.getSignature().getName()); } }
|
存在问题:
- 由于AspectJ使用静态织入,无法检测到Android SDK中的方法,因此只会对Activity中重写的生命周期生效
- on开头的不一定是生命周期方法
- 重复织入:如果定义BaseActivity并且继承的话,会打印两遍生命周期
解决方法:
- 定义BaseActivity,以 BaseActivity 作为切入点。如:
execution(* BaseActivity.on**(..))
- 通过Application.ActivityLifecycleCallbacks监听生命周期的变化。
对APP中所有方法进行Systrace函数插桩,用于分析性能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Aspect public class DebugTraceAspect { @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";
@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()); }
@AfterThrowing(value = "execution(* *(..))", throwing = "e") public void afterThrowing(JoinPoint joinPoint, Exception e) { Log.e(TAG, "afterThrowing: "); } }
|
参数校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Aspect public class CheckArgsAspect { private static final String TAG = "CheckArgsAspect";
@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
|
@Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface CheckLogin { }
@Aspect public class CheckLoginAspect { private static final String TAG = "CheckLoginAspect";
@Around("execution(@CheckLogin * *(..))") public void checkLogin(ProceedingJoinPoint joinPoint) { 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
|
@Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface DebugTrace { }
@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); } }
|
权限检查和动态申请
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
|
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface CheckPermissions { String[] value(); }
@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) { 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()) { 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
|
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface BindView { int value(); }
@Aspect class BindViewAspect { @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
|
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ThrottleClick { int duration() default 500; }
@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(); } } } }
|