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。
 
切点表达式
- *:匹配任何数量字符;
- ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
- +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
可以使用 且(&&)、或(||)、非(!)来组合切入点表达式。
切点定义有两种写法,如下:
| 12
 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) | 

间接选择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 对象 | 

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插件
| 12
 3
 
 | classpath 'org.aspectj:aspectjtools:1.8.9'
 classpath 'org.aspectj:aspectjweaver:1.8.9'
 
 | 
- 模块build.gradle添加ajc编译
| 12
 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.IMessageimport 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中。
代码已上传至仓库
打印生命周期
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | @Aspectpublic 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函数插桩,用于分析性能
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | @Aspectpublic 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();
 }
 }
 
 | 
监测和捕获异常
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | @Aspectpublic 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: ");
 }
 
 }
 
 | 
参数校验
| 12
 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();
 }
 }
 }
 
 | 
登录校验
| 12
 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();
 }
 }
 }
 
 | 
统计特定方法耗时
| 12
 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);
 }
 }
 
 
 
 | 
权限检查和动态申请
| 12
 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
| 12
 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());
 }
 }
 
 | 
点击事件节流
| 12
 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();
 }
 }
 }
 }
 
 |