前言
Gradle是一个灵活和高效自动化构建工具,它的构建脚本采用Groovy或kotlin语言编写,Groovy或Kotlin都是基于JVM的语言,它们的语法和java的语法有很多的类似并且兼容java的语法,所以对于java开发者,只需很少的学习成本就能快速上手Gradle开发,同时Gradle也是Android官方的构建工具,学习Gradle,能够帮助我们更好的了解Android项目的构建过程,当项目构建出现问题时,我们也能更好的排查问题,所以Gradle的学习能帮助我们更好的管理Android项目,Gradle的官方地址如下:
Gradle的特点
1、Gradle构建脚本采用Groovy或Kotlin语言编写,如果采用Groovy编写,构建脚本后缀为.gradle,在里面可以使用Groovy语法,如果采用Kotlin编写,构建脚本后缀为.gradle.kts,在里面可以使用Kotlin语法;
2、因为Groovy或Kotlin都是面向对象语言,所以在Gradle中处处皆对象,Gradle的.gradle或.gradle.kts脚本本质上是一个Project对象,在脚本中一些带名字的配置项如buildscript、allprojects等本质上就是对象中的方法,而配置项后面的闭包{}就是参数,所以我们在使用这个配置项时本质上是在调用对象中的一个方法;
3、在Groovy或Kotlin中,函数和类一样都是一等公民,它们都提供了很好的闭包{}支持,所以它们很容易的编写出具有DSL风格的代码,用DSL编写构建脚本的Gradle比其他采用xml编写构建脚本的构建工具如maven、Ant等的可读性更强,动态性更好,整体更简洁;
4、Gradle中主要有Project和Task对象,Project是Gradle中构建脚本的表示,一个构建脚本对应一个Project对象,Task是Gradle中最小的执行单元,它表示一个独立的任务,Project为Task提供了执行的上下文。
Groovy基础入门
本文的所有示例都是采用Groovy语言编写,在阅读本文前先简单的入门Groovy:
下面主要讲Groovy与java的主要区别:
1、Groovy语句后面的分号可以忽略
1 | int num1 = 1 |
2、Groovy支持动态类型推导,使用def来定义变量和方法时可以不指定变量的类型和方法的返回值类型,同时定义方法时参数可以不指定类型
1 | def num1 = 1 |
3、Groovy的方法调用传参时可以不添加括号,方法不指定return语句时,最后一行默认为返回值
1 | def result = add 1, 2 |
4、Groovy可以用单、双、三引号来表示字符串,其中单引号表示普通字符串,双引号表示的字符串可以使用取值运算符${},而$在单引号只只是表示一个普通的字符,三引号表示的字符串又称为模版字符串,它可以保留文本的换行和缩进格式,三引号同样不支持$
1 | def world = 'world' |
5、Groovy会为类中每个没有可见性修饰符的字段生成get/set方法,我们访问这个字段其实是调用它的get/set方法,同时如果类中的方法以get/set开头,我们也可以像普通字段一样访问这个方法
1 | class Person{ |
6、Groovy中的闭包是用{参数列表 -> 代码体}表示的代码块,当参数 <= 1个时,->箭头可以省略,同时当只有一个参数时,如果不指定参数名默认以it为参数名,在Groovy中闭包对应的类型是Closure,所以闭包可以作为参数传递,同时闭包有一个delegate字段, 通过delegate可以把闭包中的执行代码委托给任意对象来执行
1 | class Person{ |
在Gradle中很多地方都使用了闭包的委托机制,通过闭包完成一些特定对象的配置,在Gradle中,如果你没有指定闭包的delegate,delegate默认为当前项目的Project对象。
以上Groovy知识,认为对于有java基础的人来说,用于学习Gradle足够了,当然对于一些集合操作、文件操作等,等以后使用到时可以到Groovy官网来查漏补缺。
Gradle的安装配置
官方教程:Installing Gradle
安装Gradle前需要确保你的电脑已经配置好JDK,JDK的版本要求是8或更高,可以通过包管理器自动安装Gradle或手动下载Gradle安装两种方式,在Window平台下我推荐使用手动安装,在Mac平台下我推荐使用Homebrew包管理器自动安装:
1、Window平台
- 1、在Gradle的安装页面选择一个Gradle版本,下载它的binary-only或complete版本,binary-only版本表示下载的Gradle压缩包只包含Gradle的源码,complete版本表示下载的Gradle压缩包包含Gradle的源码和源码文档说明,这里我下载了gradle-6.5-bin版本;
- 2、下载好Gradle后,把它解压到特定目录,如我这里解压到:D:/gradle,然后像配置java环境一样把D:/gradle/gradle-6.5/bin路径添加到系统的PATH变量下;
- 3、打开cmd,输入
gradle -v
校验是否配置成功,输出以下类似信息则配置成功.
1 | C:\Users\HY>gradle -v |
2、Mac平台
- 1、安装Homebrew;
- 2、打开终端,输入
brew install gradle
,它默认会下载安装binary-only版本; - 3、当Homebrew安装Gradle完成后,在终端输入
gradle -v
校验是否安装成功.
Gradle的项目结构
Gradle项目可以使用Android Studio、IntelliJ IDEA等IDE工具或文本编辑器来编写,这里我以Mac平台为例采用文本编辑器配合命令行来编写,Window平台类似。
新建一个目录,如我这里为:~/GradleDemo,打开命令行,输入cd ~/GradleDemo
切换到这个目录,然后输入gradle init
,接着gradle会执行init
这个task任务,它会让你选择生成的项目模版、编写脚本的语言、项目名称等,我选择了basic模版(即原始的Gradle项目)、Groovy语言、项目名称为GradleDemo,如下:
这样会init
任务就会自动替你生成相应的项目模版,如下:
忽略掉以.开头的隐藏文件或目录,gradle init为我们自动生成了以下文件或目录:
1、build.gradle
它表示Gradle的项目构建脚本,在里面我们可以通过Groovy来编写脚本,在Gradle中,一个build.gradle就对应一个项目,build.gradle放在Gradle项目的根目录下,表示它对应的是根项目,build.gradle放在Gradle项目的其他子目录下,表示它对应的是子项目,Gradle构建时会为每一个build.gradle创建一个对应的Project对象,这样编写build.gradle时就可以使用Project接口中的方法。
2、settings.gradle
它表示Gradle的多项目配置脚本,存放在Gradle项目的根目录下,在里面可以通过include来决定哪些子项目会参与构建,Gradle构建时会为settings.gradle创建一个对应的Settings对象,include也只是Settings接口中的一个方法。
3、Gradle Wrapper
gradle init
执行时会同时执行wrapper
任务,wrapper
任务会创建gradle/wrapper目录,并创建gradle/wrapper目录下的gradle-wrapper.jar、gradle-wrapper.properties这两个文件,还同时创建gradlew、gradlew.bat这两个脚本,它们统称为Gradle Wrapper,是对Gradle的一层包装。
Gradle Wrapper的作用就是可以让你的电脑在不安装配置Gradle环境的前提下运行Gradle项目,例如当你的Gradle项目被用户A clone下来时,而用户A的电脑上没有安装配置Gradle环境,用户A通过Gradle构建项目时,Gradle Wrapper就会从指定下载位置下载Gradle,并解压到电脑的指定位置,然后用户A就可以在不配置Gradle系统变量的前提下在Gradle项目的命令行中运行gradlew或gradlew.bat脚本来使用gradle命令,假设用户A要运行gradle -v
命令,在linux平台下只需要运行./gradlew -v
,在window平台下只需要运行gradlew -v
,只是把gradle
替换成gradlew
。
Gradle Wrapper的每个文件含义如下:
1、gradlew:用于在linux平台下执行gradle命令的脚本;
2、gradlew.bat:用于在window平台下执行gradle命令的脚本;
3、gradle-wrapper.jar:包含Gradle Wrapper运行时的逻辑代码;
4、gradle-wrapper.properties:用于指定Gradle的下载位置和解压位置;
gradle-wrapper.properties中各个字段解释如下:
字段名 | 解释 |
---|---|
distributionBase | 下载的Gradle的压缩包解压后的主目录,为GRADLE_USER_HOME,在window中它表示C:/用户/你电脑登录的用户名/.gradle/,在mac中它表示~/.gradle/ |
distributionPath | 相对于distributionBase的解压后的Gradle的路径,为wrapper/dists |
distributionUrl | Grade压缩包的下载地址,在这里可以修改下载的Gradle的版本和版本类型(binary或complete),例如gradle-6.5-all.zip表示Gradle 6.5的complete版本,gradle-6.5-bin.zip表示Gradle 6.5的binary版本 |
zipStoreBase | 同distributionBase,不过是表示存放下载的Gradle的压缩包的主目录 |
zipStorePath | 同distributionPath,不过是表示存放下载的Gradle的压缩包的路径 |
使用Gradle Wrapper后,就可以统一项目在不同用户电脑上的Gradle版本,同时不必让运行这个Gradle项目的人安装配置Gradle环境,提高了开发效率。
Gradle的多项目配置
现在我们创建的Gradle项目默认已经有一个根项目了,它的build.gradle文件就处于Gradle项目的根目录下,如果我们想要添加多个子项目,这时就需要通过settings.gradle进行配置。
首先我们在GradleDemo中创建多个文件夹,这里我创建了4个文件夹,分别为:subproject_1、subproject_2,subproject_3,subproject_4,然后在每个新建的文件夹下创建build.gradle文件,如下:
接着打开settings.gradle,添加如下:
1 | include ':subproject_1', ':subproject_2', ':subproject_3', ':subproject_4' |
这样就完成了子项目的添加,打开命令行,切换到GradleDemo目录处,输入gradle projects
,执行projects任务展示所有项目之间的依赖关系,如下:
1 | # in ~/GradleDemo |
可以看到,4个子项目依赖于根项目,接下来我们来配置项目,配置项目一般在当前项目的build.gradle中进行,可以通过buildscript方法、repositories方法、dependencies方法等Project接口提供的方法进行配置,但是如果有多个项目时,而每个项目的某些配置又一样,那么在每个项目的build.gradle进行相同的配置是很浪费时间,而Gradle的Project接口为我们提供了allprojects和subprojects方法,在根项目的build.gradle中使用这两个方法可以全局的为所有子项目进行配置,allprojects和subprojects的区别是:allprojects的配置包括根项目而subprojects的配置不包括根项目,例如:
1 | //根项目的build.gradle |
Gradle构建的生命周期
当在命令行输入gradle build
构建整个项目或gradle task名称
执行某个任务时就会进行Gradle的构建,它的构建过程分为3个阶段:
init(初始化阶段) -> configure(配置阶段) -> execute(执行阶段)
- init:初始化阶段主要是解析settings.gradle,生成Settings对象,确定哪些项目需要参与构建,为需要构建的项目创建Project对象;
- configure:配置阶段主要是解析build.gradle,配置init阶段生成的Project对象,构建根项目和所有子项目,同时生成和配置在build.gradle中定义的Task对象,构造Task的关系依赖图,关系依赖图是一个有向无环图;
- execute:根据configure阶段的关系依赖图执行Task.
Gradle在上面3个阶段中每一个阶段的开始和结束都会hook一些监听,暴露给开发者使用,方便开发者在Gradle的不同生命周期阶段做一些事情。
settings.gradle和build.gradle分别代表Settings对象和Project对象,它们都有一个Gradle对象,我们可以在Gradle项目根目录的settings.gradle或build.gradle中获取到Gradle对象,然后进行生命周期监听,如下:
1 | //build.gradle或settings.gradle |
上面监听方法的放置顺序就是整个Gradle构建的顺序,但是要注意的是Gradle的buildStarted方法永远不会被回调,因为我们注册监听的时机太晚了,当解析settings.gradle或build.gradle时,Gradle就已经构建开始了,所以这个方法也被Gradle标记为废弃的了,因为我们没有机会监听到Gradle构建开始,同时如果你是在build.gradle中添加上面的所有监听,那么Gradle的settingsEvaluated和projectsLoaded方法也不会被回调,因为settings.gradle的解析是在build.gradle之前,在build.gradle中监听这两个方法的时机也太晚了。
也可以通过Gradle对象的addBuildListener方法添加BuildListener来监听Gradle的整个生命周期回调
上面是监听整个Gradle构建的生命周期回调监听,那么我怎么监听我的当前单个项目的构建开始和结束呢?只需要在你当前项目的build.gradle中添加:
1 | //build.gradle |
但是要注意的是在根项目的build.gradle添加上述方法,其beforeEvaluate方法是无法被回调的,因为注册时机太晚,解析根项目的build.gradle时根项目已经开始构建了,但是子项目的build.gradle添加上述方法是可以监听到项目构建的开始和结束,因为根项目构建完成后才会轮到子项目的构建。
Task
1、Task的创建
Task是Gradle中最小执行单元,它是一个接口,默认实现类为DefaultTask,在Project中提供了task方法来创建Task,所以Task的创建必须要处于Project上下文中,这里我在subproject_1/build.gradle中创建Task,如下:
1 | //subproject_1/build.gradle |
上述代码通过task方法创建了一个名为task1的Task,在创建Task的同时可以通过闭包配置它的doFirst和doLast动作,doFirst和doLast都是Task中的方法,其中doFirst方法会在Task的action执行前执行,doLast方法会在Task的action执行后执行,而action就是Task的执行单元,在后面自定义Task会介绍到,除此之外还可以在创建Task之后再指定它的doFirst和doLast动作,如下:
1 | //subproject_1/build.gradle |
上面通过Project的task方法创建的Task默认被放在Project的TaskContainer类型的容器中,我们可以通过Project的getTasks方法获取到这个容器,而TaskContainer提供了create方法来创建Task,如下:
1 | //subproject_1/build.gradle |
以上就是创建Task最常用的几种方式,创建Task之后,就可以执行它,执行一个Task只需要把task名称接在gradle
命令后,如下我在命令行输入gradle task1
执行了task1:
1 | # in ~/GradleDemo |
如果你想精简输出的信息,只需要添加-q
参数,如:gradle -q task1
,这样输出就只包含task1的输出:
1 | # in ~/GradleDemo |
如果要执行多个Task,多个task名称接在gradle
命令后用空格隔开就行,如:gradle task1 task2 task3
。
2、Task的属性配置
Gradle为每个Task定义了默认的属性(Property), 比如description、group、dependsOn、inputs、outputs等, 我们可以配置这些Property,如下配置Task的描述和分组:
1 | //subproject_2/build.gradle |
Gradle在执行一个Task之前,会先配置这个Task的Property,然后再执行这个Task的执行代码块,所以配置Task的代码块放在哪里都无所谓,如下:
1 | //subproject_2/build.gradle |
我们可以通过Task的dependsOn属性指定Task之间的依赖关系,如下:
1 | //subproject_2/build.gradle |
上述的依赖关系是task3 -> task4 -> task5,依赖的Task先执行,所以当我在命令行输入gradle task5
执行task5时,输出:
1 | # in ~/GradleDemo |
依次执行task3、 task4 、task5。
3、自定义Task
前面创建的Task默认都是DefaultTask类型,我们可以通过继承DefaultTask来自定义Task类型,Gradle中也内置了很多具有特定功能的Task,它们都间接继承自DefaultTask,如Copy(复制文件)、Delete(文件清理)等,我们可以直接在build.gradle中自定义Task,如下:
1 | //subproject_3/build.gradle |
在MyTask中,通过@TaskAction注解的方法就是该Task的action,action是Task最主要的组成,它表示Task的一个执行动作,当Task中有多个action时,多个action的执行顺序按照@TaskAction注解的方法放置的逆顺序,所以执行一个Task的过程就是:doFirst方法 -> action方法 -> doLast方法,在MyTask中定义了两个action,接下来我们使用这个Task,如下:
1 | //subproject_3/build.gradle |
在定义Task时通过type指定Task的类型,同时还可以通过闭包配置MyTask中的message参数,在命令行输入gradle myTask
执行这个Task,如下:
1 | # in ~/GradleDemo |
我们自定义的Task本质上就是一个类,除了直接在build.gradle文件中编写自定义Task,还可以在Gradle项目的根目录下新建一个buildSrc目录,在buildSrc/src/main/[java/kotlin/groovy]目录中定义编写自定义Task,可以采用java、kotlin、groovy三种语句之一,或者在一个独立的项目中编写自定义Task然后发布到远程仓库托管平台,在后面自定义Plugin时会讲到这几种方式。
4、让Task支持增量式构建
上述我们自定义的Task每次执行时,它的action都会被执行,进行全量构建,其实Gradle支持增量式构建的Task,增量式构建就是当Task的输入和输出没有变化时,跳过action的执行,当Task输入或输出发生变化时,在action中只对发生变化的输入或输出进行处理,这样就可以避免一个没有变化的Task被反复构建,还有当Task发生变化时只处理变化部分,这样就会提高整个Gradle的构建效率,大大缩短整个Gradle的构建时间,所以当我们编写复杂的Task时,让Task支持增量式构建是很有必要的,让Task支持增量式构建只需要做到两步:
1、让Task的inputs和outputs参与Gradle的Up-to-date检查;
2、让Task的action支持增量式构建;
下面我们通过这两步自定义一个简单的、支持增量式构建的Copy任务,这个Copy任务的作用是把输入的文件复制到输出的位置中:
首先我们要让Copy任务的inputs和outputs参与Gradle的Up-to-date检查,每一个Task都有inputs和outputs属性,它们的类型分别为TaskInputs和TaskOutputs,Task的inputs和outputs主要有以下三种类型:
- 可序列化类型:可序列化类型是指实现了Serializable的类或者一些基本类型如int、string等;
- 文件类型:文件类型是指标准的java.io.File或者Gradle衍生的文件类型如FileCollection、FileSystemLocation等;
- 自定义类型:自定义类型是指自己定义的类,这个类含有Task的部分输入和输出属性,或者说任务的部分输入和输出属性嵌套在这个类中.
我们可以在自定义Task时通过注解指定Task的inputs和outputs,通过注解指定的inputs和outputs会参与Gradle的Up-to-date检查,它是编写增量式Task的前提,Up-to-date检查是指Gradle每次执行Task前都会检查Task的输入和输出,如果一个Task的输入和输出自上一次构建以来没有发生变化,Gradle就判定这个Task是可以跳过执行的,这时你就会看到Task构建旁边会有一个UP-TO-DATE文本,Gradle提供了很多注解让我们指定Task的inputs和outputs,常用的如下:
注解 | 对应的类型 | 含义 |
---|---|---|
@Input | 可序列化类型 | 指单个输入可序列化的值,如基本类型int、string或者实现了Serializable的类 |
@InputFile | 文件类型 | 指单个输入文件,不表示文件夹,如File、RegularFile等 |
@InputDirectory | 文件类型 | 指单个输入文件夹,不表示文件,如File、Directory等 |
@InputFiles | 文件类型 | 指多个输入的文件或文件夹,如FileCollection、FileTree等 |
@OutputFile | 文件类型 | 指单个输出文件,不表示文件夹,如File、RegularFile等 |
@OutputDirectory | 文件类型 | 指单个输出文件夹,不表示文件,如File、Directory等 |
@OutputFiles | 文件类型 | 指多个输出的文件,如FileCollection、Map<String, File>等 |
@OutputDirectories | 文件类型 | 指多个输出的文件夹,如FileCollection、Map<String, File>等 |
@Nested | 自定义类型 | 指一种自定义的类,这个类它可能没有实现Serializable,但这个类里面至少有一个属性使用本表中的一个注解标记,即这个类会含有Task的输入或输出 |
@Internal | 任何类型 | 它可以用在可序列化类型、文件类型、还有自定义类型上,它指该属性只在Task的内部使用,即不是Task的输入也不是Task的输出,通过@Internal注解的属性不参与Up-to-date检查 |
@Optional | 任何类型 | 它可以用在可序列化类型、文件类型、还有自定义类型上,它指该属性是可选的,通过@Optional注解的属性可以不为它赋值,关闭校验 |
@Incremental | Provider<FileSystemLocation> 或者 FileCollection | 它和@InputFiles或@InputDirectory一起使用,它用来指示Gradle跟踪文件属性的更改,通过@Incremental注解的文件属性可以通过InputChanges的getFileChanges方法查询文件的更改,帮助实现增量构建Task |
@SkipWhenEmpty | 文件类型 | 它和@InputFiles或@InputDirectory一起使用,它用来告诉Gradle如果相应的文件或文件夹为空就跳过该Task,通过@SkipWhenEmpty注解的所有输入属性如果都为空,就会导致Gradle跳过这个Task,从而在控制台产出一个NO-SOURCE输出结果 |
我们自定义Task时可以使用表中的注解来指定输入和输出,其中@InputXX是用来指定输入属性,@OuputXX是用来指定输出属性,@Nested是用来指定自定义类,这个类里面至少含有一个使用@InputXX或@OuputXX指定的属性,而@Internal和@Optional是可以用来指定输入或输出的,最后的@Incremental和@SkipWhenEmpty是用来与@InputFiles或@InputDirectory一起使用的,用于支持增量式构建任务,后面会讲,还有一点要注意的是这些注解只有声明在属性的get方法中才有效果,前面讲过groovy的字段默认都生成了get/set方法,而如果你是用java自定义Task的,要记得声明在属性的get方法中,我们来看Copy任务的实现,如下:
1 | //subproject_3/build.gradle |
这里Copy任务只使用了@InputFiles和@OutputDirectory,通过@InputFiles指定Copy任务复制的来源文件,通过@OutputDirectory指定Copy任务复制的目标文件夹,然后在action方法中执行复制步骤,然后我们来使用这个Copy任务,如下:
1 | //subproject_3/build.gradle |
为了使用这个Copy任务,我在subproject_3目录下创建了一个from文件夹,里面只有一个名为text1.txt的文本文件,然后把from文件夹指定为Copy任务的输入,to文件夹指定为Copy任务的输出,在命令行输入gradle copyTask
执行这个Task,如下:
1 | # in ~/GradleDemo |
任务执行成功后就可以把from文件夹中的文件复制到to文件夹,此时文件结构如下:
1 | subproject_3 |
接着我们再次在命令行输入gradle copyTask
执行这个Task,如下:
1 | # in ~/GradleDemo |
再次执行时由于Copy任务的输入和输出都没有变化,所以Gradle判定为UP-TO-DATE,跳过执行。
目前Copy任务已经支持Up-to-date检查,但还不支持增量构建,即如果此时我们往from文件夹新增一个text2.txt文件,由于Copy任务的输入发生变化,这时重新执行Copy任务时就会重新执行action方法,进行全量构建,把text1.txt、text2.txt文件复制到to文件夹中,你会发现text1.txt被重复复制了,我们希望的是每次from中新增或修改文件时,只对新增或修改的文件进行复制,而之前没有变化的文件不进行复制,所以要做到这一步,还要让Task的action方法支持增量构建,要让Task的action方法支持增量式构建,只需要让action方法带一个InputChanges类型的参数就可以,带InputChanges类型参数的action方法表示这是一个增量任务操作方法,该参数告诉Gradle,该action方法仅需要处理更改的输入,此外,Task还需要通过使用@Incremental或@SkipWhenEmpty来指定至少一个增量文件输入属性,我们继续看Copy任务的实现,如下:
1 | //subproject_3/build.gradle |
Copy任务中通过@Incremental指定了需要增量处理的输入,然后在action方法中通过InputChanges进行增量复制文件,我们可以通过InputChanges的getFileChanges方法获取变化的文件,该方法接收一个FileCollection类型的参数,传入的参数必须要通过@Incremental或@SkipWhenEmpty注解,getFileChanges方法返回的是一个FileChange列表,FileChange持有变化的文件File、文件类型FileType和文件的变化类型ChangeType,这样我们就可以根据变化的文件、ChangeType、FileType进行增量输出,ChangeType有三种取值:
- ADDED:表示这个文件是新增的;
- MODIFIED:表示这个文件被修改了;
- REMOVED:表示这个文件被删除了.
同时并不是每次执行都是增量构建,我们可以通过InputChanges的isIncremental方法判断本次构建是否是增量构建,当处于以下情况时,Task会以非增量形式即全量执行:
- 该Task是第一次执行;
- 该Task只有输入没有输出;
- 该Task的upToDateWhen条件返回了false;
- 自上次构建以来,该Task的某个输出文件已更改;
- 自上次构建以来,该Task的某个属性输入发生了变化,例如一些基本类型的属性;
- 自上次构建以来,该Task的某个非增量文件输入发生了变化,非增量文件输入是指没有使用@Incremental或@SkipWhenEmpty注解的文件输入.
当Task处于非增量构建时,即InputChanges的isIncremental方法返回false时,通过InputChanges的getFileChanges方法能获取到所有的输入文件,并且每个文件的ChangeType都为ADDED,当Task处于增量构建时,即InputChanges的isIncremental方法返回true时,通过InputChanges的getFileChanges方法能获取到只发生变化的输入文件。
接下来让我们在上面的基础下执行Copy任务,在命令行输入gradle copyTask
执行这个Task,如下:
1 | # in ~/GradleDemo |
可以看到第一次执行Task,以非增量方式执行,把from/text1.txt文件复制到了to文件夹,接下来,我们在from文件夹下新增text2.txt、text3.text,然后再次在命令行输入gradle copyTask
执行Copy任务,如下:
1 | # in ~/GradleDemo |
可以看到当我们新增文件后第二次执行Task,以增量方式执行,只把新增的text2.txt、 text3.txt复制到to文件夹,而text1.txt没有被重复复制,此时文件结构如下:
1 | subproject_3 |
接下来我们把text1.txt修改在里面添加一行hello world,然后再次在命令行输入gradle copyTask
执行Copy任务,如下:
1 | # in ~/GradleDemo |
可以看到当我们修改文件后第三次执行Task,以增量方式执行,只把修改的text1.txt复制到to文件夹,而其他文件没有被重复复制,接下来我们把to文件夹里的text1.txt删除,然后再次在命令行输入gradle copyTask
执行Copy任务,如下:
1 | # in ~/GradleDemo |
可以看到当我们删除某个输出文件后第四次执行Task,以非增量方式执行,把from文件夹下的文件重新复制到to文件夹,并且所有的文件状态都是ADDED,此时如果我们不做任何操作,再次在命令行输入gradle copyTask
执行Copy任务会输出UP-TO-DATE。
到现在我们就实现了一个支持增量式构建的Copy任务,通过Gradle提供的注解和InputChanges,我们很容易就自定义一个支持增量构建的Task,当你编写的Task支持增量构建后,你还可以考虑更进一步,让你的Task的支持延迟配置 - Lazy Configuration,这是Gradle提供的对属性的一种状态管理,就不在本文展开了。
自定义Plugin
Plugin可以理解为一系列Task的集合,通过实现Plugin
- 1、在build.gradle中直接编写:可以在任何一个build.gradle文件中编写自定义Plugin,此方式自定义的Plugin只对该build.gradle对应的项目可见;
- 2、在buildSrc目录下编写:可以在Gradle项目根目录的buildSrc/src/main/[java/kotlin/groovy]目录中编写自定义Plugin,可以采用java、kotlin、groovy三种语句之一,Gradle在构建时会自动的编译buildSrc/src/main/[java/kotlin/groovy]目录下的所有类文件为class文件,供本项目所有的build.gradle引用,所以此方式自定义的Plugin只对本Gradle项目可见;
- 3、在独立项目中编写:可以新建一个Gradle项目,在该Gradle项目中编写自定义Plugin,然后把Plugin源码打包成jar,发布到maven、lvy等托管平台上,这样其他项目就可以引用该插件,所以此方式自定义的Plugin对所有Gradle项目可见.
由于在上面自定义Task的介绍中已经讲过了如何在build.gradle中直接编写,自定义Plugin也类似,所以下面就主要介绍2、3两种方式,而且这两种方式也是平时开发中自定义Plugin最常用的方式。
1、在buildSrc目录下编写
在GradleDemo中新建一个buildSrc目录,然后在buildSrc目录新建src/main/groovy目录,如果你要使用java或kotlin,则新建src/main/java或src/main/kotlin,src/main/groovy目录下你还可以继续创建package,这里我的package为com.example.plugin,然后在该package下新建一个类MyPlugin.groovy,该类继承自Plugin接口,如下:
1 | class MyPlugin implements Plugin<Project>{ |
现在MyPlugin中没有任何逻辑,我们平时是在build.gradle中通过apply plugin: ‘Plugin名’来引用一个Plugin,而apply plugin中的apply就是指apply方法中的逻辑,而apply方法的参数project指的就是引用该Plugin的build.gradle对应的Project对象,接下来我们让我们在apply方法中编写逻辑,如下:
1 | package com.example.plugin |
上述我在apply方法中创建了一个扩展和一个Task,其中Task好理解,那么扩展是什么?我们平时引用android插件时,一定见过这样类似于android这样的命名空间,如下:
1 | apply plugin: 'com.android.application' |
它并不是一个名为android的方法,它而是android插件中名为android的扩展,该扩展对应一个bean类,该bean类中有compileSdkVersion、buildToolsVersion等方法,所以配置android就是在配置andorid对应的bean类,现在回到我们的MyPlugin中,MyPlugin也定义了一个bean类:OuterExt,该bean类有messag字段,Groovy会自动为我们生成messag的get/set方法,而apply方法中通过project实例的ExtensionContainer的create方法创建一个名为outerExt的扩展,扩展对应的bean类为OuterExt,扩展的名字可以随便起,其中ExtensionContainer类似于TaskContainer,它也是Project中的一个容器,这个容器存放Project中所有的扩展,通过ExtensionContainer的create方法可以创建一个扩展,create方法返回的是扩展对应的类的实例,这样我们使用MyPlugin就可以这样使用,如下:
1 | //subproject_4/build.gradle |
扩展的特点就是可以通过闭包来配置扩展对应的类,这样就可以通过扩展outerExt来配置我们的Plugin,很多自定义Plugin都是都通过添加扩展这种方式来配置自定义的Plugin,很多人就问了,那么类似于android的嵌套DSL如何实现,如下:
1 | apply plugin: 'com.android.application' |
android{}中有嵌套了一个defaultConfig{},但是defaultConfig并不是一个扩展,而是一个名为defaultConfig的方法,参数为Action类型,它是一个接口,里面只有一个execute方法,这里就我参考android 插件的内部实现实现了嵌套DSL,嵌套DSL可以简单的理解为扩展对应的类中再定义一个类,MyPlugin的实现如下:
1 | package com.example.plugin |
使用MyPlugin就可以这样使用,如下:
1 | //subproject_4/build.gradle |
outerExt {}中嵌套了inner{},其中inner是一个方法,参数类型为Action,Gradle内部会把inner方法后面的闭包配置给InnerExt类,这是Gradle中的一种转换机制,总的来说,定义嵌套DSL的大概步骤如下:
- 1、定义嵌套的DSL对应的bean类,如这里为InnerExt;
- 2、定义一个使用@Inject注解的、抽象的获取ObjectFactory实例的get方法,或者定义一个使用@Inject注解的带ObjectFactory类型参数的构造,@Inject是javax包下的,ObjectFactory是属于Gradle的model包下的类,当Gradle实例化OuterExt时,它会自动注入通过@Inject注解的实例,例如这里就自动注入了ObjectFactory实例,需要注意的是通过@Inject注解的方法或构造必须是public的;
- 3、在构造中通过ObjectFactory对象的newInstance方法来创建bean类实例,通过ObjectFactory实例化的对象可以被闭包配置;
- 4、定义一个方法,该方法的参数类型为Action,泛型类型为嵌套的DSL对应的bean类,方法名随便起,如这里为inner,然后在方法中调用Action的execute方法,传入bean类实例.
上面4步就是定义嵌套DSL时需要在自定义Plugin中做的事,还有一点要注意的是,对于扩展对应的bean类,如果你把它定义在自定义的Plugin的类文件中,一定要用static修饰,如这里的OuterExt类、InnerExt类使用了static修饰,或者把它们定义在单独的类文件中。
除了以上这种为单个对象配置的方式,Gradle还为我们提供了更为灵活地对多个相同类型对象进行配置的方式,又名命名对象容器,它类似于android中buildType{}, 如下:
1 | android { |
上面buildTypes中定义了2个命名空间,分别为:release、debug,每个命名空间都会生成一个BuildType配置,在不同的场景下使用,并且我还可以根据使用场景定义更多的命名空间如:test、testDebug等,buildTypes{}中的命名空间的数量不定、名字不定,这是因为buildTypes是通过NamedDomainObjectContainer容器实现的,下面在前面的基础上实现OuterExt对应的命名对象容器:
1 | package com.example.plugin |
使用MyPlugin就可以这样使用,如下:
1 | //subproject_4/build.gradle |
outerExts可以想象为一个容器,然后容器的元素就是OuterExt,放进容器中的元素都要有一个名字,这个名字是任意的,上面我们通过DSL在outerExts扩展中配置了两个OuterExt,一个名为ext1,一个名为ext2,然后我们在Plugin中就可以通过遍历outerExts获取每个元素对应的配置,所以总的来说,定义命名对象容器作为扩展的大概步骤如下:
1、首先要确定NamedDomainObjectContainer容器的元素类型,如我这里为OuterExt类,容器中的每个元素必须要有一个只可读的、名为name的常量字符串,然后提供一个使用@Inject注解的带有String类型参数的构造,在构造中为name赋值,这个name就对应你在DSL中填写的命名,如我这里为ext1、ext2,当我们在DSL中添加命名空间时,其实就是在往容器中添加元素,这时Gradle会自动调用元素带有String类型参数的构造来实例化它,并在构造中传入命名字符串;
2、通过Project的getObjects方法获取ObjectFactory实例,调用ObjectFactory的domainObjectContainer方法创建容器实例,前面讲过通过ObjectFactory实例化的对象可以被闭包配置,如我这里创建的容器为NamedDomainObjectContainer<OuterExt>;
3、然后再通过Project的getExtensions方法获取ExtensionContainer实例,调用ExtensionContainer的add方法添加扩展名和容器实例的映射,如我这里添加了名为outerExts的扩展,扩展对应的类为NamedDomainObjectContainer<OuterExt>.
上面3步就是定义命名对象容器作为扩展时需要在自定义Plugin中做的事,1、2步骤是如何定义一个命名对象容器,2步骤是把命名对象容器添加为一个扩展,但是往往命名对象容器只是作为某个扩展中的一个嵌套DSL,而不是直接作为扩展,这时我们只需要结合前面讲过的定义扩展步骤、定义嵌套DSL步骤和这里的1、2步骤就行,这时我们就可以实现下面的DSL写法:
1 | //扩展 |
这时我们已经很接近android gradle plugin提供的android{}扩展的DSL写法了,在自定义Plugin中如何实现就留给大家自己实现了,只要结合一下前面所讲过的方法就行,上面所介绍的扩展、嵌套DSL、命名对象容器已经能满足自定义Plugin开发时获取配置的大部分场景了。
2、在独立项目中编写
在独立项目中编写和在buildSrc目录下编写是一样的,只是多了一个发布过程,这里我为了方便就不新建一个独立项目了,而是在GradleDemo中新增一个名为gradle_plugin的子项目,然后在gradle_plugin下新建一个src/main/groovy和src/main/resources目录,接着把刚刚在buildSrc编写的com.example.plugin.MyPlugin复制到src/main/groovy下,最后在GradleDemo新建一个repo目录,当作待会发布插件时的仓库,此时GradleDemo结构如下:
在Gradle中有两种方式在脚本中引入一个Plugin,一种是在buildScript中引入类路径,然后通过apply plugin引入插件,如下
1 | //build.gradle |
一种是直接通过plugins DSL引入,如下:
1 | //setting.gradle |
两种方式分别对应着两种类型的发布方式,下面分别介绍:
2.1、自定义META-INF发布
因为gradle_plugin项目中的MyPlugin.groovy使用了Gradle的相关api,如Project等,所以你要在gradle_plugin/build.gradle中引进Gradle api,打开gradle_plugin/build.gradle,添加如下:
1 | //gradle_plugin/build.gradle |
现在MyPlugin已经有了,我们需要给插件起一个名字,在gradle_plugin/src/main/resources目录下新建META-INF/gradle-plugins目录,然后在该目录下新建一个XX.properties文件,XX就是你想要给插件起的名字,就是apply plugin后填写的插件名字,也称为插件id,例如andorid gradle plugin的插件id叫com.android.application,所以它的properties文件为com.android.application.properties,这里我给我的MyPlugin插件起名为myplugin,所以我新建myplugin.properties,如下:
打开myplugin.properties文件,添加如下:
1 | # 在这里指明自定义插件的实现类 |
通过implementation-class指明你要发布的插件的实现类,如这里为com.example.plugin.MyPlugin,接下来我们就来发布插件。
发布插件你可以选择你要发布到的仓库,如maven、lvy,我们最常用的就是maven了,所以这里我选择maven,Gradle提供了maven-publish插件来帮助我们发布到maven仓库,在gradle_plugin/build.gradle添加如下:
1 | //gradle_plugin/build.gradle |
我们首先通过repositories{}定义了发布仓库,这里为本地目录,然后在publications{}中定义了名为myplugin的发布,它会被maven-publish插件用来生成发布任务,在其中通过groupId、artifactId和version配置了插件的GAV坐标,这样发布后我们就可以通过GAV坐标引用插件,即在dependencies{}中使用groupId:artifactId:version的形式来引用我们发布的插件,同时还引入了components.java,它是java插件为我们提供的把代码打包成jar的能力,当你做完这一切后,同步Gradle项目,maven-publish插件会为我们生成两种类型的发布任务:publishXXPublicationToMavenRepository和publishXXPublicationToMavenLocal,publishXXPublicationToMavenRepository任务表示把插件发布到maven远程仓库中,publishXXPublicationToMavenLocal任务表示把插件发布到maven本地仓库,其中XX就是在publications{}定义的发布名字,如我这里为myplugin。
接着在Gradle项目所处目录的命令行输入gradle publishMypluginPublicationToMavenLocal
把myplugin插件发布到maven本地仓库,任务执行成功后,在maven本地仓库(mac下为~/.m/repository/,在window下为:C:/用户/用户名/.m/repository/)中会看到发布的插件jar和pom文件,如下:
接着在Gradle项目所处目录的命令行输入gradle publishMypluginPublicationToMavenRepository
把myplugin插件发布到maven远程仓库,任务执行成功后,在GradleDemo/repo/中会看到发布的插件jar和pom文件,如下:
现在MyPlugin 1.0已经发布成功了,接下来让我们使用这个插件,在subproject_4/build.gradle中添加如下:
1 | //subproject_4/build.gradle |
这里使用了Project的buildscript方法,通过该方法可以为当前项目的build.gradle脚本导入外部依赖,在该方法中可以使用repositories方法和dependencies方法指定外部依赖的所在仓库和类路径,当前项目在构建时会去repositories定义的仓库地址寻找classpath指定路径下的所有jar文件并引入,如我这里repositories方法中指定了mavenLocal()和maven {url uri(‘../repo’)},其中mavenLocal()对应maven的本地仓库即/.m/repository,maven {url uri(‘../repo’)}就是刚刚发布时指定的maven远程仓库,如果你通过发布任务发布插件到了maven中央仓库,则可以新增mavenCentral(),它代表maven中央仓库的远端地址,而dependencies方法中则通过classpath指定了myplugin插件在仓库下的类路径即GAV坐标: com.example.customplugin:myplugin:1.0,现在我们可以通过apply plugin使用myplugin了,在subproject_4/build.gradle中添加如下:
1 | //subproject_4/build.gradle |
其中apply plugin后面的插件id就是我们在gradle_plugin/src/main/resources/META-INF/gradle-plugins目录下编写的properties文件的名称。
现在已经成功发布了一个插件并引入使用,但是上面的发布过程和引入插件过程不免有点繁琐,Gradle为我们提供了java-gradle-plugin用于简化我们的发布过程,同时根据java-gradle-plugin发布的插件使用时只需要直接引入插件id就行,而无需在buildscript中指定classpath。
2.2、使用java-gradle-plugin生成META-INF发布
还是上面MyPlugin的例子,我们在gradle_plugin/build.gradle中引入java-gradle-plugin插件,通过java-gradle-plugin提供的gradlePlugin{}扩展配置插件的META-INF信息,同时通过Project的setGroup方法和setVersion方法定义插件发布的groupId和version,我们还要引入maven-publish插件,因为要依赖它提供的发布能力,如下:
1 | //gradle_plugin/build.gradle |
通过在gradlePlugin{}中的配置,执行发布任务时java-gradle-plugin会自动为我们生成插件的META-INF信息并打包进插件的jar中,同时java-gradle-plugin会自动为我们在dependencies引入gradleApi(),除此之外,执行发布任务时java-gradle-plugin还为插件生成了一个插件标记工件 - Plugin Marker Artifact,它的作用就是用来定位插件的位置,我们在plugins DSL中通过插件id来引用插件时并不需要定义插件的classpath即GAV坐标就可以直接使用插件,那么Gradle是如何根据插件id定位到插件的呢?答案就是插件标记工件,它跟插件发布时一起发布,它里面只有一个pom文件,pom文件里面依赖了插件真正的GAV坐标,这样通过插件id来引用插件时就会先下载这个pom文件,从而解析到插件的GAV坐标,再根据插件的GAV坐标把插件的jar文件引入,那么Gradle又是如何定位到插件标记工件的呢?答案就是使用插件id按一定的规则生成插件标记工件的GAV坐标,插件标记工件的GAV生成规则为pluginId:pluginId.gradle.plugin:pluginVersion,如这里myplugin的插件标记工件生成的GAV为com.example.customplugin.myplugin:com.example.customplugin.myplugin.gradle.plugin:2.0,发布插件时会同时把插件标记工件发布到生成的GAV处,然后通过插件id引用时又根据插件id拼接出相同的GAV从而定位到插件标记工件,这么说有点绕,让我们执行发布任务看看生成的产物就懂了。
我们在Gradle项目所处目录的命令行输入gradle publishPluginMavenPublicationToMavenRepository publishMypluginPluginMarkerMavenPublicationToMavenRepository
来执行插件发布任务和插件标记工件发布任务,这两个任务都是maven-publish根据java-gradle-plugin定义的发布过程为我们生成好的,除此之外还生成了发布到maven本地仓库的任务,这里只以发布到maven远程仓库任务做示例,两个任务执行成功后,在GradleDemo/repo/中会看到发布的插件和插件标识工件,如下:
可以看到在repo下发布了两个组件,一个为插件,artifactId为gradle_plugin,一个为插件标识工件,artifactId为com.example.customplugin.myplugin.gradle.plugin,它们都处于com.example.customplugin空间下,版本都为2.0,我们看一下插件标识工件里面的内容,如下:
可以看到插件标识工件中并没有jar文件只有pom文件,我们打开它的pom文件,如下:
可以看到依赖了真正的插件的GAV坐标,为com.example.customplugin:gradle_plugin:2.0,这样通过插件id引用时就可以根据插件标识工件定位到插件,通过java-gradle-plugin这种方式发布插件时还有一点要注意的是,插件的id最好以groupId为前缀,这样才能保证插件标识工件和插件发布时都处于同一个仓库路径。
现在MyPlugin 2.0已经发布成功了,接下来让我们使用这个插件,需要通过plugins DSL引用的插件不需要定义classpath,但还是要定义仓库位置,我们在settings.gradle中添加如下:
1 | //settings.gradle |
plugins DSL通过pluginManagement{}管理插件仓库还有插件,而且pluginManagement{}必须要放在settings.gradle脚本的头部,然后我们可以通过plugins DSL使用myplugin了,在subproject_4/build.gradle中添加如下:
1 | //subproject_4/build.gradle |
plugins DSL根据插件id寻找插件时默认会先从Gradle中央仓库 - Gradle Plugin Portal找,找不到再从pluginManagement中指定的仓库找。
上面发布时为了讲解原理把发布任务逐个执行,其实maven-publish还为我们生成了更为方便的publishAllPublicationsToMavenRepository 、publishToMavenLocal、publish发布任务,只需要执行一个任务就可以把多个发布任务同时执行:
执行publishAllPublicationsToMavenRepository任务表示执行所有发布到maven远程仓库的任务;
执行publishToMavenLocal任务表示执行所有发布到maven本地仓库的任务;
执行publish任务表示执行publishAllPublicationsToMavenRepository和publishToMavenLocal任务
结语
本文通过Gradle的特点、项目结构、生命周期、Task、自定义Plugin等来全面的学习Gradle,掌握上面的知识已经足够让你入门Gradle,但是如果你想要更进一步的学习Gradle,掌握本文的知识点是完全不足的,你可能还需要熟悉掌握Project中各个api的使用、能独立的自定义一个有完整功能的Plugin、能熟练地编写Gradle脚本来管理项目等,下面有一份Gradle DSL Reference,你可以在需要来查找Gradle相关配置项的含义和用法:
对于android开发者,我们引入的android插件中也有很多配置项,下面的Android Plugin DSL Reference,你可以在需要来查找android 插件相关配置项的含义和用法:
本文的源码位置:
以上就是本文的全部内容,希望大家有所收获!
参考内容: