gradle中的增量構建

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

自定義inputs和outputs

既然task中的input和output在增量編譯中這麼重要,本章將會給你們講解一下怎麼纔可以在task中定義input和output。ui

若是咱們自定義一個task類型,那麼知足下面兩點就可使用上增量構建了:this

第一點,須要爲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 ,表示的是類路徑上的文件,對於類路徑上的文件須要考慮文件的順序。若是類路徑上的文件是jar的話,jar中的文件建立時間戳的修改,並不會影響input。

  • @CompileClasspath:至關於Iterable ,表示的是類路徑上的java文件,會忽略類路徑上的非java文件。

  • @OutputFile: 至關於File,表示輸出文件。

  • @OutputFiles: 至關於Map<String, File> 或者 Iterable ,表示輸出文件。

  • @OutputDirectories: 至關於Map<String, File> 或者 Iterable ,表示輸出文件。

  • @Destroys: 至關於File 或者 Iterable ,表示這個task將會刪除的文件。

  • @LocalState: 至關於File 或者 Iterable ,表示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/

最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!

歡迎關注個人公衆號:「程序那些事」,懂技術,更懂你!

相關文章
相關標籤/搜索