gradle中的增量构建java
在咱们使用的各类工具中,为了提高工做效率,总会使用到各类各样的缓存技术,好比说docker中的layer就是缓存了以前构建的image。在gradle中这种以task组合起来的构建工具也不例外,在gradle中,这种技术叫作增量构建。docker
gradle为了提高构建的效率,提出了增量构建的概念,为了实现增量构建,gradle将每个task都分红了三部分,分别是input输入,任务自己和output输出。下图是一个典型的java编译的task。api
以上图为例,input就是目标jdk的版本,源代码等,output就是编译出来的class文件。缓存
增量构建的原理就是监控input的变化,只有input发送变化了,才从新执行task任务,不然gradle认为能够重用以前的执行结果。安全
因此在编写gradle的task的时候,须要指定task的输入和输出。工具
而且要注意只有会对输出结果产生变化的才能被称为输入,若是你定义了对初始结果彻底无关的变量做为输入,则这些变量的变化会致使gradle从新执行task,致使了没必要要的性能的损耗。性能
还要注意不肯定执行结果的任务,好比说一样的输入可能会获得不一样的输出结果,那么这样的任务将不可以被配置为增量构建任务。gradle
既然task中的input和output在增量编译中这么重要,本章将会给你们讲解一下怎么才可以在task中定义input和output。ui
若是咱们自定义一个task类型,那么知足下面两点就可使用上增量构建了:this
第一点,须要为task中的inputs和outputs添加必要的getter方法。
第二点,为getter方法添加对应的注解。
gradle支持三种主要的inputs和outputs类型:
简单类型:简单类型就是全部实现了Serializable接口的类型,好比说string和数字。
文件类型:文件类型就是 File 或者 FileCollection 的衍生类型,或者其余能够做为参数传递给 Project.file(java.lang.Object) 和 Project.files(java.lang.Object...) 的类型。
嵌套类型:有些自定义类型,自己不属于前面的1,2两种类型,可是它内部含有嵌套的inputs和outputs属性,这样的类型叫作嵌套类型。
接下来,咱们来举个例子,假如咱们有一个相似于FreeMarker和Velocity这样的模板引擎,负责将模板源文件,要传递的数据最后生成对应的填充文件,咱们考虑一下他的输入和输出是什么。
输入:模板源文件,模型数据和模板引擎。
输出:要输出的文件。
若是咱们要编写一个适用于模板转换的task,咱们能够这样写:
import java.io.File; import java.util.HashMap; import org.gradle.api.*; import org.gradle.api.file.*; import org.gradle.api.tasks.*; public class ProcessTemplates extends DefaultTask { private TemplateEngineType templateEngine; private FileCollection sourceFiles; private TemplateData templateData; private File outputDir; @Input public TemplateEngineType getTemplateEngine() { return this.templateEngine; } @InputFiles public FileCollection getSourceFiles() { return this.sourceFiles; } @Nested public TemplateData getTemplateData() { return this.templateData; } @OutputDirectory public File getOutputDir() { return this.outputDir; } // 上面四个属性的setter方法 @TaskAction public void processTemplates() { // ... } }
上面的例子中,咱们定义了4个属性,分别是TemplateEngineType,FileCollection,TemplateData和File。前面三个属性是输入,后面一个属性是输出。
除了getter和setter方法以外,咱们还须要在getter方法中添加相应的注释: @Input , @InputFiles ,@Nested 和 @OutputDirectory
, 除此以外,咱们还定义了一个 @TaskAction
表示这个task要作的工做。
TemplateEngineType表示的是模板引擎的类型,好比FreeMarker或者Velocity等。咱们也能够用String来表示模板引擎的名字。可是为了安全起见,这里咱们自定义了一个枚举类型,在枚举类型内部咱们能够安全的定义各类支持的模板引擎类型。
由于enum默认是实现Serializable的,因此这里能够做为@Input使用。
sourceFiles使用的是FileCollection,表示的是一系列文件的集合,因此可使用@InputFiles。
为何TemplateData是@Nested类型的呢?TemplateData表示的是咱们要填充的数据,咱们看下它的实现:
import java.util.HashMap; import java.util.Map; import org.gradle.api.tasks.Input; public class TemplateData { private String name; private Map<String, String> variables; public TemplateData(String name, Map<String, String> variables) { this.name = name; this.variables = new HashMap<>(variables); } @Input public String getName() { return this.name; } @Input public Map<String, String> getVariables() { return this.variables; } }
能够看到,虽然TemplateData自己不是File或者简单类型,可是它内部的属性是简单类型的,因此TemplateData自己能够看作是@Nested的。
outputDir表示的是一个输出文件目录,因此使用的是@OutputDirectory。
使用了这些注解以后,gradle在构建的时候就会检测和上一次构建相比,这些属性有没有发送变化,若是没有发送变化,那么gradle将会直接使用上一次构建生成的缓存。
注意,上面的例子中咱们使用了FileCollection做为输入的文件集合,考虑一种状况,假如只有文件集合中的某一个文件发送变化,那么gradle是会从新构建全部的文件,仍是只重构这个被修改的文件呢?
留给你们讨论
除了上讲到的4个注解以外,gradle还提供了其余的几个有用的注解:
@InputFile: 至关于File,表示单个input文件。
@InputDirectory: 至关于File,表示单个input目录。
@Classpath: 至关于Iterable
@CompileClasspath:至关于Iterable
@OutputFile: 至关于File,表示输出文件。
@OutputFiles: 至关于Map<String, File> 或者 Iterable
@OutputDirectories: 至关于Map<String, File> 或者 Iterable
@Destroys: 至关于File 或者 Iterable
@LocalState: 至关于File 或者 Iterable
@Console: 表示属性不是input也不是output,可是会影响console的输出。
@Internal: 内部属性,不是input也不是output。
@ReplacedBy: 属性被其余的属性替换了,不能算在input和output中。
@SkipWhenEmpty: 和@InputFiles 跟 @InputDirectory一块儿使用,若是相应的文件或者目录为空的话,将会跳过task的执行。
@Incremental: 和@InputFiles 跟 @InputDirectory一块儿使用,用来跟踪文件的变化。
@Optional: 忽略属性的验证。
@PathSensitive: 表示须要考虑paths中的哪一部分做为增量的依据。
自定义task固然是一个很是好的办法来使用增量构建。可是自定义task类型须要咱们编写新的class文件。有没有什么办法能够不用修改task的源代码,就可使用增量构建呢?
答案是使用Runtime API。
gradle提供了三个API,用来对input,output和Destroyables进行获取:
Task.getInputs() of type TaskInputs
Task.getOutputs() of type TaskOutputs
Task.getDestroyables() of type TaskDestroyables
获取到input和output以后,咱们就是能够其进行操做了,咱们看下怎么用runtime API来实现以前的自定义task:
task processTemplatesAdHoc { inputs.property("engine", TemplateEngineType.FREEMARKER) inputs.files(fileTree("src/templates")) .withPropertyName("sourceFiles") .withPathSensitivity(PathSensitivity.RELATIVE) inputs.property("templateData.name", "docs") inputs.property("templateData.variables", [year: 2013]) outputs.dir("$buildDir/genOutput2") .withPropertyName("outputDir") doLast { // Process the templates here } }
上面例子中,inputs.property() 至关于 @Input ,而outputs.dir() 至关于@OutputDirectory。
Runtime API还能够和自定义类型一块儿使用:
task processTemplatesWithExtraInputs(type: ProcessTemplates) { // ... inputs.file("src/headers/headers.txt") .withPropertyName("headers") .withPathSensitivity(PathSensitivity.NONE) }
上面的例子为ProcessTemplates添加了一个input。
除了直接使用dependsOn以外,咱们还可使用隐式依赖:
task packageFiles(type: Zip) { from processTemplates.outputs }
上面的例子中,packageFiles 使用了from,隐式依赖了processTemplates的outputs。
gradle足够智能,能够检测到这种依赖关系。
上面的例子还能够简写为:
task packageFiles2(type: Zip) { from processTemplates }
咱们看一个错误的隐式依赖的例子:
plugins { id 'java' } task badInstrumentClasses(type: Instrument) { classFiles = fileTree(compileJava.destinationDir) destinationDir = file("$buildDir/instrumented") }
这个例子的本意是执行compileJava任务,而后将其输出的destinationDir做为classFiles的值。
可是由于fileTree自己并不包含依赖关系,因此上面的执行的结果并不会执行compileJava任务。
咱们能够这样改写:
task instrumentClasses(type: Instrument) { classFiles = compileJava.outputs.files destinationDir = file("$buildDir/instrumented") }
或者使用layout:
task instrumentClasses2(type: Instrument) { classFiles = layout.files(compileJava) destinationDir = file("$buildDir/instrumented") }
或者使用buildBy:
task instrumentClassesBuiltBy(type: Instrument) { classFiles = fileTree(compileJava.destinationDir) { builtBy compileJava } destinationDir = file("$buildDir/instrumented") }
gradle会默认对@InputFile ,@InputDirectory 和 @OutputDirectory 进行参数校验。
若是你以为这些参数是可选的,那么可使用@Optional。
上面的例子中,咱们使用from来进行增量构建,可是from并无添加@InputFiles, 那么它的增量缓存是怎么实现的呢?
咱们看一个例子:
public class ProcessTemplates extends DefaultTask { // ... private FileCollection sourceFiles = getProject().getLayout().files(); @SkipWhenEmpty @InputFiles @PathSensitive(PathSensitivity.NONE) public FileCollection getSourceFiles() { return this.sourceFiles; } public void sources(FileCollection sourceFiles) { this.sourceFiles = this.sourceFiles.plus(sourceFiles); } // ... }
上面的例子中,咱们将sourceFiles定义为可缓存的input,而后又定义了一个sources方法,能够将新的文件加入到sourceFiles中,从而改变sourceFile input,也就达到了自定义修改input缓存的目的。
咱们看下怎么使用:
task processTemplates(type: ProcessTemplates) { templateEngine = TemplateEngineType.FREEMARKER templateData = new TemplateData("test", [year: 2012]) outputDir = file("$buildDir/genOutput") sources fileTree("src/templates") }
咱们还可使用project.layout.files()将一个task的输出做为输入,能够这样作:
public void sources(Task inputTask) { this.sourceFiles = this.sourceFiles.plus(getProject().getLayout().files(inputTask)); }
这个方法传入一个task,而后使用project.layout.files()将task的输出做为输入。
看下怎么使用:
task copyTemplates(type: Copy) { into "$buildDir/tmp" from "src/templates" } task processTemplates2(type: ProcessTemplates) { // ... sources copyTemplates }
很是的方便。
若是你不想使用gradle的缓存功能,那么可使用upToDateWhen()来手动控制:
task alwaysInstrumentClasses(type: Instrument) { classFiles = layout.files(compileJava) destinationDir = file("$buildDir/instrumented") outputs.upToDateWhen { false } }
上面使用false,表示alwaysInstrumentClasses这个task将会一直被执行,并不会使用到缓存。
要想比较gradle的输入是不是同样的,gradle须要对input进行归一化处理,而后才进行比较。
咱们能够自定义gradle的runtime classpath 。
normalization { runtimeClasspath { ignore 'build-info.properties' } }
上面的例子中,咱们忽略了classpath中的一个文件。
咱们还能够忽略META-INF中的manifest文件的属性:
normalization { runtimeClasspath { metaInf { ignoreAttribute("Implementation-Version") } } }
忽略META-INF/MANIFEST.MF :
normalization { runtimeClasspath { metaInf { ignoreManifest() } } }
忽略META-INF中全部的文件和目录:
normalization { runtimeClasspath { metaInf { ignoreCompletely() } } }
若是你的gradle由于某种缘由暂停了,你能够送 --continuous 或者 -t 参数,来重用以前的缓存,继续构建gradle项目。
你还可使用 --parallel 来并行执行task。
本文已收录于 http://www.flydean.com/gradle-incremental-build/
最通俗的解读,最深入的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注个人公众号:「程序那些事」,懂技术,更懂你!