介紹:
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下替換配置插件就行了。