Gradle插件开发

前言

学习Gradle也有一段时间了,感觉知道了很多,但是还是有些朦朦胧胧,这时候就该写点代码来融会贯通一下, 于是便决定做一个简单的插件来真正理解一下Gradle

插件开发

在开始之前,我们要知道,插件是做什么的,Gradle的插件类似java中的jar包,主要用于代码复用和逻辑封装,复杂的如同java插件,提供了一整套的java编译系统,简单的也可以就仅仅只是封装一些共有的方法、task之类,这里我们就由浅入深,从最简单的插件说起。

语言选择

Gradle User Guide里面的原话是“You can implement a custom plugin in any language you like”,虽然是这么说,但是我也见过用groovy,js,java这些语言实现的插件,并没有见过c++实现的插件,本文选择使用的是java,毕竟groovy不熟

插件形式

在Gradle中,一个插件并不是什么很神奇的东西,它其实就是Class,只是实现的一个plugin的接口,有了一个apply的方法,能够被apply到构建脚本中,它可以直接写在build.gradle的脚本中(当然这样就只能使用java或是groovy的语法),可以写在/src/main这样的资源目录下,当然也可以作为一个jar包导入 ,考虑到插件的用途,把插件打包成一个jar也算是必需的

简单插件

首先是开发工具的选择,这里我使用的是AndroidStudio(据说更好的是使用IntelliJ Idea,但是我在尝试了几个小时之后放弃了,还是Studio好),首先创建一个工程,然后新建个Module(类型无所谓,反正我们只用用这个目录而已),然后开始目录改造,该删的删,该建的建,最后的目录结构如下:

目录结构.png

res和androidTest这两个目录直接删掉,然后新建src/main/resources/META-INF/gradle-plugins目录,这个目录算是插件id的索引,只有通过这个目录定义了插件的id和与之关联的插件类,比如上面,我们定义了一个com.mime.houyi.helloworld.preperties的文件,那我们的插件id就是com.mime.houyi.helloworld,至于关联的类后面再细说。
做完上面这里,下面就直接上代码了,首先是build.gradle这里我原本建的是一个Android Library的Module,所以原本的内容全部删掉,新建内容如下

apply plugin: 'java'//导入java插件用于,编译打包我们的插件
apply plugin: 'maven'//maven插件,用于上传插件到仓库

//uploadArchives 类型是upload,这个task不是'maven'创建的
//而是'maven'定义了一个rule,而后由我们自己创建的,关于rule,请看后面内容
uploadArchives{
    //本地仓库的一种
    repositories{
        flatDir{
            name "localRepository"
            dir "localRepository/libs"
        }
    }
}
group = "com.mime.houyi"//project属性
version = "1.0"//project属性
dependencies {
    //导入Gradle的api,要写插件,肯定要使用Gradle的api
    compile gradleApi()
}

我们先分析一下上面代码,很多东西在注释中已经提到,我就不一一赘述了,uploadArchives这个Task虽然不是maven插件创建的,但是这里可以不做太多关注,后面对于这种会更详细讲解,这里姑且认为它就是maven插件创建的,用于上传整个工程的jar包。这里我们主要说明一下repositories的几种仓库定义(这个属于题外话,既然用到了就解释一下,方便更好的理解

几种仓库说明

首先是上面的使用flatDir方法创建一个仓库,定义比较简单,属性也就上面两个,name和dir,使用类似如下

repositories {
    flatDir name: 'libs', dirs: "$projectDir/libs"
    flatDir dirs: ["$projectDir/libs1", "$projectDir/libs2"]
}

第二种是ivy仓库,ivy是Apache Ant的子项目,一种类似maven的仓库,搭建和使用请自行Google,这里配置方法也很简单

repositories {
    ivy {
        //这里url可以是远程地址,也可以是本地地址
        url "http://repo.mycompany.com/repo"
    }
}

第三种是maven仓库,使用和ivy仓库一样

repositories {
    maven {
        url "http://repo.mycompany.com/maven2"
    }
}

第四种是jcenter,Bintray的JCenter仓库,这个用的多,不多说
第五种是mavenCentral,这个类似jcenter,也不多说
第六种是mavenLocal,这个是一个默认的本地仓库,具体位置可以进行配置,如果没有配置默认是在(user)/.m2/repository

插件实现

如果仅仅只是实现一个插件,那是非常的简单,在java目录下创建一个class,实现Plugin接口

public class HelloWorldPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.getTasks().create("hello", DefaultTask.class);
    }
}

可以看到我们上面的代码几乎没有做什么,仅仅只是在project中创建了一个名为hello的task,TaskType是DefaultTask,不过也可以看出来,我们去apply一个插件,事实上是把我们编译脚本的project对象作为一个参数传给了插件
做完上面这一步,事实上Gradle并不能找到我们的插件,这时候就需要META-INF/gradle-plugins这个目录下的properties文件了,我们已经建好了,然后

