AspectJ 学习笔记(一)

简介

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整个项目试下。

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜小徐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值