关于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/

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

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

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据