1. 程式人生 > >動手寫一個AspectJ的gradle外掛

動手寫一個AspectJ的gradle外掛

越是深入學習 Android ,就越發感覺到 Gradle 這個構建工具十分強大, Android 外掛化都是依賴於 Gradle ,因此有必要學會怎麼用 Gradle 來編寫外掛,從而進一步去理解 Gradle 的自動化構建過程。

由於我同時對 AspectJ 十分感興趣 ,這裡就總結一下我是如何把 AspectJ 做成一個 Gradle 外掛 的過程。

Gradle 外掛開發

首先新建一個 Java Library Module,然後手動將工程結構修改為 Groovy 工程結構,也就是將原來的 main 資料夾下的 java 資料夾修改為 groovy

這裡寫圖片描述

同時在 Module 裡面的 build.gradle 中引入外掛開發所需的 gradlegroovy 這兩個 SDK 的依賴,並把 Module 外掛修改為 groovymaven

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk
}

//這裡建議使用 Java7
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
repositories { jcenter() }

這裡也沒有什麼地方需要特別注意的,畢竟要弄成本地外掛,所以需要宣告 maven ,還有就是建議這裡宣告 Java 的版本為 1.7 版本,雖說我電腦安裝的環境是 Java 1.8 ,但是發現宣告為 1.8 會經常出現編譯問題,因此建議這裡就宣告 1.7 吧。

然後就是在 main 目錄下面增加 resources/META-INF/gradle-plugins 這樣的資料夾目錄結構:

這裡寫圖片描述

暫時就先讓最後的 gradle-plugins 資料夾空著,等下再做處理,那麼現在整個 Module 的目錄如下:

這裡寫圖片描述

這樣,一個基於 Groovy

Gradle 外掛 Module 就完成了,我們可以來建立一個 groovy 檔案來玩耍了。

在包名下新建一個檔案,然後手動將檔案字尾名修改為 groovy ,是的,你沒有看錯,就是這麼手動去建立的,目前還沒發現有其他什麼法子可以讓 Android Studio 來自動建立 groovy 檔案,就先這麼幹吧:

這裡寫圖片描述

新建立的 groovy 需要實現 org.gradle.api.Plugin 接口才可以編譯成 Gradle 外掛,如下所示:

import org.gradle.api.Plugin
import org.gradle.api.Project

public class AspectjPlugin implements Plugin<Project> {

    void apply(Project project) {
       println("=============")
       println("hello world!")
       println("=============")
    }
}

一開始就不需要做什麼,就只需要和 Gradle 外掛打個招呼。

還記得之前的 resources\META-INF\gradle-plugins 路徑嗎?現在我們需要在裡面建立一個 .properties 檔案了,而檔名就是這個外掛的 id 。如下所示:

這裡寫圖片描述

然後需要在 aspectj.properties 檔案裡面指向我們的 groovy 檔案:

implementation-class=com.fritz.groovy.plugin.AspectjPlugin

這時候已經自定義好了外掛,接下來就是要打包到本地或遠端倉庫了,我們在 build.gradle 裡面新增下面的程式碼:

//生成外掛的包名
group='com.fritz.plugin'
//外掛的版本號
version='1.0.0'

//外掛每次修改都需要重新打包
uploadArchives {
    repositories {
        mavenDeployer {
            //提交到遠端伺服器
            // repository(url: "http://www.xxx.com/xxx") {
            //    authentication(userName: "admin", password: "admin")
            // }
            //本地倉庫的地址設定為專案根目錄的:/repos
            repository(url: uri('../repo'))
        }
    }
}

等待 Gradle 重新編譯完成後,就可以看到右邊的 Gradle 構建列表裡面這個命令了:

這裡寫圖片描述

點選執行,即可生成本地的 Gradle 外掛檔案:

這裡寫圖片描述

可以看到本地已經生成了 jar 的外掛檔案了,其中 \com\fritz\plugin 是有 group 指定的,第二個 plugin 就是生成外掛的模組名。

開啟壓縮包就可以看到裡面的編輯生成檔案:

這裡寫圖片描述

現在外掛已經生成了,是時候使用它了,到專案的根目錄下的 build.gradle 裡面新增本地外掛的依賴路徑:

