动手写一个AspectJ的gradle插件

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/f409031mn/article/details/81264110

越是深入学习 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 的目录如下:

这里写图片描述

这样,一个基于 GroovyGradle 插件 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

猜你喜欢

转载自blog.csdn.net/f409031mn/article/details/81264110