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下面也發現很多人提同樣的問題,都說項目不更新,建議別使用了。

最后我看到一個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下替換配置插件就行了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容