任何建置工具的重要部分是避免執行已完成的工作的能力。考慮編譯的過程。一旦您的原始碼檔案經過編譯,除非有影響輸出的變更,例如修改原始碼檔案或移除輸出檔案,否則不應需要重新編譯它們。而且編譯可能需要相當長的時間,因此在不需要時跳過步驟可以節省大量時間。
Gradle 透過稱為增量建置的功能,開箱即用地支援此行為。您幾乎肯定已經在實際操作中看過它。當您執行任務且任務在主控台輸出中標記為 UP-TO-DATE
時,表示增量建置正在運作。
增量建置如何運作?您如何確保您的任務支援增量執行?讓我們來看看。
任務輸入和輸出
在最常見的情況下,任務會取得一些輸入並產生一些輸出。我們可以將 Java 編譯過程視為任務範例。Java 原始碼檔案充當任務的輸入,而產生的類別檔案,即編譯結果,是任務的輸出。

輸入的一個重要特徵是它會影響一個或多個輸出,如您從上圖所見。根據原始碼檔案的內容和您想要執行程式碼的 Java 執行階段最低版本,會產生不同的位元組碼。這使它們成為任務輸入。但是,編譯可用的最大記憶體為 500MB 或 600MB(由 memoryMaximumSize
屬性決定),對產生的位元組碼沒有任何影響。在 Gradle 術語中,memoryMaximumSize
只是一個內部任務屬性。
作為增量建置的一部分,Gradle 會測試自上次建置以來,是否有任何任務輸入或輸出發生變更。如果沒有,Gradle 可以認為任務是最新的,因此可以跳過執行其動作。另請注意,除非任務至少有一個任務輸出,否則增量建置將無法運作,儘管任務通常也至少有一個輸入。
這對建置作者的意義很簡單:您需要告訴 Gradle 哪些任務屬性是輸入,哪些是輸出。如果任務屬性影響輸出,請務必將其註冊為輸入,否則任務在不應該是最新的時候會被視為最新的。相反地,如果屬性不影響輸出,請不要將其註冊為輸入,否則任務可能會在不需要時執行。另請注意非決定性任務,這些任務可能會針對完全相同的輸入產生不同的輸出:這些任務不應配置為增量建置,因為最新檢查將無法運作。
現在讓我們看看如何將任務屬性註冊為輸入和輸出。
透過註解宣告輸入和輸出
如果您將自訂任務實作為類別,則只需兩個步驟即可使其與增量建置協同運作
-
為每個任務輸入和輸出建立類型化屬性(透過 getter 方法)
-
將適當的註解新增至每個屬性
註解必須放在 getter 或 Groovy 屬性上。放在 setter 或沒有對應註解 getter 的 Java 欄位上的註解會被忽略。 |
Gradle 支援四個主要類別的輸入和輸出
-
簡單值
例如字串和數字。更廣泛地說,簡單值可以具有任何實作
Serializable
的類型。 -
檔案系統類型
這些類型包含
RegularFile
、Directory
和標準File
類別,但也包含 Gradle FileCollection 類型的衍生類型,以及可以傳遞至 Project.file(java.lang.Object) 方法(用於單一檔案/目錄屬性)或 Project.files(java.lang.Object...) 方法的任何其他類型。 -
依賴解析結果
這包括用於成品中繼資料的 ResolvedArtifactResult 類型和用於依賴圖形的 ResolvedComponentResult 類型。請注意,它們僅在包裝在
Provider
中時才受支援。 -
巢狀值
不符合其他兩個類別的自訂類型,但具有自己的屬性,這些屬性是輸入或輸出。實際上,任務輸入或輸出巢狀在這些自訂類型內。
例如,假設您有一個任務,用於處理各種不同類型的範本,例如 FreeMarker、Velocity、Moustache 等。它會取得範本原始碼檔案,並將它們與一些模型資料結合,以產生範本檔案的已填入版本。
此任務將具有三個輸入和一個輸出
-
範本原始碼檔案
-
模型資料
-
範本引擎
-
輸出檔案的寫入位置
當您撰寫自訂任務類別時,可以透過註解輕鬆將屬性註冊為輸入或輸出。為了示範,以下是一個骨架任務實作,其中包含一些合適的輸入和輸出,以及它們的註解
package org.example;
import java.util.HashMap;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileSystemOperations;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.*;
import javax.inject.Inject;
public abstract class ProcessTemplates extends DefaultTask {
@Input
public abstract Property<TemplateEngineType> getTemplateEngine();
@InputFiles
public abstract ConfigurableFileCollection getSourceFiles();
@Nested
public abstract TemplateData getTemplateData();
@OutputDirectory
public abstract DirectoryProperty getOutputDir();
@Inject
public abstract FileSystemOperations getFs();
@TaskAction
public void processTemplates() {
// ...
}
}
package org.example;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
public abstract class TemplateData {
@Input
public abstract Property<String> getName();
@Input
public abstract MapProperty<String, String> getVariables();
}
gradle processTemplates
的輸出> gradle processTemplates > Task :processTemplates BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 up-to-date
gradle processTemplates
的輸出(再次執行)> gradle processTemplates > Task :processTemplates UP-TO-DATE BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 up-to-date
此範例有很多值得討論的地方,因此讓我們依序瞭解每個輸入和輸出屬性
-
templateEngine
表示處理原始範本時要使用的引擎,例如 FreeMarker、Velocity 等。您可以將其實作為字串,但在這種情況下,我們使用了自訂列舉,因為它提供更佳的類型資訊和安全性。由於列舉會自動實作
Serializable
,因此我們可以將其視為簡單值並使用@Input
註解,就像使用String
屬性一樣。 -
sourceFiles
任務將處理的原始範本。單一檔案和檔案集合需要它們自己的特殊註解。在這種情況下,我們正在處理輸入檔案的集合,因此我們使用
@InputFiles
註解。您稍後將在表格中看到更多面向檔案的註解。 -
templateData
對於此範例,我們使用自訂類別來表示模型資料。但是,它未實作
Serializable
,因此我們無法使用@Input
註解。這不是問題,因為TemplateData
中的屬性(字串和具有可序列化類型參數的雜湊對應)是可序列化的,並且可以使用@Input
進行註解。我們在templateData
上使用@Nested
,以告知 Gradle 這是一個具有巢狀輸入屬性的值。 -
outputDir
產生檔案的目錄。與輸入檔案一樣,輸出檔案和目錄也有多個註解。表示單一目錄的屬性需要
@OutputDirectory
。您很快就會瞭解其他屬性。
這些註解屬性表示,如果自上次 Gradle 執行任務以來,原始碼檔案、範本引擎、模型資料或產生檔案均未變更,則 Gradle 將跳過任務。這通常可以節省大量時間。您可以瞭解 Gradle 如何偵測稍後的變更。
此範例特別有趣,因為它適用於原始碼檔案的集合。如果只有一個原始碼檔案變更會發生什麼事?任務會再次處理所有原始碼檔案,還是只處理修改後的檔案?這取決於任務實作。如果是後者,則任務本身是增量的,但這與我們在此處討論的功能不同。Gradle 透過其 增量任務輸入 功能協助任務實作者。
現在您已經在實務中看到了一些輸入和輸出註解,讓我們看看所有可用的註解以及何時應該使用它們。下表列出了可用的註解以及您可以與每個註解搭配使用的對應屬性類型。
註解 | 預期的屬性類型 | 描述 |
---|---|---|
任何 |
簡單的輸入值或依賴解析結果 |
|
|
單一輸入檔案(非目錄) |
|
|
單一輸入目錄(非檔案) |
|
|
輸入檔案和目錄的可迭代物件 |
|
|
代表 Java 類別路徑的輸入檔案和目錄的可迭代物件。這可讓任務忽略屬性的不相關變更,例如相同檔案的不同名稱。它類似於註解屬性 注意: |
|
|
代表 Java 編譯類別路徑的輸入檔案和目錄的可迭代物件。這可讓任務忽略不影響類別路徑中類別 API 的不相關變更。另請參閱 使用類別路徑註解。 以下類別路徑的變更將被忽略
注意 - |
|
|
單一輸出檔案(非目錄) |
|
|
單一輸出目錄(非檔案) |
|
|
輸出檔案的可迭代物件或對應。使用檔案樹會關閉任務的 快取。 |
|
|
輸出目錄的可迭代物件。使用檔案樹會關閉任務的 快取。 |
|
|
指定此任務移除的一個或多個檔案。請注意,任務可以定義輸入/輸出或可銷毀物件,但不能同時定義兩者。 |
|
|
指定代表任務本機狀態的一個或多個檔案。當從快取載入任務時,會移除這些檔案。 |
|
任何自訂類型 |
可能未實作 |
|
任何類型 |
表示屬性既不是輸入也不是輸出。它只會以某種方式影響任務的主控台輸出,例如增加或減少任務的詳細程度。 |
|
任何類型 |
表示屬性在內部使用,但既不是輸入也不是輸出。 |
|
任何類型 |
表示屬性已被另一個屬性取代,應作為輸入或輸出被忽略。 |
|
|
與 暗示 |
|
|
與 |
|
任何類型 |
||
|
||
|
與 |
|
|
與 |
與上述類似, |
註解是從所有父類型(包括實作的介面)繼承而來。屬性類型註解會覆寫父類型中宣告的任何其他屬性類型註解。透過這種方式,@InputFile
屬性可以在子任務類型中轉換為 @InputDirectory
屬性。
在類型中宣告的屬性上的註解會覆寫超類別和任何實作介面宣告的類似註解。超類別註解優先於實作介面中宣告的註解。
表格中的 Console 和 Internal 註解是特殊情況,因為它們未宣告任務輸入或任務輸出。那麼為什麼要使用它們呢?這是為了讓您可以利用 Java Gradle 外掛程式開發外掛程式 來協助您開發和發佈自己的外掛程式。此外掛程式會檢查您的自訂任務類別的任何屬性是否缺少增量建置註解。這可保護您在開發期間不會忘記新增適當的註解。
使用類別路徑註解
除了 @InputFiles
之外,對於與 JVM 相關的任務,Gradle 還瞭解類別路徑輸入的概念。當 Gradle 尋找變更時,執行階段和編譯類別路徑的處理方式不同。
與使用 @InputFiles
註解的輸入屬性相反,對於類別路徑屬性,檔案集合中項目的順序很重要。另一方面,類別路徑本身上的目錄和 jar 檔案的名稱和路徑會被忽略。時間戳記和類別路徑上 jar 檔案內類別檔案和資源的順序也會被忽略,因此重新建立具有不同檔案日期的 jar 檔案不會使任務過時。
使用 @CompileClasspath
註解的輸入屬性會被視為 Java 編譯類別路徑。除了上述一般類別路徑規則之外,編譯類別路徑還會忽略除類別檔案之外的所有變更。Gradle 使用 Java 編譯避免 中描述的相同類別分析,以進一步篩選不影響類別 ABI 的變更。這表示僅觸及類別實作的變更不會使任務過時。
巢狀輸入
當分析 @Nested
任務屬性以取得宣告的輸入和輸出子屬性時,Gradle 會使用實際值的類型。因此,它可以探索執行階段子類型宣告的所有子屬性。
當將 @Nested
新增至可迭代物件時,每個元素都會被視為個別的巢狀輸入。可迭代物件中的每個巢狀輸入都會被指派一個名稱,預設名稱是錢字號,後接可迭代物件中的索引,例如 $2
。如果可迭代物件的元素實作 Named
,則該名稱會用作屬性名稱。如果並非所有元素都實作 Named
,則可迭代物件中元素的順序對於可靠的最新檢查和快取至關重要。不允許具有相同名稱的多個元素。
當將 @Nested
新增至對應時,則會為每個值新增巢狀輸入,並將索引鍵用作名稱。
巢狀輸入的類型和類別路徑也會被追蹤。這可確保對巢狀輸入實作的變更會導致建置過時。透過這樣做,也可以將使用者提供的程式碼新增為輸入,例如,透過使用 @Nested
註解 @Action
屬性。請注意,應追蹤此類動作的任何輸入,方法是在動作上使用註解屬性,或手動將它們註冊到任務。
使用巢狀輸入可為任務提供更豐富的建模和擴充性,例如 Test.getJvmArgumentProviders() 所示。
這讓我們可以為 JaCoCo Java 代理建模,從而宣告必要的 JVM 引數,並為 Gradle 提供輸入和輸出
class JacocoAgent implements CommandLineArgumentProvider {
private final JacocoTaskExtension jacoco;
public JacocoAgent(JacocoTaskExtension jacoco) {
this.jacoco = jacoco;
}
@Nested
@Optional
public JacocoTaskExtension getJacoco() {
return jacoco.isEnabled() ? jacoco : null;
}
@Override
public Iterable<String> asArguments() {
return jacoco.isEnabled() ? ImmutableList.of(jacoco.getAsJvmArg()) : Collections.<String>emptyList();
}
}
test.getJvmArgumentProviders().add(new JacocoAgent(extension));
為了使其運作,JacocoTaskExtension
需要具有正確的輸入和輸出註解。
此方法適用於測試 JVM 引數,因為 Test.getJvmArgumentProviders()
是使用 @Nested
註解的 Iterable
。
還有其他任務類型也提供此類巢狀輸入
-
JavaExec.getArgumentProviders() - 模型,例如自訂工具
-
JavaExec.getJvmArgumentProviders() - 用於 Jacoco Java 代理
-
CompileOptions.getCompilerArgumentProviders() - 模型,例如註解處理器
-
Exec.getArgumentProviders() - 模型,例如自訂工具
-
JavaCompile.getOptions().getForkOptions().getJvmArgumentProviders() - 模型 Java 編譯器守護程序命令列引數
-
GroovyCompile.getGroovyOptions().getForkOptions().getJvmArgumentProviders() - 模型 Groovy 編譯器守護程序命令列引數
-
ScalaCompile.getScalaOptions().getForkOptions().getJvmArgumentProviders() - 模型 Scala 編譯器守護程序命令列引數
同樣地,此類建模適用於自訂任務。
透過執行階段 API 宣告輸入和輸出
自訂任務類別是將您自己的建置邏輯帶入增量建置領域的簡單方法,但您並非總是具有該選項。這就是 Gradle 也提供替代 API 的原因,該 API 可以與任何任務搭配使用,我們接下來將介紹。
當您無法存取自訂任務類別的來源時,就無法新增我們在上一節中介紹的任何註解。幸運的是,Gradle 為此類情境提供執行階段 API。它也可以用於特設任務,如您接下來將看到的。
宣告特設任務的輸入和輸出
此執行階段 API 是透過一對適當命名的屬性提供,這些屬性在每個 Gradle 任務上都可用
這些物件具有方法,可讓您指定構成任務輸入和輸出的檔案、目錄和值。實際上,執行階段 API 幾乎具有與註解相同的功能。
它缺少以下項目的等效項
讓我們採用之前的範本處理範例,看看它作為使用執行階段 API 的特設任務會是什麼樣子
tasks.register("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", mapOf("year" to "2013"))
outputs.dir(layout.buildDirectory.dir("genOutput2"))
.withPropertyName("outputDir")
doLast {
// Process the templates here
}
}
tasks.register('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(layout.buildDirectory.dir('genOutput2'))
.withPropertyName('outputDir')
doLast {
// Process the templates here
}
}
gradle processTemplatesAdHoc
的輸出> gradle processTemplatesAdHoc > Task :processTemplatesAdHoc BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 executed
與之前一樣,有很多值得討論的地方。首先,您應該真正為此撰寫自訂任務類別,因為它是一個非平凡的實作,具有多個配置選項。在這種情況下,沒有任務屬性來儲存根原始碼資料夾、輸出目錄的位置或任何其他設定。這是刻意強調執行階段 API 不需要任務具有任何狀態的事實。就增量建置而言,上述特設任務的行為將與自訂任務類別相同。
所有輸入和輸出定義都是透過 inputs
和 outputs
上的方法完成,例如 property()
、files()
和 dir()
。Gradle 會對引數值執行最新檢查,以判斷任務是否需要再次執行。每個方法都對應於其中一個增量建置註解,例如 inputs.property()
對應於 @Input
,而 outputs.dir()
對應於 @OutputDirectory
。
任務移除的檔案可以透過 destroyables.register()
指定。
tasks.register("removeTempDir") {
val tmpDir = layout.projectDirectory.dir("tmpDir")
destroyables.register(tmpDir)
doLast {
tmpDir.asFile.deleteRecursively()
}
}
tasks.register('removeTempDir') {
def tempDir = layout.projectDirectory.dir('tmpDir')
destroyables.register(tempDir)
doLast {
tempDir.asFile.deleteDir()
}
}
執行時期 API 和註解之間一個顯著的差異是缺少直接對應於 @Nested
的方法。這就是範例針對範本資料使用兩個 property()
宣告的原因,每個 TemplateData
屬性各一個。當搭配巢狀值使用執行時期 API 時,您應該運用相同的技巧。任何給定的任務可以宣告可銷毀物或輸入/輸出,但不能同時宣告兩者。
細部配置
執行時期 API 方法僅允許您宣告輸入和輸出本身。然而,以檔案為導向的方法會傳回一個建構器 (builder) — 類型為 TaskInputFilePropertyBuilder — 可讓您提供關於這些輸入和輸出的額外資訊。
您可以在其 API 文件中了解建構器提供的所有選項,但我們將在此處展示一個簡單的範例,讓您了解您可以做什麼。
假設我們不希望在沒有來源檔案的情況下執行 processTemplates
任務,無論是否為乾淨建置 (clean build)。畢竟,如果沒有來源檔案,任務就無事可做。建構器允許我們像這樣配置:
tasks.register("processTemplatesAdHocSkipWhenEmpty") {
// ...
inputs.files(fileTree("src/templates") {
include("**/*.fm")
})
.skipWhenEmpty()
.withPropertyName("sourceFiles")
.withPathSensitivity(PathSensitivity.RELATIVE)
.ignoreEmptyDirectories()
// ...
}
tasks.register('processTemplatesAdHocSkipWhenEmpty') {
// ...
inputs.files(fileTree('src/templates') {
include '**/*.fm'
})
.skipWhenEmpty()
.withPropertyName('sourceFiles')
.withPathSensitivity(PathSensitivity.RELATIVE)
.ignoreEmptyDirectories()
// ...
}
gradle clean processTemplatesAdHocSkipWhenEmpty
的輸出> gradle clean processTemplatesAdHocSkipWhenEmpty > Task :processTemplatesAdHocSkipWhenEmpty NO-SOURCE BUILD SUCCESSFUL in 0s 3 actionable tasks: 2 executed, 1 up-to-date
TaskInputs.files()
方法傳回一個具有 skipWhenEmpty()
方法的建構器。調用此方法等同於使用 @SkipWhenEmpty
註解屬性。
既然您已經看過註解和執行時期 API,您可能想知道應該使用哪個 API。我們的建議是盡可能使用註解,有時甚至值得建立自訂任務類別,以便您可以使用它們。執行時期 API 更適用於您無法使用註解的情況。
為自訂任務類型宣告輸入和輸出
另一種範例類型涉及為自訂任務類別的實例註冊額外的輸入和輸出。例如,假設 ProcessTemplates
任務也需要讀取 src/headers/headers.txt
(例如,因為它包含在其中一個來源中)。您會希望 Gradle 知道這個輸入檔案,以便在該檔案的內容變更時重新執行任務。使用執行時期 API,您可以做到這一點:
tasks.register<ProcessTemplates>("processTemplatesWithExtraInputs") {
// ...
inputs.file("src/headers/headers.txt")
.withPropertyName("headers")
.withPathSensitivity(PathSensitivity.NONE)
}
tasks.register('processTemplatesWithExtraInputs', ProcessTemplates) {
// ...
inputs.file('src/headers/headers.txt')
.withPropertyName('headers')
.withPathSensitivity(PathSensitivity.NONE)
}
像這樣使用執行時期 API 有點像使用 doLast()
和 doFirst()
將額外的動作附加到任務,不同之處在於,在這種情況下,我們附加的是關於輸入和輸出的資訊。
如果任務類型已經在使用增量建置 (incremental build) 註解,則使用相同的屬性名稱註冊輸入或輸出將會導致錯誤。 |
宣告任務輸入和輸出的好處
一旦您宣告了任務的正式輸入和輸出,Gradle 就可以推斷出關於這些屬性的資訊。例如,如果一個任務的輸入設定為另一個任務的輸出,那就表示第一個任務依賴於第二個任務,對吧?Gradle 知道這一點並可以據此採取行動。
接下來我們將看看這個功能,以及 Gradle 了解關於輸入和輸出的資訊所帶來的其他一些功能。
推斷的任務依賴性
考慮一個封裝 processTemplates
任務輸出的封存 (archive) 任務。建置作者會看到封存任務顯然需要先執行 processTemplates
,因此可能會新增明確的 dependsOn
。但是,如果您像這樣定義封存任務:
tasks.register<Zip>("packageFiles") {
from(processTemplates.map { it.outputDir })
}
tasks.register('packageFiles', Zip) {
from processTemplates.map { it.outputDir }
}
gradle clean packageFiles
的輸出> gradle clean packageFiles > Task :processTemplates > Task :packageFiles BUILD SUCCESSFUL in 0s 5 actionable tasks: 4 executed, 1 up-to-date
Gradle 將自動使 packageFiles
依賴於 processTemplates
。它可以做到這一點,因為它知道 packageFiles
的其中一個輸入需要 processTemplates
任務的輸出。我們稱之為推斷的任務依賴性。
上述範例也可以寫成:
tasks.register<Zip>("packageFiles2") {
from(processTemplates)
}
tasks.register('packageFiles2', Zip) {
from processTemplates
}
gradle clean packageFiles2
的輸出> gradle clean packageFiles2 > Task :processTemplates > Task :packageFiles2 BUILD SUCCESSFUL in 0s 5 actionable tasks: 4 executed, 1 up-to-date
這是因為 from()
方法可以接受任務物件作為引數。在幕後,from()
使用 project.files()
方法來包裝引數,而這反過來將任務的正式輸出公開為檔案集合 (file collection)。換句話說,這是一個特例!
輸入和輸出驗證
增量建置註解提供了足夠的資訊,讓 Gradle 可以對註解的屬性執行一些基本驗證。特別是,在任務執行之前,它會針對每個屬性執行以下操作:
-
@InputFile
- 驗證屬性具有值,且路徑對應於現有的檔案 (而非目錄)。 -
@InputDirectory
- 與@InputFile
相同,但路徑必須對應於目錄。 -
@OutputDirectory
- 驗證路徑不符合檔案,並在目錄尚不存在時建立目錄。
如果一個任務在某個位置產生輸出,而另一個任務透過將該位置稱為輸入來使用它,則 Gradle 會檢查消費者任務是否依賴於生產者任務。當生產者和消費者任務同時執行時,建置會失敗,以避免捕獲不正確的狀態。
這種驗證提高了建置的穩健性,讓您可以快速識別與輸入和輸出相關的問題。
您偶爾會想要停用其中一些驗證,特別是當輸入檔案可能合法地不存在時。這就是 Gradle 提供 @Optional
註解的原因:您可以使用它來告知 Gradle 某個特定的輸入是選用的,因此如果對應的檔案或目錄不存在,建置不應失敗。
持續建置
定義任務輸入和輸出的另一個好處是持續建置。由於 Gradle 知道任務依賴哪些檔案,因此如果任務的任何輸入變更,它可以自動重新執行任務。當您執行 Gradle 時,透過 --continuous
或 -t
選項啟動持續建置,您會將 Gradle 置於一種狀態,在該狀態下,它會持續檢查變更,並在遇到此類變更時執行請求的任務。
您可以在 持續建置 中找到關於此功能的更多資訊。
任務平行處理
定義任務輸入和輸出的最後一個好處是,當使用 "--parallel" 選項時,Gradle 可以使用此資訊來決定如何執行任務。例如,Gradle 會在選擇要執行的下一個任務時檢查任務的輸出,並避免同時執行寫入到相同輸出目錄的任務。同樣地,Gradle 將使用關於任務銷毀哪些檔案的資訊 (例如,由 Destroys
註解指定),並避免在另一個任務正在執行以使用或建立相同檔案時 (反之亦然) 執行移除一組檔案的任務。它還可以確定建立一組檔案的任務已經執行,而使用這些檔案的任務尚未執行,並且將避免在兩者之間執行移除這些檔案的任務。透過以這種方式提供任務輸入和輸出資訊,Gradle 可以推斷任務之間的建立/使用/銷毀關係,並可以確保任務執行不會違反這些關係。
它是如何運作的?
在首次執行任務之前,Gradle 會取得輸入的指紋 (fingerprint)。此指紋包含輸入檔案的路徑和每個檔案內容的雜湊值 (hash)。然後 Gradle 執行任務。如果任務成功完成,Gradle 會取得輸出的指紋。此指紋包含輸出檔案的集合和每個檔案內容的雜湊值。Gradle 會持久保存這兩個指紋,以供下次執行任務時使用。
在那之後的每次,在執行任務之前,Gradle 都會取得輸入和輸出的新指紋。如果新指紋與先前的指紋相同,Gradle 會假設輸出是最新的並跳過任務。如果它們不相同,Gradle 會執行任務。Gradle 會持久保存這兩個指紋,以供下次執行任務時使用。
如果檔案的統計資訊 (stats) (即 lastModified
和 size
) 沒有變更,Gradle 將重複使用先前執行中的檔案指紋。這表示當檔案的統計資訊沒有變更時,Gradle 不會偵測到變更。
Gradle 還將任務的 *程式碼* 視為任務輸入的一部分。當任務、其動作或其依賴性在執行之間發生變更時,Gradle 會將任務視為過時 (out-of-date)。
Gradle 了解檔案屬性 (例如,持有 Java classpath 的屬性) 是否對順序敏感。在比較此類屬性的指紋時,即使檔案順序的變更也會導致任務過時。
請注意,如果任務指定了輸出目錄,則自上次執行以來新增到該目錄的任何檔案都會被忽略,並且 *不會* 導致任務過時。這是為了讓不相關的任務可以共享一個輸出目錄而不會互相干擾。如果由於某些原因這不是您想要的行為,請考慮使用 TaskOutputs.upToDateWhen(groovy.lang.Closure)
另請注意,變更不可用檔案的可用性 (例如,將損壞的符號連結 (symlink) 的目標修改為有效的檔案,反之亦然),將會被偵測到並由最新檢查 (up-to-date check) 處理。
為了追蹤任務、任務動作和巢狀輸入的實作 (implementation),Gradle 使用類別名稱和包含實作的 classpath 的識別符。在某些情況下,Gradle 無法精確地追蹤實作:
- 未知的類別載入器 (classloader)
-
當載入實作的類別載入器不是由 Gradle 建立時,無法確定 classpath。
- Java lambda
-
Java lambda 類別在執行時期使用非確定性的類別名稱建立。因此,類別名稱無法識別 lambda 的實作,並且在不同的 Gradle 執行之間會發生變更。
當任務、任務動作或巢狀輸入的實作無法精確追蹤時,Gradle 會停用任務的任何快取。這表示任務永遠不會是最新的,也不會從 建置快取 載入。
進階技術
到目前為止,您在本節中看到的所有內容將涵蓋您會遇到的大多數使用案例,但有些情境需要特殊處理。接下來我們將介紹其中一些情境以及適當的解決方案。
新增您自己的快取輸入/輸出方法
您是否曾經想過 Copy
任務的 from()
方法是如何運作的?它沒有使用 @InputFiles
註解,但傳遞給它的任何檔案都被視為任務的正式輸入。這是怎麼回事?
實作非常簡單,您可以將相同的技術用於您自己的任務,以改進其 API。編寫您的方法,使其直接將檔案新增到適當的註解屬性。例如,以下是如何將 sources()
方法新增到我們稍早介紹的自訂 ProcessTemplates
類別:
tasks.register<ProcessTemplates>("processTemplates") {
templateEngine = TemplateEngineType.FREEMARKER
templateData.name = "test"
templateData.variables = mapOf("year" to "2012")
outputDir = layout.buildDirectory.dir("genOutput")
sources(fileTree("src/templates"))
}
tasks.register('processTemplates', ProcessTemplates) {
templateEngine = TemplateEngineType.FREEMARKER
templateData.name = 'test'
templateData.variables = [year: '2012']
outputDir = file(layout.buildDirectory.dir('genOutput'))
sources fileTree('src/templates')
}
public abstract class ProcessTemplates extends DefaultTask {
// ...
@SkipWhenEmpty
@InputFiles
@PathSensitive(PathSensitivity.NONE)
public abstract ConfigurableFileCollection getSourceFiles();
public void sources(FileCollection sourceFiles) {
getSourceFiles().from(sourceFiles);
}
// ...
}
gradle processTemplates
的輸出> gradle processTemplates > Task :processTemplates BUILD SUCCESSFUL in 0s 3 actionable tasks: 3 executed
換句話說,只要您在配置階段將值和檔案新增到正式的任務輸入和輸出,它們就會被視為正式的輸入和輸出,無論您從建置中的哪個位置新增它們。
如果我們也想支援將任務作為引數,並將其輸出視為輸入,我們可以像這樣直接使用 TaskProvider
:
val copyTemplates by tasks.registering(Copy::class) {
into(file(layout.buildDirectory.dir("tmp")))
from("src/templates")
}
tasks.register<ProcessTemplates>("processTemplates2") {
// ...
sources(copyTemplates)
}
def copyTemplates = tasks.register('copyTemplates', Copy) {
into file(layout.buildDirectory.dir('tmp'))
from 'src/templates'
}
tasks.register('processTemplates2', ProcessTemplates) {
// ...
sources copyTemplates
}
// ...
public void sources(TaskProvider<?> inputTask) {
getSourceFiles().from(inputTask);
}
// ...
gradle processTemplates2
的輸出> gradle processTemplates2 > Task :copyTemplates > Task :processTemplates2 BUILD SUCCESSFUL in 0s 4 actionable tasks: 4 executed
此技術可以讓您的自訂任務更易於使用,並產生更簡潔的建置檔案。作為額外的好處,我們使用 TaskProvider
意味著我們的自訂方法可以設定推斷的任務依賴性。
最後要注意的一點:如果您正在開發一個像此範例一樣將來源檔案集合作為輸入的任務,請考慮使用內建的 SourceTask。它可以讓您免於實作我們放入 ProcessTemplates
中的一些基礎架構 (plumbing)。
將 @OutputDirectory
連結到 @InputFiles
當您想要將一個任務的輸出連結到另一個任務的輸入時,類型通常會匹配,而簡單的屬性賦值 (property assignment) 即可提供該連結。例如,File
輸出屬性可以賦值給 File
輸入。
不幸的是,當您希望任務的 @OutputDirectory
(類型為 `File`) 中的檔案成為另一個任務的 @InputFiles
屬性 (類型為 `FileCollection`) 的來源時,這種方法會失效。由於兩者具有不同的類型,屬性賦值將無法運作。
例如,假設您想要使用 Java 編譯任務的輸出 — 透過 `destinationDir` 屬性 — 作為自訂任務的輸入,該自訂任務會檢測一組包含 Java 位元組碼 (bytecode) 的檔案。這個自訂任務,我們將其稱為 `Instrument`,具有一個使用 @InputFiles
註解的 `classFiles` 屬性。您最初可能會嘗試像這樣配置任務:
plugins {
id("java-library")
}
tasks.register<Instrument>("badInstrumentClasses") {
classFiles.from(fileTree(tasks.compileJava.flatMap { it.destinationDirectory }))
destinationDir = layout.buildDirectory.dir("instrumented")
}
plugins {
id 'java-library'
}
tasks.register('badInstrumentClasses', Instrument) {
classFiles.from fileTree(tasks.named('compileJava').flatMap { it.destinationDirectory }) {}
destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean badInstrumentClasses
的輸出> gradle clean badInstrumentClasses > Task :clean UP-TO-DATE > Task :badInstrumentClasses NO-SOURCE BUILD SUCCESSFUL in 0s 3 actionable tasks: 2 executed, 1 up-to-date
這段程式碼顯然沒有任何問題,但您可以從主控台輸出看到編譯任務遺失了。在這種情況下,您需要透過 `dependsOn` 在 `instrumentClasses` 和 compileJava
之間新增明確的任務依賴性。使用 fileTree()
意味著 Gradle 無法自行推斷任務依賴性。
一個解決方案是使用 TaskOutputs.files
屬性,如下列範例所示:
tasks.register<Instrument>("instrumentClasses") {
classFiles.from(tasks.compileJava.map { it.outputs.files })
destinationDir = layout.buildDirectory.dir("instrumented")
}
tasks.register('instrumentClasses', Instrument) {
classFiles.from tasks.named('compileJava').map { it.outputs.files }
destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClasses
的輸出> gradle clean instrumentClasses > Task :clean UP-TO-DATE > Task :compileJava > Task :instrumentClasses BUILD SUCCESSFUL in 0s 5 actionable tasks: 4 executed, 1 up-to-date
或者,您可以讓 Gradle 存取適當的屬性本身,方法是使用 project.files()
、project.layout.files()
或 project.objects.fileCollection()
之一來取代 project.fileTree()
。
layout.files()
設定推斷的任務依賴性tasks.register<Instrument>("instrumentClasses2") {
classFiles.from(layout.files(tasks.compileJava))
destinationDir = layout.buildDirectory.dir("instrumented")
}
tasks.register('instrumentClasses2', Instrument) {
classFiles.from layout.files(tasks.named('compileJava'))
destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClasses2
的輸出> gradle clean instrumentClasses2 > Task :clean UP-TO-DATE > Task :compileJava > Task :instrumentClasses2 BUILD SUCCESSFUL in 0s 5 actionable tasks: 4 executed, 1 up-to-date
請記住,files()
、layout.files()
和 objects.fileCollection()
可以將任務作為引數,而 fileTree()
則不能。
這種方法的缺點是,來源任務的所有檔案輸出都變成了目標的輸入檔案 — 在這種情況下是 `instrumentClasses`。只要來源任務只有單一的基於檔案的輸出 (例如 `JavaCompile` 任務),這就沒有問題。但是,如果您必須在多個輸出屬性中僅連結一個,則需要使用 `builtBy` 方法明確告知 Gradle 哪個任務產生輸入檔案。
tasks.register<Instrument>("instrumentClassesBuiltBy") {
classFiles.from(fileTree(tasks.compileJava.flatMap { it.destinationDirectory }) {
builtBy(tasks.compileJava)
})
destinationDir = layout.buildDirectory.dir("instrumented")
}
tasks.register('instrumentClassesBuiltBy', Instrument) {
classFiles.from fileTree(tasks.named('compileJava').flatMap { it.destinationDirectory }) {
builtBy tasks.named('compileJava')
}
destinationDir = file(layout.buildDirectory.dir('instrumented'))
}
gradle clean instrumentClassesBuiltBy
的輸出> gradle clean instrumentClassesBuiltBy > Task :clean UP-TO-DATE > Task :compileJava > Task :instrumentClassesBuiltBy BUILD SUCCESSFUL in 0s 5 actionable tasks: 4 executed, 1 up-to-date
您當然可以直接透過 `dependsOn` 新增明確的任務依賴性,但上述方法提供了更多的語義含義,解釋了為什麼必須事先執行 `compileJava`。
停用最新檢查
Gradle 自動處理輸出檔案和目錄的最新檢查,但如果任務輸出完全是其他東西呢?也許是對 Web 服務或資料庫表格的更新。或者有時您有一個應該始終執行的任務。
這就是 `Task` 上的 `doNotTrackState()` 方法的用武之地。可以使用它來完全停用任務的最新檢查,如下所示:
tasks.register<Instrument>("alwaysInstrumentClasses") {
classFiles.from(layout.files(tasks.compileJava))
destinationDir = layout.buildDirectory.dir("instrumented")
doNotTrackState("Instrumentation needs to re-run every time")
}
tasks.register('alwaysInstrumentClasses', Instrument) {
classFiles.from layout.files(tasks.named('compileJava'))
destinationDir = file(layout.buildDirectory.dir('instrumented'))
doNotTrackState("Instrumentation needs to re-run every time")
}
gradle clean alwaysInstrumentClasses
的輸出> gradle clean alwaysInstrumentClasses > Task :compileJava > Task :alwaysInstrumentClasses BUILD SUCCESSFUL in 0s 4 actionable tasks: 1 executed, 3 up-to-date
gradle alwaysInstrumentClasses
的輸出> gradle alwaysInstrumentClasses > Task :compileJava UP-TO-DATE > Task :alwaysInstrumentClasses BUILD SUCCESSFUL in 0s 4 actionable tasks: 1 executed, 3 up-to-date
如果您正在編寫自己的應該始終執行的任務,那麼您也可以在任務類別上使用 @UntrackedTask
註解,而不是調用 `Task.doNotTrackState()`。
整合執行自身最新檢查的外部工具
有時您想要整合像 Git 或 Npm 這樣的外部工具,它們都執行自己的最新檢查。在這種情況下,Gradle 也執行最新檢查就沒有太大的意義。您可以使用包裝工具的任務上的 @UntrackedTask
註解來停用 Gradle 的最新檢查。或者,您可以使用執行時期 API 方法 Task.doNotTrackState()
。
例如,假設您想要實作一個克隆 Git 儲存庫 (repository) 的任務。
@UntrackedTask(because = "Git tracks the state") (1)
public abstract class GitClone extends DefaultTask {
@Input
public abstract Property<String> getRemoteUri();
@Input
public abstract Property<String> getCommitId();
@OutputDirectory
public abstract DirectoryProperty getDestinationDir();
@TaskAction
public void gitClone() throws IOException {
File destinationDir = getDestinationDir().get().getAsFile().getAbsoluteFile(); (2)
String remoteUri = getRemoteUri().get();
// Fetch origin or clone and checkout
// ...
}
}
tasks.register<GitClone>("cloneGradleProfiler") {
destinationDir = layout.buildDirectory.dir("gradle-profiler") // <3
remoteUri = "https://github.com/gradle/gradle-profiler.git"
commitId = "d6c18a21ca6c45fd8a9db321de4478948bdf801b"
}
tasks.register("cloneGradleProfiler", GitClone) {
destinationDir = layout.buildDirectory.dir("gradle-profiler") (3)
remoteUri = "https://github.com/gradle/gradle-profiler.git"
commitId = "d6c18a21ca6c45fd8a9db321de4478948bdf801b"
}
1 | 將任務宣告為 untracked。 |
2 | 使用輸出目錄來執行外部工具。 |
3 | 在您的建置中新增任務並配置輸出目錄。 |
配置輸入正規化
為了進行最新檢查和 建置快取,Gradle 需要確定兩個任務輸入屬性是否具有相同的值。為了做到這一點,Gradle 首先正規化兩個輸入,然後比較結果。例如,對於編譯 classpath,Gradle 從 classpath 上的類別中提取 ABI 簽名,然後比較上次 Gradle 執行和目前 Gradle 執行之間的簽名,如 Java 編譯避免 中所述。
正規化適用於 classpath 上的所有 zip 檔案 (例如 jars、wars、aars、apks 等)。這讓 Gradle 可以識別兩個 zip 檔案在功能上是否相同,即使 zip 檔案本身可能由於元數據 (metadata) (例如時間戳記或檔案順序) 而略有不同。正規化不僅適用於 classpath 上直接的 zip 檔案,也適用於巢狀在目錄內或 classpath 上其他 zip 檔案內的 zip 檔案。
可以自訂 Gradle 內建的執行時期 classpath 正規化策略。所有使用 @Classpath
註解的輸入都被視為執行時期 classpaths。
假設您想要將檔案 build-info.properties
新增到您產生的所有 jar 檔案中,其中包含關於建置的資訊,例如建置開始的時間戳記或用於識別發布成品 (artifact) 的 CI 作業的 ID。此檔案僅用於稽核目的,對執行測試的結果沒有影響。儘管如此,此檔案是 `test` 任務的執行時期 classpath 的一部分,並且在每次建置調用時都會變更。因此,`test` 將永遠不會是最新的,也不會從建置快取中提取。為了再次從增量建置中受益,您可以透過使用 Project.normalization(org.gradle.api.Action) (在 *使用* 專案中) 告知 Gradle 在專案層級忽略執行時期 classpath 上的此檔案。
normalization {
runtimeClasspath {
ignore("build-info.properties")
}
}
normalization {
runtimeClasspath {
ignore 'build-info.properties'
}
}
如果將此類檔案新增到您的 jar 檔案是您針對建置中所有專案執行的事情,並且您想要為所有消費者篩選此檔案,則應考慮在 慣例外掛程式 (convention plugin) 中配置此類正規化,以便在子專案之間共享。
此配置的效果是,對 build-info.properties
的變更將在最新檢查和 建置快取 鍵計算中被忽略。請注意,這不會變更 `test` 任務的執行時期行為 — 也就是說,任何測試仍然能夠載入 build-info.properties
,並且執行時期 classpath 仍然與之前相同。
屬性檔案正規化
預設情況下,屬性檔案 (即以 `.properties` 副檔名結尾的檔案) 將被正規化以忽略註解、空白字元和屬性順序的差異。Gradle 透過載入屬性檔案,並僅在最新檢查或建置快取鍵計算期間考慮個別屬性來做到這一點。
不過,有時某些屬性具有執行時期影響,而其他屬性則沒有。如果變更的屬性對執行時期 classpath 沒有影響,則可能希望將其從最新檢查和 建置快取 鍵計算中排除。但是,排除整個檔案也會排除確實具有執行時期影響的屬性。在這種情況下,可以從執行時期 classpath 上的任何或所有屬性檔案中選擇性地排除屬性。
可以使用 RuntimeClasspathNormalization 中描述的模式,將忽略屬性的規則應用於一組特定的檔案。如果檔案符合規則,但無法作為屬性檔案載入 (例如,因為其格式不正確或使用非標準編碼),則它將作為普通檔案併入最新或建置快取鍵計算中。換句話說,如果檔案無法作為屬性檔案載入,則對空白字元、屬性順序或註解的任何變更都可能導致任務過時或導致快取未命中 (cache miss)。
normalization {
runtimeClasspath {
properties("**/build-info.properties") {
ignoreProperty("timestamp")
}
}
}
normalization {
runtimeClasspath {
properties('**/build-info.properties') {
ignoreProperty 'timestamp'
}
}
}
normalization {
runtimeClasspath {
properties {
ignoreProperty("timestamp")
}
}
}
normalization {
runtimeClasspath {
properties {
ignoreProperty 'timestamp'
}
}
}
Java META-INF
正規化
對於 jar 封存 (archive) 的 META-INF
目錄中的檔案,由於它們的執行時期影響,並非總是能夠完全忽略檔案。
META-INF
中的 Manifest 檔案被正規化以忽略註解、空白字元和順序差異。Manifest 屬性名稱以不區分大小寫和順序的方式比較。Manifest 屬性檔案根據 屬性檔案正規化 進行正規化。
META-INF
manifest 屬性normalization {
runtimeClasspath {
metaInf {
ignoreAttribute("Implementation-Version")
}
}
}
normalization {
runtimeClasspath {
metaInf {
ignoreAttribute("Implementation-Version")
}
}
}
META-INF
屬性鍵normalization {
runtimeClasspath {
metaInf {
ignoreProperty("app.version")
}
}
}
normalization {
runtimeClasspath {
metaInf {
ignoreProperty("app.version")
}
}
}
META-INF/MANIFEST.MF
normalization {
runtimeClasspath {
metaInf {
ignoreManifest()
}
}
}
normalization {
runtimeClasspath {
metaInf {
ignoreManifest()
}
}
}
META-INF
內的所有檔案和目錄normalization {
runtimeClasspath {
metaInf {
ignoreCompletely()
}
}
}
normalization {
runtimeClasspath {
metaInf {
ignoreCompletely()
}
}
}
提供自訂的最新邏輯
Gradle 自動處理輸出檔案和目錄的最新檢查,但如果任務輸出完全是其他東西呢?也許是對 Web 服務或資料庫表格的更新。在這種情況下,Gradle 無法知道如何檢查任務是否為最新。
這就是 `TaskOutputs` 上的 `upToDateWhen()` 方法的用武之地。這需要一個述詞 (predicate) 函數,用於確定任務是否為最新。例如,您可以從資料庫讀取資料庫結構描述的版本號碼。或者,您可以檢查資料庫表格中的特定記錄是否存在或已變更。
請注意,最新檢查應該為您 *節省* 時間。不要新增成本與任務的標準執行時間一樣多或更多的檢查。實際上,如果任務最終還是經常執行,因為它很少是最新的,那麼可能不值得完全不進行最新檢查,如 停用最新檢查 中所述。請記住,如果任務在執行任務圖 (execution task graph) 中,您的檢查將始終執行。
一個常見的錯誤是使用 upToDateWhen()
而不是 `Task.onlyIf()`。如果您想要根據與任務輸入和輸出無關的某些條件跳過任務,那麼您應該使用 `onlyIf()`。例如,在您想要在設定或未設定特定屬性時跳過任務的情況下。
過時的任務輸出
當 Gradle 版本變更時,Gradle 會偵測到需要移除使用較舊版本 Gradle 執行的任務的輸出,以確保最新版本的任務從已知的乾淨狀態開始。
過時輸出目錄的自動清理僅針對來源集 (source set) (Java/Groovy/Scala 編譯) 的輸出實作。 |