简介
AspectJ 作为 Java 中流行的 AOP(aspect-oriented programming) 编程扩展框架,其内部使用的是 BCEL框架 来完成其功能。调用时机是在 Java 文件编译成 .class 文件之后,生成 Dalvik 字节码之前执行。
利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。
无侵入性,修改方便。此外,AOP 不同于 OOP 将问题划分到单个模块之中,它把涉及到众多模块的同一类问题进行了统一处理。
优点
1、成熟稳定
字节码的处理并不简单,特别是 针对于字节码的格式和各种指令规则,如果处理出错,就会导致程序编译或者运行过程中出现问题。而 AspectJ 作为从 2001 年发展至今的框架,它已经发展地非常成熟,通常不用考虑插入的字节码发生正确性相关的问题。
2、使用非常简单
AspectJ 的使用非常简单,并且它的功能非常强大,我们完全不需要理解任何 Java 字节码相关的知识,就可以在很多情况下对字节码进行操控。例如,它可以在如下五个位置插入自定义的代码:
1)在方法(包括构造方法)被调用的位置。
2)在方法体(包括构造方法)的内部。
3)在读写变量的位置。
4)在静态代码块内部。
5)在异常处理的位置的前后。
此外,它也可以 直接将原位置的代码替换为自定义的代码。
缺点
1、切入点固定
AspectJ 只能在一些固定的切入点来进行操作,如果想要进行更细致的操作则很难实现,它无法针对一些特定规则的字节码序列做操作。
2、正则表达式的局限性
AspectJ的匹配规则采用了类似正则表达式的规则,比如 匹配 Activity 生命周期的onXXX方法,如果有自定义的其他以on开头的方法也会匹配到,这样匹配的正确性就无法满足。
3、性能较低
AspectJ在实现时会包装自己一些特定的类,它并不会直接把 Trace 函数直接插入到代码中,而是经过一系列自己的封装。这样不仅生成的字节码比较大,而且对原函数的性能会有不小的影响。如果想对App中所有的函数都进行插桩,性能影响肯定会比较大。如果你只插桩一小部分函数,那么AspectJ带来的性能损耗几乎可以忽略不计。
语法简介
1、横切关注点
对哪些方法进行拦截,拦截后怎么处理。
2、切面(Aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象。
3、连接点(JoinPoint)
JPoint 是一个程序的关键执行点,也是我们关注的重点。它就是指被拦截到的点(如方法、字段、构造器等等)。函数调用、获取设置变量、类初始化。
4、切入点(PointCut)
对 JoinPoint 进行拦截的定义。PointCut 的目的就是提供一种方法使得开发者能够选择自己感兴趣的 JoinPoint。
5、通知(Advice)
切入点仅用于捕捉连接点集合,但是,除了捕捉连接点集合以外什么事情都没有做。事实上实现横切行为我们要使用通知。它一般指拦截到 JoinPoint 后要执行的代码,分为 前置、后置、环绕 三种类型。这里我们需要注意 Advice Precedence(优先权)的情况,比如我们对同一个切面方法同时使用了 @Before 和 @Around 时就会报错,此时会提示需要设置 Advice 的优先级。
Before:PointCut 之前执行、After:PointCut 之后执行、Around:PointCut 之前、之后分别执行。
切入点和通知动态地影响程序流程,类型间声明则是静态的影响程序的类等级结构,而切面则是对所有这些新结构的封装。
实践
这里我们可以直接使用沪江的 AspectJX 框架:https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
环境配置
1、在项目根目录的 build.gradle
buildscript {
...
dependencies {
...
classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10"
}
}
2、在要使用AspectJ的module的build.gradle
plugins {
...
id 'android-aspectjx'
}
...
dependencies {
...
implementation "org.aspectj:aspectjrt:1.9.5"
}
简单实例
实例一
@Aspect
class FirstAspectJxInjector {
@Around("execution(* com.demo.myapplication.MainActivity.on**(..))")
@Throws(Throwable::class)
fun activitLifeFunTime(joinPoint: ProceedingJoinPoint) {
val startTime = System.currentTimeMillis()
joinPoint.proceed()
val total = System.currentTimeMillis() - startTime
Log.e("on方法时长", "${joinPoint.signature.name}结束时间:$total")
}
@After("call(* com.lsy.myapplication.MyApp.onCreate(..))")
@Throws(Throwable::class)
fun appOnCreateTime(joinPoint: JoinPoint) {
Log.e("测试日志", "测试内容")
}
}
类用@Aspect标注
第一个方法:在 execution 中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配MainActivity中以on开头的方法。打印Activity中on方法所用的时长
第二个方法:在 call中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配Application中onCreate的方法。打印日志
1)call:代表调用方法的位置,插入在函数体外面。
2)execution:代表方法执行的位置,插入在函数体内部。
@Before、@After和@Around的除了调用时机不同外,对应的参数也不同。@Before和@After的参数是JoinPoint,@Around的参数是ProceedingJoinPoint,两者的不同之处在于ProceedingJoinPoint其提供了 proceed 方法执行目标方法。
其他通知类型还有@AfterReturning和@AfterThrowing,它们和@After的区别就是方法调用结束后返回结果或异常,除JoinPoint之外再多一个参数。
实例二
//1、添加自定义注解
/**
* AnnotationRetention.SOURCE:不存储在编译后的 Class 文件。
* AnnotationRetention.BINARY:存储在编译后的 Class 文件,但是反射不可见。
* AnnotationRetention.RUNTIME:存储在编译后的 Class 文件,反射可见。
*/
/**
* AnnotationTarget.CLASS:类,接口或对象,注解类也包括在内。
* AnnotationTarget.ANNOTATION_CLASS:只有注解类。
* AnnotationTarget.TYPE_PARAMETER:Generic type parameter (unsupported yet)通用类型参数(还不支持)。
* AnnotationTarget.PROPERTY:属性。
* AnnotationTarget.FIELD:字段,包括属性的支持字段。
* AnnotationTarget.LOCAL_VARIABLE:局部变量。
* AnnotationTarget.VALUE_PARAMETER:函数或构造函数的值参数。
* AnnotationTarget.CONSTRUCTOR:仅构造函数(主函数或者第二函数)。
* AnnotationTarget.FUNCTION:方法(不包括构造函数)。
* AnnotationTarget.PROPERTY_GETTER:只有属性的 getter。
* AnnotationTarget.PROPERTY_SETTER:只有属性的 setter。
* AnnotationTarget.TYPE:类型使用。
* AnnotationTarget.EXPRESSION:任何表达式。
* AnnotationTarget.FILE:文件。
* AnnotationTarget.TYPEALIAS:@SinceKotlin("1.1") 类型别名,Kotlin1.1已可用。
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class FunTime()
/**
* 2、@Aspect修饰一个类
*/
@Aspect
class AspectJxInjector {
/**
* 3、定义切点 使用@Pointcut修饰一个用@Aspect修饰的类中的方法
*/
@Pointcut("execution(@com.demo.myapplication.start.annotation.FunTime * *(..))")
fun methodFunTime() {}
/**
* 4、关联切点与处理逻辑方法 使用@Around修饰定义的方法,参数为第三步的切点方法
*/
@Around("methodFunTime()")
@Throws(Throwable::class)
fun funTime(joinPoint: ProceedingJoinPoint) {
val startTime = System.currentTimeMillis()
joinPoint.proceed()
val total = System.currentTimeMillis() - startTime
Log.e("方法时长", "${joinPoint.toShortString()}方法时间:$total")
}
}
//5、最后需要操作的地方加上注解
@FunTime
fun print() {
Log.e("测试日志","测试内容")
}
AspectJ开发中遇到的问题
- @Around 注解的方法,无法进入断点debug,@Before、@After都可以进入断点,但是@Around却不能进入断点。
- 解决方案:新建一个library库工程,将annotation和aspect文件都放在库工程中。
- java.util.zip.ZipException: zip file is empty
- 一般来说是AspectJ注解使用不当的问题。比如@Before、@Around、@After同时存在,需要注意顺序,@Before在最前、其次是@Around,最后才是@After,不然就会出错。
- org.aspectj.weaver.bcel.BcelWeaver.weave Unknown constant type 18
- 这是因为aspect文件中使用了lambda表达式的原因。解决方案就是在aspect文件中暂时不使用lambda表达式,这个可能是org.aspectj.weaver的版本原因导致的。
- Attempt to invoke interface method ‘void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)’
- 尝试把app和library下build文件夹删除后,rebuild整个项目试下。