buildscript {

    repositories {
        google()
        jcenter()
        //新增本地倉庫的路徑
        maven{
            url uri('repo')
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:3.0.1"
        //這裡就是外掛所在的包名:生成外掛的模組名:外掛的版本號
        classpath 'com.fritz.plugin:plugin:1.0.0'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

這時候就可以在任意 Module 裡面使用剛才定義的外掛了,到 App Module 裡面使用定義的外掛:

apply plugin: 'com.android.application'
//這裡新增外掛,aspectj 就是外掛的 id
apply plugin: 'aspectj'

添加了之後,開啟 Android StudioMessage 窗口裡面的 Gradle Console 視窗,執行一個 clean 操作,就可以看到下面的輸出了:

這裡寫圖片描述

完成 AspectJ 外掛

前面已經建立一個簡單的 Gradle 外掛的模型了,現在就要在它的基礎上面,完成一個 AspectJ 的外掛,而通過 AspectJ 完成 AOP 程式設計。

(PS: 不瞭解 AOP 的朋友可以看這篇文章 https://www.jianshu.com/p/0fa8073fd144 )

到專案的根路徑裡面新增 aspectj 的依賴路徑:

dependencies {
        classpath "com.android.tools.build:gradle:3.0.1"
        //這裡就是外掛所在的包名:生成外掛的模組名:外掛的版本號
        classpath 'com.fritz.plugin:plugin:1.0.0'
        //aspectj的路徑依賴
        classpath "org.aspectj:aspectjtools:1.8.9"
    }

同時到外掛的 module 裡面新增 aspectj 的依賴:

dependencies {
    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk
    //aspectj的依賴
    compile 'org.aspectj:aspectjtools:1.8.9'
}

這時候就可以開始改造我們剛才的 AspectjPlugin.groovy 檔案了:

public class AspectjPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        project.dependencies {
            compile 'org.aspectj:aspectjrt:1.8.9'
        }
        final def log = project.logger
        //列印
        log.error "========================";
        log.error "Aspectj切片開始編織Class!";
        log.error "========================";
        project.android.applicationVariants.all { variant ->
            def javaCompile = variant.javaCompile
            javaCompile.doLast {
                String[] args = ["-showWeaveInfo",
                                 "-1.7",//對應外掛module宣告的Java版本
                                 "-inpath", javaCompile.destinationDir.toString(),
                                 "-aspectpath", javaCompile.classpath.asPath,
                                 "-d", javaCompile.destinationDir.toString(),
                                 "-classpath", javaCompile.classpath.asPath,
                                 "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
                log.debug "ajc args: " + Arrays.toString(args)

                MessageHandler handler = new MessageHandler(true);
                new Main().run(args, handler)
                for (IMessage message : handler.getMessages(null, true)) {
                    switch (message.getKind()) {
                        case IMessage.ABORT:
                        case IMessage.ERROR:
                        case IMessage.FAIL:
                            log.error message.message, message.thrown
                            break
                        case IMessage.WARNING:
                            log.warn message.message, message.thrown
                            break
                        case IMessage.INFO:
                            log.info message.message, message.thrown
                            break
                        case IMessage.DEBUG:
                            log.debug message.message, message.thrown
                            break
                    }
                }
            }
        }
    }
}

這時候只要重新通過 uploadArchives 來重新生成新的本地外掛,我們就可以在專案中使用到 Aspectj 來實現 AOP 程式設計了。

巧用 Aspectj 防抖

前期準備已經完成了,那麼使用 Aspectj 能夠幫助我們解決那些開發中的痛點?

它可以解決不少痛點難點,其中最有名就是 抖動點選 :使用者可能會因為手誤而多次點選了某個按鈕,導致同一個頁面給多次開啟。

使用 Aspectj ,可以很優雅地解決這個問題。

首先建立一個 Java 註解:

/**
 * 防止抖動點選
 */
@Retention(RetentionPolicy.CLASS)//只在編譯為class時做處理
@Target(ElementType.METHOD)//針對函式方法
public @interface SingleClick {
}

接著建立一個 AspectSingleClickAspect,簡單來說,它就是一個註解處理器:

@Aspect//這個必須有,宣告為 Aspect 類
public class SingleClickAspect {

    /**
     * 過濾時間
     */
    private static final int MIN_CLICK_DELAY_TIME = 600;
    /**
     * 最近一次點選的時間
     */
    private static long mLastClickTime;
    /**
     * 最近一次點選的控制元件ID
     */
    private static int mLastClickViewId;

    /**
     * 方法切入點
     * com.fritz.java.lib.SingleClick 表示識別攔截的註解
     * 第一個 * 表示任意返回值
     * 第二個 * 表示任意方法名
     * (..) 表示任意引數
     */
    @Pointcut("execution(@com.fritz.java.lib.SingleClick * *(..))")
    public void methodAnnotated() {
    }

    /**
     * 在連線點進行方法替換
     */
    @Around("methodAnnotated()")
    public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        View view = null;
        //遍歷方法的全部引數,找到View
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof View) {
                view = (View) arg;
            }
        }
        if (view != null) {
            Log.e("SingleClickAspect", "lastClickTime:" + mLastClickTime);
            long currentTime = Calendar.getInstance().getTimeInMillis();
            //過濾掉600毫秒內的連續點選
            if (currentTime - mLastClickTime < MIN_CLICK_DELAY_TIME && view.getId()
                    == mLastClickViewId) {
                return;
            }
            mLastClickTime = currentTime;
            mLastClickViewId = view.getId();
            Log.e("SingleClickAspect", "currentTime:" + currentTime);
            //執行原方法
            joinPoint.proceed();
        }
    }
}

關於 AspectJ 的語法,網上資料已經很多了,這裡就不在多說了。

現在測試一下,給一個點選方法新增 @SingleClick 註解吧:

    @SingleClick
    public void click(View view) {
        if (view instanceof Button) {
            Log.e("SingleClickAspect", "click:" + mCount);
            ((Button) view).setText(String.valueOf(mCount));
            mCount++;
        }
    }

看下執行效果吧:

這裡寫圖片描述

輸出列印為:

這裡寫圖片描述

可以看到成功過濾了 600ms 內的點選事件了,這樣子的解決方法非常優雅呢?

只要新增一個註解即可進行防抖處理,對程式碼的侵入性極低,尤其是在團隊開發中或舊專案維護中,這個方案尤其受歡迎,因為比起重寫 OnClickListener 回撥和 使用 RxBinding 之類的方法,它無需強制規定團隊其他小夥伴的程式碼規範,因而沒有增加太多的人力成本。

當然了,如果僅僅是為了防抖就是用 AspectJ 就太大材小用了,AspectJ 能做到的事情還有很多的,具體可以參考下面這個開源庫 XAOP ,這裡就不多說了,我也趕緊把這個庫裡面的乾貨挖個乾淨。
文章 demo 的下載連結:https://download.csdn.net/download/f409031mn/10569939