Kotlin Aspect入门篇
介绍:
AspectJ是一个面向切面编程的一个框架,它扩展了java语言,并定义了实现AOP的语法。
在将.java文件编译为.class文件时默认使用javac编译工具,AspectJ会有一套符合java字节码编码规范的编译工具来替代javac;
在将.java文件编译为.class文件时,会动态的插入一些代码来做到对某一类特定东西的统一处理。
通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的技术。
对业务逻辑的各个部分进行隔离,耦合度降低,提高程序的可重用性,同时提高了开发的效率。
OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分;
AOP针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果;主要用途有:日志记录,行为统计,安全控制,事务处理,异常处理,系统统一的认证、权限管理等。
AspectJ的配置很麻烦,这里使用 AspectJX 框架,框架地址:https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
首先通过一个简单的测试方法展开流程。
常用的埋点统计,在我们需要自己去实现埋点统计的时候,会写个埋点工具类,然后在每个入口的地方去添加埋点的方法,如果埋点较多会感觉很烦。
用AspectJ怎么去实现,抛开业务逻辑,一切从简:
首先添加依赖:
dependencies {
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
}
plugins {
id "com.android.application"
id "kotlin-android"
id "kotlin-kapt"
id "android-aspectjx"
}
//aspectjx
implementation "org.aspectj:aspectjrt:${Versions.aspectjx}"
接着需要定义一个测试方法,testAspect,这个方法就是触发点,调用了这个方法,就统计一下。
该方法不做任何逻辑处理。
/** 切面测试方法 */
fun testAspect() {}
重点是切面类,AspectUtils,这个类需要用 @Aspect 声明为标记类。
然后开始编写测试方法。
/**
@Pointcut("execution(" +//执行语句
"@com.kotlinstrong.utils.aspect.MyAnnotationOnclick" +//注解筛选
"*" + //类路径,*为任意路径
"*" + //方法名,*为任意方法名
"(..)" +//方法参数,'..'为任意个任意类型参数
")" +
" && " +//并集
)
@Aspect:声明切面,标记类
@Pointcut(切点表达式):定义切点,标记方法
@Before(切点表达式):前置通知,切点之前执行
@Around(切点表达式):环绕通知,切点前后执行
@After(切点表达式):后置通知,切点之后执行
@AfterReturning(切点表达式):返回通知,切点方法返回结果之后执行
@AfterThrowing(切点表达式):异常通知,切点抛出异常时执行
* */
/** 切面工具类 */
@Aspect
class AspectUtils {
@Before("execution(* com.strong.ui.home.HomeFragment.test*(..))")
fun testAspectBefore(point: JoinPoint) {
LogUtils.d("AspectUtils ${point.signature.name} ---testAspectBefore")
}
}
可以看出逻辑,测试方法是test开头的,before是在方法执行前切入,在onCreate里面调用test方法。
假如要根据方法的返回值来添加逻辑定义埋点,再写一个测试方法,返回一个数值。
fun testAfterReturning(): Int {
return 666
}
在工具类里面需要加一个方法,从上面的注释可以看到,有返回值的需要用 @AfterReturning 注解标注。
@AfterReturning("execution(* com.strong.ui.home.HomeFragment.test*(..))", returning = "id")
fun testAspectAfterReturning(point: JoinPoint, id: Int) {
LogUtils.d("AspectUtils ${point.signature.name} ---testAspectAfterReturning $id")
}
定义了一个返回参数id,这里要注意,注解上的returning标注的参数名称 id 要跟下面申明的参数名称一致。
运行一波。
埋点方便多了,只要在需要执行的方法处匹配就能自动切入埋点,自成一类。
根据方法名称去匹配切入,但是如果修改了方法名,工具类也要跟着修改,显然存在依赖。
这里可以自定义注解去解决。
刚好我们平时跳转页面会做一个比较频繁的操作,防抖,这里里用防抖来测试。
在一个列表中,添加item点击事件,点击后跳转详情页,如果快速点击,会进入多次,这显然不是我们想要的效果。
那么自定义一个注解,然后在跳转的方法上添加注解,在切面类编写防抖触发,点击间隔设置为1000毫秒。
自定义注解MyAnnotationOnclick。
/* 防抖点击 */
/**
* SOURCE:运行时 不存储在编译后的 Class 文件。
* BINARY:加载时 存储在编译后的 Class 文件,但是反射不可见。
* RUNTIME:编译时 存储在编译后的 Class 文件,反射可见。
*/
@Retention(AnnotationRetention.RUNTIME)
/**
* CLASS:类,接口或对象,注解类也包括在内。
* ANNOTATION_CLASS:只有注解类。
* TYPE_PARAMETER:Generic type parameter (unsupported yet)通用类型参数(还不支持)。
* PROPERTY:属性。
* FIELD:字段,包括属性的支持字段。
* LOCAL_VARIABLE:局部变量。
* VALUE_PARAMETER:函数或构造函数的值参数。
* CONSTRUCTOR:仅构造函数(主函数或者第二函数)。
* FUNCTION:方法(不包括构造函数)。
* PROPERTY_GETTER:只有属性的 getter。
* PROPERTY_SETTER:只有属性的 setter。
* TYPE:类型使用。
* EXPRESSION:任何表达式。
* FILE:文件。
* TYPEALIAS:@SinceKotlin("1.1") 类型别名,Kotlin1.1已可用。
*/
@Target(AnnotationTarget.FUNCTION)
annotation class MyAnnotationOnclick(
/** 点击间隔时间 */
val value: Long = 1000
)
然后在工具类中添加具体逻辑。
/*
* 定义切点,标记切点为所有被@AopOnclick注解的方法
* @+注解全路径
*/
@Pointcut("execution(@com.strong.utils.aspect.MyAnnotationOnclick * *(..))")
fun methodAnnotated(){}
识别注解标识的方法,告诉代码,在何处注入一段特定带条件的代码的表达式,此处的条件就是我们定义的注解,然后在编写我们的切入方法。
/*
* 定义一个切面方法,包裹切点方法
* ProceedingJoinPoint:继承自JoinPoint,为了支持Around注解,其他的几种切面只需要用到JoinPoint
*/
@Around("methodAnnotated()")
@Throws(Throwable::class)
fun aroundJoinPoint(joinPoint: ProceedingJoinPoint) {
LogUtils.d("AspectUtils ${joinPoint.signature.name} put aroundJoinPoint")
// 取出方法的注解,返回连接点处的签名
val methodSignature = joinPoint.signature as MethodSignature
val method = methodSignature.method
//判断注释是否在method上
if (!method.isAnnotationPresent(MyAnnotationOnclick::class.java)) {
return
}
val aopOnclick = method.getAnnotation(MyAnnotationOnclick::class.java)
// 判断是否快速点击
if (!ClickUtils.isFastDoubleClick(aopOnclick.value)) {
// 执行原方法
joinPoint.proceed()
}
}
该方法判断了是否连续点击,然后才会执行具体测试方法。
这里用到了一个自定义ClickUtils点击工具类。
object ClickUtils {
/**
* 最近一次点击的时间
*/
private var mLastClickTime: Long = 0
/**
* 是否是快速点击
*
* @param intervalMillis 时间间期(毫秒)
* @return true:是,false:不是
*/
fun isFastDoubleClick(intervalMillis: Long): Boolean {
// long time = System.currentTimeMillis();
val time: Long = SystemClock.elapsedRealtime()
val timeInterval = abs(time - mLastClickTime)
return if (timeInterval < intervalMillis) {
true
} else {
mLastClickTime = time
false
}
}
}
接着在列表点击事件中添加防抖注解;
继续,说到切面编程就不得不说登录了。
一般app就是两种方式,先登录在进入,先进入,在检查让你登录。
那么如果是先进入,然后开始浏览,在判断让用户登录,就需要在多个地方去判断了,这里也一样,可以用Aspect去做,简化原本的逻辑。
一样的操作,定义一个注解。
/* 检测登录 */
/**
* SOURCE:运行时 不存储在编译后的 Class 文件。
* BINARY:加载时 存储在编译后的 Class 文件,但是反射不可见。
* RUNTIME:编译时 存储在编译后的 Class 文件,反射可见。
*/
@Retention(AnnotationRetention.RUNTIME)
/**
* CLASS:类,接口或对象,注解类也包括在内。
* ANNOTATION_CLASS:只有注解类。
* TYPE_PARAMETER:Generic type parameter (unsupported yet)通用类型参数(还不支持)。
* PROPERTY:属性。
* FIELD:字段,包括属性的支持字段。
* LOCAL_VARIABLE:局部变量。
* VALUE_PARAMETER:函数或构造函数的值参数。
* CONSTRUCTOR:仅构造函数(主函数或者第二函数)。
* FUNCTION:方法(不包括构造函数)。
* PROPERTY_GETTER:只有属性的 getter。
* PROPERTY_SETTER:只有属性的 setter。
* TYPE:类型使用。
* EXPRESSION:任何表达式。
* FILE:文件。
* TYPEALIAS:@SinceKotlin("1.1") 类型别名,Kotlin1.1已可用。
*/
@Target(AnnotationTarget.FUNCTION)
annotation class MyAnnotationLogin
工具类里写切面表达式跟方法,这里简化实现效果,具体登录逻辑更复杂。
@Pointcut("execution(@com.strong.utils.aspect.MyAnnotationLogin * *(..))")
fun methodLogin(){}
@Around("methodLogin()")
@Throws(Throwable::class)
fun aroundLoginPoint(joinPoint: ProceedingJoinPoint) {
ToastUtils.showShort("login.json")
//此处判断是否登录,如果没有不执行方法,跳转到登录,如果已经登录只执行原方法
joinPoint.proceed()
}
在打开详情页的时候触发,如果没有登录的话,直接跳转到登录页面。
点击详情,会发现打开了一个详情后再次打开了一个登录页面,但是单纯的从方法上看又什么逻辑都没有。
对于这些情况的处理,Aspect 无疑很方便。
这里有一份注解说明,可以大致对照使用。
/**
@Pointcut("execution(" +//执行语句
"@com.strong.utils.aspect.MyAnnotationOnclick" +//注解筛选
"*" + //类路径,*为任意路径
"*" + //方法名,*为任意方法名
"(..)" +//方法参数,'..'为任意个任意类型参数
")" +
" && " +//并集
)
@Aspect:声明切面,标记类
@Pointcut(切点表达式):定义切点,标记方法,告诉代码注入工具,在何处注入一段特定代码的表达式
@Before(切点表达式):前置通知,切点之前执行
@Around(切点表达式):环绕通知,切点前后执行
@After(切点表达式):后置通知,切点之后执行
@AfterReturning(切点表达式):返回通知,切点方法返回结果之后执行
@AfterThrowing(切点表达式):异常通知,切点抛出异常时执行
**/
小扩展
老规矩,Android 坑多是老惯例了。
在kotlin下aspectjx包重复,各种zip is empty。
我想很多使用过的对这个异常很熟悉。
当时我也曾找了很多方案,无非就是更新版本,排除或者去重,还有的说把插件名称换了,更改为com.github.franticn:gradle_plugin_android_aspectjx,但是没有用。
最后我看到一个issues下面有人给出解决方案。
该作者提到,因为 kotlin 和 java 互调时,aspectjx 会重复备份且备份过程不是追加而是覆盖,导致 zip file is empty 异常。
而解决方案中也很直接,作者自己写了一个 aspectj 插件,支持 kotlin 和 java 混编。
所以我这里直接用他的方案。
//classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:${Versions.aspectjx_plugin}"
classpath "com.github.2017398956:AspectPlugin:${Versions.aspectjx_plugin}"
还有对应引入的插件也一样,把之前的替换就行了。
plugins {
id "com.android.application"
id "kotlin-android"
id "kotlin-kapt"
//id "android-aspectjx"
id "AspectPlugin"
}
只需要在build.gradle配置文件中的dependencies下替换配置插件就行了。