乐趣区

关于maven:gradle中的增量构建

简介

在咱们应用的各种工具中,为了晋升工作效率,总会应用到各种各样的缓存技术,比如说 docker 中的 layer 就是缓存了之前构建的 image。在 gradle 中这种以 task 组合起来的构建工具也不例外,在 gradle 中,这种技术叫做增量构建。

增量构建

gradle 为了晋升构建的效率,提出了增量构建的概念,为了实现增量构建,gradle 将每一个 task 都分成了三局部,别离是 input 输出,工作自身和 output 输入。下图是一个典型的 java 编译的 task。

以上图为例,input 就是指标 jdk 的版本,源代码等,output 就是编译进去的 class 文件。

增量构建的原理就是监控 input 的变动,只有 input 发送变动了,才从新执行 task 工作,否则 gradle 认为能够重用之前的执行后果。

所以在编写 gradle 的 task 的时候,须要指定 task 的输出和输入。

并且要留神只有会对输入后果产生变动的能力被称为输出,如果你定义了对初始后果齐全无关的变量作为输出,则这些变量的变动会导致 gradle 从新执行 task,导致了不必要的性能的损耗。

还要留神不确定执行后果的工作,比如说同样的输出可能会失去不同的输入后果,那么这样的工作将不可能被配置为增量构建工作。

自定义 inputs 和 outputs

既然 task 中的 input 和 output 在增量编译中这么重要,本章将会给大家解说一下怎么才可能在 task 中定义 input 和 output。

如果咱们自定义一个 task 类型,那么满足上面两点就能够应用上增量构建了:

第一点,须要为 task 中的 inputs 和 outputs 增加必要的 getter 办法。

第二点,为 getter 办法增加对应的注解。

gradle 反对三种次要的 inputs 和 outputs 类型:

  1. 简略类型:简略类型就是所有实现了 Serializable 接口的类型,比如说 string 和数字。
  2. 文件类型:文件类型就是 File 或者 FileCollection 的衍生类型,或者其余能够作为参数传递给 Project.file(java.lang.Object) 和 Project.files(java.lang.Object…) 的类型。
  3. 嵌套类型:有些自定义类型,自身不属于后面的 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<File>,示意的是类门路上的文件,对于类门路上的文件须要思考文件的程序。如果类门路上的文件是 jar 的话,jar 中的文件创建工夫戳的批改,并不会影响 input。
  • @CompileClasspath:相当于 Iterable<File>,示意的是类门路上的 java 文件,会疏忽类门路上的非 java 文件。
  • @OutputFile:相当于 File,示意输入文件。
  • @OutputFiles:相当于 Map<String, File> 或者 Iterable<File>,示意输入文件。
  • @OutputDirectories:相当于 Map<String, File> 或者 Iterable<File>,示意输入文件。
  • @Destroys:相当于 File 或者 Iterable<File>,示意这个 task 将会删除的文件。
  • @LocalState:相当于 File 或者 Iterable<File>,示意 task 的本地状态。
  • @Console:示意属性不是 input 也不是 output,然而会影响 console 的输入。
  • @Internal:外部属性,不是 input 也不是 output。
  • @ReplacedBy:属性被其余的属性替换了,不能算在 input 和 output 中。
  • @SkipWhenEmpty:和 @InputFiles 跟 @InputDirectory 一起应用,如果相应的文件或者目录为空的话,将会跳过 task 的执行。
  • @Incremental:和 @InputFiles 跟 @InputDirectory 一起应用,用来跟踪文件的变动。
  • @Optional:疏忽属性的验证。
  • @PathSensitive:示意须要思考 paths 中的哪一部分作为增量的根据。

运行时 API

自定义 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/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

退出移动版