implementation-class=com.mime.houyi.HelloWorldPlugin

我们的插件就已经完成了,虽然这个插件没有什么功能,接下来,我们使用运行uploadArchives这个task把我们的插件打包成jar,上传到我们先前的那个flatDir创建的仓库。接下来可以实测一下我们的插件是否有问题
首先在整个工程的build.gradle下添加

 buildscript {
    repositories {
        jcenter()
        flatDir  name:'localRepository',dir:"helloworld/localRepository/libs'
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'
        classpath 'com.mime.houyi:helloworld:1.0'//命名是我们的groupId:moduleName:version
    }
}

然后在app的build.gradle下添加

apply plugin: 'com.mime.houyi.helloworld'

可以看到,在Gradle的task视窗里面:app/other下多了一个hello的task,到此最最简单的插件就完成的。Then Next

自定义Task

在开始之前我们再次对Task的做一个介绍,Gradle中所有的Task都是继承自DefaultTask,我们可以把它理解成Task中的Object类,所有的Task都是继承自DefaultTask(虽然DefaultTask也是实现了一个Task接口,但是我们不会去直接实现Task接口),Task有什么,是做什么的,这里我们用Gradle文档里面的一张图来做一个解释

image

在正常情况下,一个task都是有一些输入,处理然后产出一些输出,我们之前也在build.gradle中写过简单的task,使用doLast,但是这种类型的task没有输入输出的概念,真正要实现一个类似于java中的jar这种的插件,我们就得去写一个自定义的Task

基本概念

在开始之前,有几个基本概念需要认识一下

  • 输出:Task的目标,我们的task都是为了,达到一个目的,这就是输出,比如jar的输出是jar文件,copy的输出是目标目录
  • 输入:只有会影响一个或多个输出结果的才算是输入
  • 属性:会影响task的执行过程,但是不会影响结果
  • action:这个官方文档没有提到,我自己加的,具体的处理过程

为什么Gradle中的Task会有这几种的类型区别,主要是在Gradle中支持Incremental Build
什么?你不知道这是什么东西,打开AndroidStudio的Gradle Console你就会看到一堆的UP-TO-DATE,这就是Incremental Build,为了加快构建的速度,Gradle每次执行一个task之前就会检查task的输入和输出,如果和上次的相比都没没有变化,那么Gradle就会认为这个task是up to date的,从而跳过这个task,以此来加快构建的速度。这也就是在Gradle Console中出现的UP-TO-DATE。

关于输入输出,Gradle支持三种类型的输入输出

  • 简单类型
    String,int几种简单类型必须是可以的,但这里只是的任何实现了Serializable的类
  • 文件类型
    包括java中的File和Files以及Gradle中的FileCollection类型
  • Nested类型
    自定义的类型,不属于上面两种,但是类型里的属性都属于上面两种
具体实现

首先是我们Task类

public class WriteHelloManTask extends DefaultTask {
    private HelloManData helloMan;
    private File targetDirectory;
    private String fileName;

    @Nested
    public HelloManData getHelloMan(){
        return helloMan;
    }

    @OutputFile
    public File getTargetFile(){
        return new File(targetDirectory, fileName);
    }

    @Input
    public String getFileName(){
        return fileName;
    }

    @InputDirectory
    public File getTargetDirectory() {
        return targetDirectory;
    }

    @TaskAction
    public void writeObject(){
        File targetFile = new File(targetDirectory, fileName);
        try {
            FileOutputStream fos = new FileOutputStream(targetFile);
            byte[] bytes = helloMan.toString().getBytes();
            fos.write(bytes);
            fos.flush();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void setHelloMan(HelloManData helloMan) {
        this.helloMan = helloMan;
    }

    public void setTargetDirectory(File targetDirectory) {
        this.targetDirectory = targetDirectory;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
}

HelloManData 类

public class HelloManData {
    private String name;
    private int age;

    public HelloManData(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Input
    public String getName() {
        return name;
    }

    @Input
    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Hello "+age+" years old "+name+" !";
    }
}

上面的代码,一眼就能看到诸如InputOutputTaskAction之类的注解,这些注解就是用来标记输入,输出以及action的,因为Gradle的Incremental Build机制,我们必须把输入都标记上才能正确的运行(如果没有标记,可能会出现输入改变了,但是Task却跳过了),但是同时Gradle也不建议把不会影响输出的属性标记为输入,这样会在整体上降低Gradle的构建速度,下面我就一张列表来看下,我们可能会使用到的注解

注解名称 文件类型 详情
@Input serializable的类型 也就是前面所说的简单类型
@InputFile File* 单个的文件类型的输入(非文件夹)
@InputDirectory File* 单个的文件类型的输入(非文件)
@InputFiles Iterable<File>* 可Iterable的文件或者文件夹的集合类型的输入
@Nested 自定义类 没有实现serializable,但是至少有一个属性使用表里的注解,包括@Nested注解
@OutputFile File* 单个的文件类型的输出(非文件夹)
@OutputDirectory File* 单个的文件类型的输入(非文件)
@OutputFiles Map<String, File>** 或者Iterable<File>* 可Iterable的文件的集合类型的输出(非文件夹)
@OutputDirectories Map<String, File>** 或者Iterable<File>* 可Iterable的文件夹的集合类型的输出(非文件)
@TaskAction - 用于标注Task真正执行的action

从表中我们可以看出,Gradle对于Task的输出偏向于文件方面,同时Task的定义也是大致如此,接下来我们就可以在我们的插件中使用这个Task

public class HelloWorldPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        WriteHelloManTask task = project.getTasks().create("writeHello", WriteHelloManTask.class);
        task.setFileName("HelloWorld.txt");
        task.setHelloMan(new HelloManData("Jim",19));
        task.setTargetDirectory(new File("D:/workspace"));
        task.setGroup("hello");
    }
}

修改一下version,重复一次之前的uploadArchives操作,我们可以在:app的Task下看到一个新的类别hello(我不会告诉你,我仅仅只是因为好找,因为other下的task实在是太多了),运行writeHello的task,就可以看到我们的相应目录下多了一个HelloWorld.txt的文件,然后再次运行,我们就可以看到

:app:writeHello UP-TO-DATE 

Task直接跳过了,因为Gradle看来输入输出和上一次没有变化。到此为止,我们一个相对简单插件就能够完成了,但是看起来和我们平时使用的插件还是有一定的差距,所以让我们继续下一节

关于DSL

前面我们已经提到过DSL(领域专用语言/domain specific language),虽然还没有学过实现DSL,但我们已经使用了很多了,是否需要定义DSL,是根据需求来的,很多插件并不需要定义DSL,需要定义DSL的大多都是需要和使用者交互,使用DSL,使用者不用关心我们的实现方式,只需要通过DSL就可以完成他们需要的配置。

几个重要的点(或者说是类,概念)
  • Extension
    通过Extension,我们可以向目标对象添加DSL扩展,这一过程通过project中的ExtensionContainer来实现,我们可以通过ExtensionContainer的create来创建新的DSL域,并与一个对应的委托类关联起来(即新建一个DSL域,并委托给一个具体类)
  • Convention
    与Extension类似,但是又有所不同,通过Convention的getPlugin方法,我们会把一个类融合到Convention所在的域,而不是新建一个域(具体区别可以参考java插件和android插件)
  • NamedDomainObjectContainer
    命名对象容器,可以用于在buildscript中创建对象,创建的对象必须要有name属性作为容器内元素的标识
  • Instantiator
    Instantiator 用于实例化对象, 使用Instantiator而不直接使用new,是因为使用Instantiator实例化对象时,会添加DSL特性
  • ext 是project的一个属性,维持一个命名空间,用于为project添加键值对属性,其实与DSL无关,只是形式和类型上和extension类似
新建DSL

简单插件的创建过程,就和上面一样创建一个名为createdsl的model,我就不一一赘述,创建DSL我们需要使用前面提到的Extension,在代码中我们可以使用project.getExtension()获取Extension(ExtensionContainer的实现类)。通过ExtensionContainer的create方法创建一个新的DSL

MyExtension mMyExtension = project.getExtensions().create("myExtension", MyExtension.class);

MyExtension对象(一个简单的实体类)

public class MyExtension {
    private String mExtensionName;
    private InnerExtension mInnerExtension;

    public MyExtension() {

    }

    public String getExtensionName() {
        return mExtensionName;
    }

    public void setExtensionName(String extensionName) {
        this.mExtensionName = extensionName;
    }

    public InnerExtension getInnerExtension() {
        return mInnerExtension;
    }

    public void setInnerExtension(InnerExtension innerExtension) {
        mInnerExtension = innerExtension;
    }
}

这样我们就成功创建了一个DSL,在apply过我们的plugin之后,我们就可以在buildscript中定义这个DSL了,我们可以直接配置简单属性如String、Boolean之类,但是直接定义对象类就会出错

myExtension {
    extensionName "ddd" //这行代码没有 问题,可以正常通过
    innerExtension{ //这个会报错
    }
}

关于上面extensionName这个属性名,大家可能会有所疑惑,因为我们上面定义的成员名是mExtensionName,这里这个属性名其实是根据set方法生成的,这个属性赋值,实质上也是调用的set方法。

对于普通对象型的extension属性,我们如果想要在buildscript中直接定义它的话,就需要使用上面说过的Instantiator。

这里希望大家有一个概念,我们写在buildscript中的myExtension并不直接就是我们代码中的mMyExtension对象, 这中间存在对应的关系,对于MyExtension我们通过extension.create来实现这一对应(其实在create方法中也是通过Instantiator来实现的),但是对于MyExtension中的一个普通成员对象innerExtension,我们需要使用Instantiator实现这一关系。

修改我们的代码,主要是两个地方,一个是apply方法

Instantiator instantiator = ((DefaultGradle) project.getGradle()).getServices().get(
        Instantiator.class);
MyExtension mMyExtension = project.getExtensions().create("myExtension", MyExtension.class,
        new Object[]{instantiator});
//create方法的第三个参数是MyExtension的构造参数

另一个是MyExtension构造方法

public MyExtension(Instantiator instantiator) {
    mInnerExtension = instantiator.newInstance(InnerExtension.class);
}

但是这样还不够,我们在上面说到,gradle是根据set方法来在buildscript中定义属性的,我们上面在buildscript中传递给set方法的是scriptblock块,所以我们的set方法要接受一个scriptblock块。。。


不过还好,gradle中定义了一个Action的interface,就是用于处理这种场景,让我们修改InnerExtension的set方法

public void innerExtension(Action<InnerExtension> action) {
    //这里方法名写成setInnerExtension,gradle也是可以会调用的,
    //但是为了防止意外,还是写出我们想要的名字
    action.execute(mInnerExtension);
}

修改后的set方法接受一个action类型的参数,perfect!

除了上面所说的简单类型和对象类型,大家可能还见过另一种,比如android中的buildTypes、productFlavors,这种类型可以用于在buildscript中创建新的指定类型的对象,也就是上面提到的NamedDomainObjectContainer,但是,How to use?这种时候就需要一份[官方文档](https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:container(java.lang.Class, org.gradle.api.NamedDomainObjectFactory))了,我们看到project中有这样一个方法:

NamedDomainObjectContainer<T> container(Class<T> type, NamedDomainObjectFactory<T> factory)
//创建一个容器来管理指定对象T的命名对象,参数factory是用于创建指定对象的,
//所有需要被创建的对象必须拥有切暴露一个"name"的属性,这个属性在对像的生命周期内必须是不变的

感觉是可以开始了,根据文档,我们首先创建一个factory类

public class SmallExtensionFactory implements NamedDomainObjectFactory<SmallExtension> {
    @Override
    public SmallExtension create(String name) {
        return new SmallExtension(name);
    }
}

好像没有问题,但是真的可以了吗?少侠,你还缺少一个Instantiator,想要写在buildscript中定义那就彻底把new放弃掉吧! 正确的姿势应该是

public class SmallExtensionFactory implements NamedDomainObjectFactory<SmallExtension> {

    private Instantiator mInstantiator;

    public SmallExtensionFactory(Instantiator instantiator) {
        this.mInstantiator = instantiator;
    }

    @Override
    public SmallExtension create(String name) {
        return mInstantiator.newInstance(SmallExtension.class, name);
    }
}

现在就是构造一个NamedDomainObjectContainer,把它放到MyExtension中。

Instantiator instantiator = ((DefaultGradle) project.getGradle()).getServices().get(
        Instantiator.class);
NamedDomainObjectContainer<SmallExtension> smallExtensionsContainer = project.container(
        SmallExtension.class, new SmallExtensionFactory(instantiator));
MyExtension mMyExtension = project.getExtensions().create("myExtension", MyExtension.class,
        new Object[]{instantiator,smallExtensionsContainer});

MyExtension中同时添加一个NamedDomainObjectContainer<SmallExtension>的成员,set方法如下

public void smallExtensions(
        Action<? super NamedDomainObjectContainer<SmallExtension>> action) {
    //这里还是是用action的方式来接受buildscript的参数
    //这里方法名写成setSmallExtensions,gradle就无法找到对应的方法了
    action.execute(mSmallExtensions);
}

还有一点需要注意的是,SmallExtension需要有这么要给属性

final String mName;

到此为止,我们一个简单的DSL就完成了,我们可以在buildscript中如下定义

myExtension {
    extensionName "ddd" //简单属性
    innerExtension{//对象类型
        extensionName "innerExtension"
    }
    smallExtensions{//命名对象容器
        extension1{
            extensionName "11"
        }
        extension2{
            extensionName "22"
        }
    }
}

上面三种类型是可以组合是用的,组成更多样的DSL。

小结

这次内容就到此为止,当然Gradle的插件开发远不是这么简单,还有一些高级特性这里还没有写到,更多的就要靠大家自己去探索了。(差点忘了github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容