簡介

組態快取是一項功能,透過快取組態階段的結果,並在後續建置中重複使用,從而顯著提升建置效能。使用組態快取,當沒有任何影響建置組態的因素(例如建置腳本)發生變更時,Gradle 可以完全略過組態階段。Gradle 也將效能提升應用於任務執行。

組態快取在概念上與建置快取相似,但快取不同的資訊。建置快取負責快取建置的輸出和中間檔案,例如任務輸出或成品轉換輸出。組態快取負責快取特定任務集的建置組態。換句話說,組態快取儲存組態階段的輸出,而建置快取儲存執行階段的輸出。

此功能目前預設未啟用。此功能有以下限制

  • 組態快取不支援所有核心 Gradle 外掛功能。完整支援正在進行中。

  • 您的建置和您依賴的外掛可能需要變更才能滿足需求

  • IDE 匯入和同步尚未使用組態快取。

它是如何運作的?

當組態快取啟用且您為特定任務集執行 Gradle 時,例如透過執行 gradlew check,Gradle 會檢查是否有可用的組態快取條目用於請求的任務集。如果可用,Gradle 會使用此條目,而不是執行組態階段。快取條目包含有關要執行的任務集的資訊,以及它們的組態和依賴關係資訊。

當您第一次執行特定任務集時,這些任務的組態快取中將沒有條目,因此 Gradle 將照常執行組態階段

  1. 執行初始化腳本。

  2. 執行建置的設定腳本,應用任何請求的設定外掛。

  3. 組態和建置 buildSrc 專案(如果存在)。

  4. 執行建置的建置腳本,應用任何請求的專案外掛。

  5. 計算請求任務的任務圖,執行任何延遲的組態動作。

在組態階段之後,Gradle 會將任務圖的快照寫入新的組態快取條目,以供稍後的 Gradle 調用使用。然後 Gradle 從組態快取載入任務圖,以便它可以將最佳化應用於任務,然後照常執行執行階段。組態時間仍然會在您第一次執行特定任務集時花費。但是,您應該立即看到建置效能的提升,因為任務將平行執行

當您後續使用相同的任務集執行 Gradle 時,例如再次執行 gradlew check,Gradle 將直接從組態快取載入任務及其組態,並完全略過組態階段。在使用組態快取條目之前,Gradle 會檢查條目的任何「建置組態輸入」(例如建置腳本)是否已變更。如果建置組態輸入已變更,Gradle 將不會使用該條目,並將再次執行組態階段,如上所述,並儲存結果以供稍後重複使用。

建置組態輸入包括

  • 初始化腳本

  • 設定腳本

  • 建置腳本

  • 在組態階段期間使用的系統屬性

  • 在組態階段期間使用的 Gradle 屬性

  • 在組態階段期間使用的環境變數

  • 使用值供應器(例如提供者)存取的組態檔

  • buildSrc 和包含外掛的建置輸入,包括建置組態輸入和原始碼檔案。

Gradle 使用其自身最佳化的序列化機制和格式來儲存組態快取條目。它會自動序列化任意物件圖的狀態。如果您的任務持有對具有簡單狀態或支援類型的物件的參考,您無需執行任何操作即可支援序列化。

作為後備方案,並為了協助遷移現有任務,支援Java 序列化的某些語義。但不建議依賴它,主要是為了效能考量。

效能提升

除了略過組態階段外,組態快取還提供了一些額外的效能提升

  • 所有任務預設平行執行,受限於依賴關係約束。

  • 依賴關係解析已快取。

  • 組態狀態和依賴關係解析狀態在寫入任務圖後從堆積中丟棄。這降低了給定任務集所需的峰值堆積使用量。

使用組態快取

建議從最簡單的任務調用開始。啟用組態快取執行 help 是很好的第一步

❯ gradle --configuration-cache help
Calculating task graph as no cached configuration is available for tasks: help
...
BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed
Configuration cache entry stored.

第一次執行此操作時,組態階段會執行,計算任務圖。

然後,再次執行相同的命令。這會重複使用快取的組態

❯ gradle --configuration-cache help
Reusing configuration cache.
...
BUILD SUCCESSFUL in 500ms
1 actionable task: 1 executed
Configuration cache entry reused.

如果它在您的建置中成功,恭喜您,您現在可以嘗試更實用的任務。您應該以您的開發迴圈為目標。一個很好的例子是在進行增量變更後執行測試。

如果在快取或重複使用組態時發現任何問題,將會產生 HTML 報告以協助您診斷和修正問題。該報告還顯示在組態階段偵測到的建置組態輸入,例如系統屬性、環境變數和值供應器。請參閱下面的疑難排解章節以取得更多資訊。

繼續閱讀以了解如何調整組態快取、在出現問題時手動使狀態失效,以及從 IDE 使用組態快取。

啟用組態快取

預設情況下,Gradle 不使用組態快取。若要在建置時啟用快取,請使用 configuration-cache 旗標

❯ gradle --configuration-cache

您也可以在 gradle.properties 檔案中使用 org.gradle.configuration-cache 屬性以持久方式啟用快取

org.gradle.configuration-cache=true

如果在 gradle.properties 檔案中啟用,您可以覆寫該設定,並在建置時使用 no-configuration-cache 旗標停用快取

❯ gradle --no-configuration-cache

忽略問題

預設情況下,如果遇到任何組態快取問題,Gradle 將會使建置失敗。當逐步改善您的外掛或建置邏輯以支援組態快取時,暫時將問題轉為警告可能會很有用,但無法保證建置會運作。

這可以從命令列完成

❯ gradle --configuration-cache-problems=warn

或在 gradle.properties 檔案中完成

org.gradle.configuration-cache.problems=warn

允許的最大問題數量

當組態快取問題轉為警告時,如果預設情況下發現 512 個問題,Gradle 將會使建置失敗。

這可以透過在命令列上指定允許的最大問題數量來調整

❯ gradle -Dorg.gradle.configuration-cache.max-problems=5

或在 gradle.properties 檔案中完成

org.gradle.configuration-cache.max-problems=5

啟用平行組態快取

組態快取的儲存和載入預設循序完成。平行儲存和載入提供更好的效能,但並非所有建置都與之相容。

若要在命令列上啟用平行組態快取

❯ gradle -Dorg.gradle.configuration-cache.parallel=true

或在 gradle.properties 檔案中完成

org.gradle.configuration-cache.parallel=true

平行組態快取功能是孵化中,因為無法保證所有建置都能正確運作。常見的症狀是在組態階段期間出現 ConcurrentModificationException 例外狀況。此功能應適用於解耦的多專案建置。

使快取失效

當組態階段的輸入變更時,組態快取會自動失效。但是,某些輸入尚未追蹤,因此當組態階段的未追蹤輸入變更時,您可能需要手動使組態快取失效。如果您忽略了問題,可能會發生這種情況。請參閱下面的需求尚未實作章節以取得更多資訊。

組態快取狀態儲存在磁碟上的目錄中,該目錄名為 .gradle/configuration-cache,位於使用的 Gradle 建置的根目錄中。如果您需要使快取失效,只需刪除該目錄即可

❯ rm -rf .gradle/configuration-cache

組態快取條目會定期檢查(最多每 24 小時一次)是否仍在使用中。如果它們已 7 天未使用,則會被刪除。

穩定的組態快取

為了實現組態快取的穩定性,當某些嚴格性對於早期採用者來說過於破壞性時,我們在功能旗標後實作了這些嚴格性。

您可以按如下方式啟用該功能旗標

settings.gradle.kts
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
settings.gradle
enableFeaturePreview "STABLE_CONFIGURATION_CACHE"

STABLE_CONFIGURATION_CACHE 功能旗標啟用以下功能

未宣告的共享建置服務使用

啟用後,使用共享建置服務但未透過 Task.usesService 方法宣告需求的任務將會發出棄用警告。

此外,當未啟用組態快取但功能旗標存在時,以下組態快取需求的棄用也會啟用

建議盡快啟用它,以便為我們移除旗標並使連結的功能成為預設功能做好準備。

IDE 支援

如果您從 gradle.properties 檔案啟用和組態組態快取,則當您的 IDE 委派給 Gradle 時,組態快取將會啟用。無需執行任何其他操作。

gradle.properties 通常會簽入原始碼控制。如果您還不想為您的整個團隊啟用組態快取,您也可以僅從您的 IDE 啟用組態快取,如下所述。

請注意,從 IDE 同步建置不會從組態快取中受益,只有執行任務才會受益。

基於 IntelliJ 的 IDE

在 IntelliJ IDEA 或 Android Studio 中,可以透過兩種方式完成此操作,即全域方式或每個執行組態方式。

若要為整個建置啟用它,請前往 Run > Edit configurations…​。這將開啟 IntelliJ IDEA 或 Android Studio 對話方塊以組態執行/偵錯組態。選取 Templates > Gradle,並將必要的系統屬性新增至 VM options 欄位。

例如,若要啟用組態快取,將問題轉為警告,請新增以下內容

-Dorg.gradle.configuration-cache=true -Dorg.gradle.configuration-cache.problems=warn

您也可以選擇僅為給定的執行組態啟用它。在這種情況下,請保持 Templates > Gradle 組態不變,並根據您的需要編輯每個執行組態。

結合這兩種方式,您可以全域啟用並為某些執行組態停用,反之亦然。

您可以使用 gradle-idea-ext-plugin 從您的建置組態 IntelliJ 執行組態。這是僅為 IDE 啟用組態快取的好方法。

Eclipse IDE

在 Eclipse IDE 中,您可以透過 Buildship 以兩種方式啟用和組態組態快取,即全域方式或每個執行組態方式。

若要全域啟用它,請前往 Preferences > Gradle。您可以使用上述屬性作為系統屬性。例如,若要啟用組態快取,將問題轉為警告,請新增以下 JVM 引數

  • -Dorg.gradle.configuration-cache=true

  • -Dorg.gradle.configuration-cache.problems=warn

若要為給定的執行組態啟用它,請前往 Run configurations…​,找到您要變更的組態,前往 Project Settings,勾選 Override project settings 核取方塊,並新增與 JVM argument 相同的系統屬性。

結合這兩種方式,您可以全域啟用並為某些執行組態停用,反之亦然。

支援的外掛

組態快取是全新的,並為外掛實作引入了新的需求。因此,核心 Gradle 外掛和社群外掛都需要調整。本節提供有關核心 Gradle 外掛社群外掛中目前支援的資訊。

社群外掛

請參閱問題 gradle/gradle#13490 以了解社群外掛的狀態。

疑難排解

以下章節將介紹有關處理組態快取問題的一些一般指南。這適用於您的建置邏輯和您的 Gradle 外掛。

在無法序列化執行任務所需的狀態時,會產生偵測到問題的 HTML 報告。Gradle 失敗輸出包含指向該報告的可點擊連結。此報告非常有用,可讓您深入了解問題,了解問題發生的原因。

讓我們看一下一個簡單的建置腳本範例,其中包含幾個問題

build.gradle.kts
tasks.register("someTask") {
    val destination = System.getProperty("someDestination") (1)
    inputs.dir("source")
    outputs.dir(destination)
    doLast {
        project.copy { (2)
            from("source")
            into(destination)
        }
    }
}
build.gradle
tasks.register('someTask') {
    def destination = System.getProperty('someDestination') (1)
    inputs.dir('source')
    outputs.dir(destination)
    doLast {
        project.copy { (2)
            from 'source'
            into destination
        }
    }
}

執行該任務失敗,並在主控台中印出以下內容

❯ gradle --configuration-cache someTask -DsomeDestination=dest
...
* What went wrong:
Configuration cache problems found in this build.

1 problem was found storing the configuration cache.
- Build file 'build.gradle': line 6: invocation of 'Task.project' at execution time is unsupported.
  See https://gradle-docs.dev.org.tw/0.0.0/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution

See the complete report at file:///home/user/gradle/samples/build/reports/configuration-cache/<hash>/configuration-cache-report.html
> Invocation of 'Task.project' by task ':someTask' at execution time is unsupported.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 0s
1 actionable task: 1 executed
Configuration cache entry discarded with 1 problem.

由於發現問題導致建置失敗,因此已捨棄組態快取條目。

詳細資訊可在連結的 HTML 報告中找到

problems report

報告會顯示兩次問題集。首先按問題訊息分組,然後按任務分組。前者可讓您快速查看您的建置正面臨哪些類型的問題。後者可讓您快速查看哪些任務有問題。在這兩種情況下,您都可以展開樹狀結構,以便在物件圖中找到罪魁禍首。

該報告還包括偵測到的建置組態輸入列表,例如在組態階段讀取的環境變數、系統屬性和值供應器

inputs report

報告中顯示的問題具有指向相應需求的連結,您可以在其中找到有關如何修正問題的指南,或指向相應的尚未實作的功能。

在變更您的建置或外掛以修正問題時,您應該考慮使用 TestKit 測試您的建置邏輯

在此階段,您可以決定將問題轉為警告,並繼續探索您的建置如何回應組態快取,或修正手頭的問題。

讓我們忽略回報的問題,並再次執行相同的建置兩次,以查看重複使用快取的有問題組態時會發生什麼情況

❯ gradle --configuration-cache --configuration-cache-problems=warn someTask -DsomeDestination=dest
Calculating task graph as no cached configuration is available for tasks: someTask
> Task :someTask

1 problem was found storing the configuration cache.
- Build file 'build.gradle': line 6: invocation of 'Task.project' at execution time is unsupported.
  See https://gradle-docs.dev.org.tw/0.0.0/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution

See the complete report at file:///home/user/gradle/samples/build/reports/configuration-cache/<hash>/configuration-cache-report.html

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration cache entry stored with 1 problem.
❯ gradle --configuration-cache --configuration-cache-problems=warn someTask -DsomeDestination=dest
Reusing configuration cache.
> Task :someTask

1 problem was found reusing the configuration cache.
- Build file 'build.gradle': line 6: invocation of 'Task.project' at execution time is unsupported.
  See https://gradle-docs.dev.org.tw/0.0.0/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution

See the complete report at file:///home/user/gradle/samples/build/reports/configuration-cache/<hash>/configuration-cache-report.html

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration cache entry reused with 1 problem.

這兩個建置都成功回報了觀察到的問題,儲存然後重複使用了組態快取。

在主控台問題摘要和 HTML 報告中提供的連結的幫助下,我們可以修正我們的問題。以下是修正後的建置腳本版本

build.gradle.kts
abstract class MyCopyTask : DefaultTask() { (1)

    @get:InputDirectory abstract val source: DirectoryProperty (2)

    @get:OutputDirectory abstract val destination: DirectoryProperty (2)

    @get:Inject abstract val fs: FileSystemOperations (3)

    @TaskAction
    fun action() {
        fs.copy { (3)
            from(source)
            into(destination)
        }
    }
}

tasks.register<MyCopyTask>("someTask") {
    val projectDir = layout.projectDirectory
    source = projectDir.dir("source")
    destination = projectDir.dir(System.getProperty("someDestination"))
}
build.gradle
abstract class MyCopyTask extends DefaultTask { (1)

    @InputDirectory abstract DirectoryProperty getSource() (2)

    @OutputDirectory abstract DirectoryProperty getDestination() (2)

    @Inject abstract FileSystemOperations getFs() (3)

    @TaskAction
    void action() {
        fs.copy { (3)
            from source
            into destination
        }
    }
}

tasks.register('someTask', MyCopyTask) {
    def projectDir = layout.projectDirectory
    source = projectDir.dir('source')
    destination = projectDir.dir(System.getProperty('someDestination'))
}
1 我們將我們的臨時任務轉變為適當的任務類別,
2 具有輸入和輸出宣告,
3 並注入了 FileSystemOperations 服務,這是 project.copy {} 的受支援替代方案。

現在執行該任務兩次,成功且沒有回報任何問題,並在第二次執行時重複使用組態快取

❯ gradle --configuration-cache someTask -DsomeDestination=dest
Calculating task graph as no cached configuration is available for tasks: someTask
> Task :someTask

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration cache entry stored.
❯ gradle --configuration-cache someTask -DsomeDestination=dest
Reusing configuration cache.
> Task :someTask

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration cache entry reused.

但是,如果我們變更系統屬性的值會怎樣?

❯ gradle --configuration-cache someTask -DsomeDestination=another
Calculating task graph as configuration cache cannot be reused because system property 'someDestination' has changed.
> Task :someTask

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration cache entry stored.

先前的組態快取條目無法重複使用,並且必須重新計算和儲存任務圖。這是因為我們在組態時讀取了系統屬性,因此當該屬性的值變更時,需要 Gradle 再次執行組態階段。修正此問題非常簡單,只需取得系統屬性的提供者並將其連接到任務輸入,而無需在組態時讀取它。

build.gradle.kts
tasks.register<MyCopyTask>("someTask") {
    val projectDir = layout.projectDirectory
    source = projectDir.dir("source")
    destination = projectDir.dir(providers.systemProperty("someDestination")) (1)
}
build.gradle
tasks.register('someTask', MyCopyTask) {
    def projectDir = layout.projectDirectory
    source = projectDir.dir('source')
    destination = projectDir.dir(providers.systemProperty('someDestination')) (1)
}
1 我們直接連接了系統屬性提供者,而無需在組態時讀取它。

透過這個簡單的變更,我們可以執行任務任意次數、變更系統屬性值,並重複使用組態快取

❯ gradle --configuration-cache someTask -DsomeDestination=dest
Calculating task graph as no cached configuration is available for tasks: someTask
> Task :someTask

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration cache entry stored.
❯ gradle --configuration-cache someTask -DsomeDestination=another
Reusing configuration cache.
> Task :someTask

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration cache entry reused.

我們現在已完成修正此簡單任務的問題。

繼續閱讀以了解如何為您的建置或外掛採用組態快取。

宣告任務與組態快取不相容

可以透過 Task.notCompatibleWithConfigurationCache() 方法宣告特定任務與組態快取不相容。

在標記為不相容的任務中發現的組態快取問題將不再導致建置失敗。

並且,當排程執行不相容的任務時,Gradle 會在建置結束時捨棄組態狀態。您可以使用此功能來協助遷移,方法是暫時選擇退出某些難以變更以使用組態快取的任務。

請查看方法文件以取得更多詳細資訊。

採用步驟

一個重要的先決條件是讓您的 Gradle 和外掛版本保持最新。以下探討了成功採用的建議步驟。它適用於建置和外掛。在完成這些步驟時,請記住 HTML 報告和下面需求章節中說明的解決方案。

:help 開始

始終嘗試使用最簡單的任務 :help 來嘗試您的建置或外掛。這將練習您的建置或外掛的最簡組態階段。

逐步以實用任務為目標

不要立即執行 build。您也可以先使用 --dry-run 來發現更多組態時問題。

在處理建置時,逐步以您的開發回饋迴圈為目標。例如,在對原始碼進行一些變更後執行測試。

在處理外掛時,逐步以貢獻或組態的任務為目標。

透過將問題轉為警告來探索

不要在第一次建置失敗時停止;將問題轉為警告,以發現您的建置和外掛的行為方式。如果建置失敗,請使用 HTML 報告來推理與失敗相關的回報問題。繼續執行更多實用任務。

這將讓您全面了解您的建置和外掛正面臨的問題的本質。請記住,當將問題轉為警告時,如果出現問題,您可能需要手動使快取失效

退後一步並反覆修正問題

當您覺得您對需要修正的內容有足夠的了解時,請退後一步並開始反覆修正最重要的問題。使用 HTML 報告和本文檔來協助您完成此旅程。

儲存組態快取時回報的問題開始。修正後,您可以依賴有效的快取組態階段,並繼續修正載入組態快取時回報的問題(如果有的話)。

回報遇到的問題

如果您遇到Gradle 功能Gradle 核心外掛的問題,而本文檔未涵蓋這些問題,請在 gradle/gradle 上回報問題。

如果您遇到社群 Gradle 外掛的問題,請查看它是否已在 gradle/gradle#13490 中列出,並考慮將問題回報給外掛的問題追蹤器。

回報此類問題的一個好方法是提供諸如以下資訊

  • 指向本文檔的連結,

  • 您嘗試的外掛版本,

  • 外掛的自訂組態(如果有的話),或理想情況下是重現建置,

  • 失敗的描述,例如給定任務的問題

  • 建置失敗的副本,

  • 獨立的 configuration-cache-report.html 檔案。

測試、測試、再測試

考慮為您的建置邏輯新增測試。請參閱下面關於測試您的建置邏輯以取得組態快取的章節。這將在您反覆執行所需變更時為您提供協助,並防止未來發生回歸。

向您的團隊推出

一旦您的開發人員工作流程正常運作,例如從 IDE 執行測試,您可以考慮為您的團隊啟用它。在變更程式碼和執行測試時,更快的周轉時間可能是值得的。您可能希望先以選擇加入的方式執行此操作。

如果需要,請將問題轉為警告,並在您的建置 gradle.properties 檔案中設定允許的最大問題數量。預設情況下保持組態快取停用。讓您的團隊知道他們可以選擇加入,例如,為支援的工作流程在其 IDE 執行組態上啟用組態快取。

稍後,當更多工作流程正常運作時,您可以將其翻轉過來。預設情況下啟用組態快取,將 CI 組態為停用組態快取,如果需要,請傳達需要停用組態快取的不支援工作流程。

在建置中回應組態快取

建置邏輯或外掛實作可以偵測組態快取是否已針對給定的建置啟用,並據此做出回應。作用中組態快取的狀態在對應的建置功能中提供。您可以透過將BuildFeatures 服務注入到您的程式碼中來存取它。

您可以使用此資訊以不同方式組態您的外掛的功能,或停用尚未相容的可選功能。另一個範例涉及為您的使用者提供額外指導,如果他們需要調整其設定或被告知臨時限制。

採用組態快取行為的變更

Gradle 版本為組態快取帶來了增強功能,使其能夠偵測更多組態邏輯與環境互動的情況。這些變更透過消除潛在的錯誤快取命中,提高了快取的正確性。另一方面,它們也對外掛程式和建置邏輯施加了更嚴格的規則,以盡可能頻繁地進行快取。

如果某些組態輸入的結果不影響已組態的任務,則可以將它們視為「良性」。由於這些輸入而導致新的組態未命中,對於建置使用者來說可能是不希望發生的,而消除它們的建議策略是

  • 借助組態快取報告,找出導致組態快取失效的組態輸入。

    • 修正專案建置邏輯存取的未宣告組態輸入。

    • 向外掛程式維護者報告由第三方外掛程式引起的問題,並在外掛程式修復後更新外掛程式。

  • 對於某些組態輸入類型,可以使用選擇退出選項,使 Gradle 回退到先前的行為,從偵測中省略這些輸入。此臨時解決方案旨在減輕過時外掛程式帶來的效能問題。

在以下情況下,可以暫時選擇退出組態輸入偵測

  • 自 Gradle 8.1 起,使用許多與檔案系統相關的 API 會被正確追蹤為組態輸入,包括檔案系統檢查,例如 File.exists()File.isFile()

    為了使輸入追蹤忽略特定路徑上的這些檔案系統檢查,可以使用 Gradle 屬性 org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks,其中路徑列表相對於根專案目錄,並以 ; 分隔。若要忽略多個路徑,請使用 * 來比對一個區段內的任意字串,或使用 ** 跨區段。以 ~/ 開頭的路徑基於使用者主目錄。例如

    gradle.properties
    org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=\
        ~/.third-party-plugin/*.lock;\
        ../../externalOutputDirectory/**;\
        build/analytics.json
  • 在 Gradle 8.4 之前,當組態快取序列化任務圖時,仍然可以讀取某些從未在組態邏輯中使用的未宣告組態輸入。但是,它們的變更之後不會使組態快取失效。從 Gradle 8.4 開始,此類未宣告的組態輸入將被正確追蹤。

    若要暫時恢復到先前的行為,請將 Gradle 屬性 org.gradle.configuration-cache.inputs.unsafe.ignore.in-serialization 設定為 true

請謹慎地忽略組態輸入,並且僅在它們不影響組態邏輯產生的任務時才忽略。對這些選項的支援將在未來的版本中移除。

測試您的建置邏輯

Gradle TestKit(又名 TestKit)是一個程式庫,可協助測試 Gradle 外掛程式和一般建置邏輯。有關如何使用 TestKit 的一般指南,請參閱專門章節

若要在您的測試中啟用組態快取,您可以將 --configuration-cache 引數傳遞給 GradleRunner,或使用啟用組態快取中描述的其他方法之一。

您需要執行任務兩次。一次是為了預熱組態快取。一次是為了重複使用組態快取。

src/test/kotlin/org/example/BuildLogicFunctionalTest.kt
@Test
fun `my task can be loaded from the configuration cache`() {

    buildFile.writeText("""
        plugins {
            id 'org.example.my-plugin'
        }
    """)

    runner()
        .withArguments("--configuration-cache", "myTask")        (1)
        .build()

    val result = runner()
        .withArguments("--configuration-cache", "myTask")        (2)
        .build()

    require(result.output.contains("Reusing configuration cache.")) (3)
    // ... more assertions on your task behavior
}
src/test/groovy/org/example/BuildLogicFunctionalTest.groovy
def "my task can be loaded from the configuration cache"() {
    given:
    buildFile << """
        plugins {
            id 'org.example.my-plugin'
        }
    """

    when:
    runner()
        .withArguments('--configuration-cache', 'myTask')    (1)
        .build()

    and:
    def result = runner()
        .withArguments('--configuration-cache', 'myTask')    (2)
        .build()

    then:
    result.output.contains('Reusing configuration cache.')      (3)
    // ... more assertions on your task behavior
}
1 第一次執行預熱組態快取。
2 第二次執行重複使用組態快取。
3 斷言組態快取已重複使用。

如果發現組態快取有問題,Gradle 將會使建置失敗並報告問題,並且測試將會失敗。

Gradle 外掛程式的一個良好測試策略是在啟用組態快取的情況下執行其整個測試套件。這需要使用受支援的 Gradle 版本測試外掛程式。

如果外掛程式已經支援一系列 Gradle 版本,它可能已經針對多個 Gradle 版本進行了測試。在這種情況下,我們建議從支援組態快取的 Gradle 版本開始啟用組態快取。

如果無法立即完成,則使用多次執行外掛程式貢獻的所有任務的測試(例如,斷言 UP_TO_DATEFROM_CACHE 行為)也是一個良好的策略。

需求

為了將任務圖的狀態擷取到組態快取中,並在稍後的建置中再次重新載入它,Gradle 對任務和其他建置邏輯應用了某些需求。每個需求都被視為組態快取「問題」,如果存在違規行為,則會使建置失敗。

在大多數情況下,這些需求實際上是浮現了一些未宣告的輸入。換句話說,使用組態快取是一種選擇加入更嚴格、更正確和更可靠的所有建置的方式。

以下章節描述了每個需求以及如何變更您的建置以修正問題。

任務不得參考的特定類型

任務實例不得從其欄位參考許多類型。同樣適用於任務動作,如閉包,例如 doFirst {}doLast {}

這些類型可分為以下幾類

  • 即時 JVM 狀態類型

  • Gradle 模型類型

  • 依賴管理類型

在所有情況下,不允許使用這些類型的原因是它們的狀態不易被組態快取儲存或重新建立。

即時 JVM 狀態類型(例如 ClassLoaderThreadOutputStreamSocket 等...)是被明確禁止的。這些類型幾乎從不代表任務輸入或輸出。唯一的例外是標準流:System.inSystem.outSystem.err。這些流可以用作參數,例如,用於 ExecJavaExec 任務。

Gradle 模型類型(例如 GradleSettingsProjectSourceSetConfiguration 等...)通常用於攜帶一些任務輸入,這些輸入應明確且精確地宣告。

例如,如果您參考 Project 以在執行時取得 project.version,則應改為直接將專案版本宣告為任務的輸入,使用 Property<String>。另一個範例是參考 SourceSet 以稍後取得來源檔案、編譯類別路徑或來源集的輸出。您應該改為將這些宣告為 FileCollection 輸入,並僅參考該輸入。

相同的需求適用於依賴管理類型,但有一些細微差別。

某些類型,例如 ConfigurationSourceDirectorySet,不能作為良好的任務輸入參數,因為它們包含大量不相關的狀態,最好將這些輸入建模為更精確的內容。我們不打算使這些類型可序列化。例如,如果您參考 Configuration 以稍後取得已解析的檔案,則應改為宣告 FileCollection 作為任務的輸入。同樣地,如果您參考 SourceDirectorySet,則應改為宣告 FileTree 作為任務的輸入。

也不允許參考依賴解析結果(例如 ArtifactResolutionQueryResolvedArtifactArtifactResult 等...)。例如,如果您參考某些 ResolvedComponentResult 實例,則應改為宣告 Provider<ResolvedComponentResult> 作為任務的輸入。此提供者可以透過調用 ResolutionResult.getRootComponent() 取得。同樣地,如果您參考某些 ResolvedArtifactResult 實例,則應改為使用 ArtifactCollection.getResolvedArtifacts(),它會傳回 Provider<Set<ResolvedArtifactResult>>,可以將其對應為任務的輸入。經驗法則是,任務不得參考已解析的結果,而應參考延遲規格,以便在執行時進行依賴解析。

某些類型,例如 PublicationDependency,不可序列化,但可能是可序列化的。如有必要,我們可能會允許直接將這些類型用作任務輸入。

以下是一個有問題的任務類型範例,它參考了 SourceSet

build.gradle.kts
abstract class SomeTask : DefaultTask() {

    @get:Input lateinit var sourceSet: SourceSet (1)

    @TaskAction
    fun action() {
        val classpathFiles = sourceSet.compileClasspath.files
        // ...
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {

    @Input SourceSet sourceSet (1)

    @TaskAction
    void action() {
        def classpathFiles = sourceSet.compileClasspath.files
        // ...
    }
}
1 這將被報告為問題,因為不允許參考 SourceSet

以下是應該如何做的

build.gradle.kts
abstract class SomeTask : DefaultTask() {

    @get:InputFiles @get:Classpath
    abstract val classpath: ConfigurableFileCollection (1)

    @TaskAction
    fun action() {
        val classpathFiles = classpath.files
        // ...
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {

    @InputFiles @Classpath
    abstract ConfigurableFileCollection getClasspath() (1)

    @TaskAction
    void action() {
        def classpathFiles = classpath.files
        // ...
    }
}
1 不再報告問題,我們現在參考受支援的類型 FileCollection

同樣地,如果您在腳本中宣告的臨時任務中遇到相同的問題,如下所示

build.gradle.kts
tasks.register("someTask") {
    doLast {
        val classpathFiles = sourceSets.main.get().compileClasspath.files (1)
    }
}
build.gradle
tasks.register('someTask') {
    doLast {
        def classpathFiles = sourceSets.main.compileClasspath.files (1)
    }
}
1 這將被報告為問題,因為 doLast {} 閉包正在捕獲對 SourceSet 的參考

您仍然需要滿足相同的需求,即不參考不允許的類型。以下是如何修正上述任務宣告的方法

build.gradle.kts
tasks.register("someTask") {
    val classpath = sourceSets.main.get().compileClasspath (1)
    doLast {
        val classpathFiles = classpath.files
    }
}
build.gradle
tasks.register('someTask') {
    def classpath = sourceSets.main.compileClasspath (1)
    doLast {
        def classpathFiles = classpath.files
    }
}
1 不再報告問題,doLast {} 閉包現在僅捕獲 classpath,它是受支援的 FileCollection 類型

請注意,有時不允許的類型是間接參考的。例如,您的任務可能會參考外掛程式中的某些類型,這是允許的。該類型可能會參考另一個允許的類型,而該類型又參考了不允許的類型。問題的 HTML 報告中提供的物件圖層次結構視圖應有助於您找出違規者。

在執行期間使用 Project 物件

任務在執行時不得使用任何 Project 物件。這包括在任務執行時呼叫 Task.getProject()

某些情況可以像不允許的類型一樣修正。

通常,ProjectTask 上都有類似的東西。例如,如果您在任務動作中需要 Logger,則應使用 Task.logger 而不是 Project.logger

否則,您可以使用注入的服務來代替 Project 的方法。

以下是一個有問題的任務類型範例,它在執行時使用了 Project 物件

build.gradle.kts
abstract class SomeTask : DefaultTask() {
    @TaskAction
    fun action() {
        project.copy { (1)
            from("source")
            into("destination")
        }
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {
    @TaskAction
    void action() {
        project.copy { (1)
            from 'source'
            into 'destination'
        }
    }
}
1 這將被報告為問題,因為任務動作在執行時使用了 Project 物件

以下是應該如何做的

build.gradle.kts
abstract class SomeTask : DefaultTask() {

    @get:Inject abstract val fs: FileSystemOperations (1)

    @TaskAction
    fun action() {
        fs.copy {
            from("source")
            into("destination")
        }
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {

    @Inject abstract FileSystemOperations getFs() (1)

    @TaskAction
    void action() {
        fs.copy {
            from 'source'
            into 'destination'
        }
    }
}
1 不再報告問題,注入的 FileSystemOperations 服務受支援作為 project.copy {} 的替代方案

同樣地,如果您在腳本中宣告的臨時任務中遇到相同的問題,如下所示

build.gradle.kts
tasks.register("someTask") {
    doLast {
        project.copy { (1)
            from("source")
            into("destination")
        }
    }
}
build.gradle
tasks.register('someTask') {
    doLast {
        project.copy { (1)
            from 'source'
            into 'destination'
        }
    }
}
1 這將被報告為問題,因為任務動作在執行時使用了 Project 物件

以下是如何修正上述任務宣告的方法

build.gradle.kts
interface Injected {
    @get:Inject val fs: FileSystemOperations (1)
}
tasks.register("someTask") {
    val injected = project.objects.newInstance<Injected>() (2)
    doLast {
        injected.fs.copy { (3)
            from("source")
            into("destination")
        }
    }
}
build.gradle
interface Injected {
    @Inject FileSystemOperations getFs() (1)
}
tasks.register('someTask') {
    def injected = project.objects.newInstance(Injected) (2)
    doLast {
        injected.fs.copy { (3)
            from 'source'
            into 'destination'
        }
    }
}
1 服務無法直接在腳本中注入,我們需要一個額外的類型來傳達注入點
2 在任務動作之外使用 project.object 建立額外類型的實例
3 不再報告問題,任務動作參考 injected,它提供了 FileSystemOperations 服務,受支援作為 project.copy {} 的替代方案

如您在上面看到的,修正在腳本中宣告的臨時任務需要相當多的儀式。現在是考慮將您的任務宣告提取為適當的任務類別(如先前所示)的好時機。

下表顯示了應使用哪些 API 或注入的服務來替代每個 Project 方法。

取代 使用

project.rootDir

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.rootDir 計算實際參數的結果。

project.projectDir

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.projectDir 計算實際參數的結果。

project.buildDir

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.buildDir 計算實際參數的結果。

project.name

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.name 計算實際參數的結果。

project.description

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.description 計算實際參數的結果。

project.group

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.group 計算實際參數的結果。

project.version

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.version 計算實際參數的結果。

project.propertiesproject.property(name)project.hasProperty(name)project.getProperty(name)project.findProperty(name)

project.logger

project.provider {}

project.file(path)

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.file(file) 計算實際參數的結果。

project.uri(path)

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.uri(path) 計算實際參數的結果。否則,可以使用 File.toURI() 或其他 JVM API。

project.relativePath(path)

project.files(paths)

project.fileTree(paths)

project.zipTree(path)

project.tarTree(path)

project.resources

任務輸入或輸出屬性或腳本變數,以捕獲使用 project.resource 計算實際參數的結果。

project.copySpec {}

project.copy {}

project.sync {}

project.delete {}

project.mkdir(path)

Kotlin、Groovy 或 Java API 可用於您的建置邏輯。

project.exec {}

project.javaexec {}

project.ant {}

project.createAntBuilder()

從另一個實例存取任務實例

任務不應直接存取另一個任務實例的狀態。相反,任務應使用輸入和輸出關係連接。

請注意,此需求使得在執行時設定其他任務的任務不受支援。

共享可變物件

將任務儲存到組態快取時,所有透過任務欄位直接或間接參考的物件都會被序列化。在大多數情況下,反序列化會保留參考相等性:如果兩個欄位 ab 在組態時參考相同的實例,則在反序列化後,它們將再次參考相同的實例,因此 a == b(或 Groovy 和 Kotlin 語法中的 a === b)仍然成立。但是,為了效能考量,某些類別,尤其是 java.lang.Stringjava.io.File 和許多 java.util.Collection 介面的實作,在序列化時不會保留參考相等性。在反序列化後,參考此類別物件的欄位可能會參考不同的但相等的物件。

讓我們看一個在任務欄位中儲存使用者定義物件和 ArrayList 的任務。

build.gradle.kts
class StateObject {
    // ...
}

abstract class StatefulTask : DefaultTask() {
    @get:Internal
    var stateObject: StateObject? = null

    @get:Internal
    var strings: List<String>? = null
}


tasks.register<StatefulTask>("checkEquality") {
    val objectValue = StateObject()
    val stringsValue = arrayListOf("a", "b")

    stateObject = objectValue
    strings = stringsValue

    doLast { (1)
        println("POJO reference equality: ${stateObject === objectValue}") (2)
        println("Collection reference equality: ${strings === stringsValue}") (3)
        println("Collection equality: ${strings == stringsValue}") (4)
    }
}
build.gradle
class StateObject {
    // ...
}

abstract class StatefulTask extends DefaultTask {
    @Internal
    StateObject stateObject

    @Internal
    List<String> strings
}


tasks.register("checkEquality", StatefulTask) {
    def objectValue = new StateObject()
    def stringsValue = ["a", "b"] as ArrayList<String>

    stateObject = objectValue
    strings = stringsValue

    doLast { (1)
        println("POJO reference equality: ${stateObject === objectValue}") (2)
        println("Collection reference equality: ${strings === stringsValue}") (3)
        println("Collection equality: ${strings == stringsValue}") (4)
    }
}
1 doLast 動作會捕獲封閉範圍內的參考。這些捕獲的參考也會序列化到組態快取。
2 比較儲存在任務欄位中的使用者定義類別物件的參考,以及 doLast 動作中捕獲的參考。
3 比較儲存在任務欄位中的 ArrayList 實例的參考,以及 doLast 動作中捕獲的參考。
4 檢查儲存的列表和捕獲的列表的相等性。

在沒有組態快取的情況下執行建置表明,參考相等性在兩種情況下都得到保留。

❯ gradle --no-configuration-cache checkEquality
> Task :checkEquality
POJO reference equality: true
Collection reference equality: true
Collection equality: true

但是,在啟用組態快取的情況下,只有使用者定義的物件參考是相同的。列表參考是不同的,儘管參考的列表是相等的。

❯ gradle --configuration-cache checkEquality
> Task :checkEquality
POJO reference equality: true
Collection reference equality: false
Collection equality: true

一般而言,不建議在組態和執行階段之間共享可變物件。如果您需要這樣做,您應始終將狀態包裝在您定義的類別中。不能保證標準 Java、Groovy 和 Kotlin 類型或 Gradle 定義的類型的參考相等性會被保留。

請注意,任務之間不會保留參考相等性:每個任務都是其自己的「領域」,因此不可能在任務之間共享物件。相反,您可以使用建置服務來包裝共享狀態。

存取任務擴充功能或慣例

任務在執行時不應存取慣例和擴充功能,包括額外屬性。相反,與任務執行相關的任何值都應建模為任務屬性。

使用建置監聽器

外掛程式和建置腳本不得註冊任何建置監聽器。也就是在組態時註冊,但在執行時收到通知的監聽器。例如 BuildListenerTaskExecutionListener

這些應替換為建置服務,如果需要接收有關任務執行的資訊,則註冊建置服務。使用資料流動作來處理建置結果,而不是 buildFinished 監聽器。

執行外部程序

外掛程式和建置腳本應避免在組態時執行外部程序。一般而言,最好在具有正確宣告的輸入和輸出的任務中執行外部程序,以避免在任務為最新狀態時進行不必要的工作。但是,如果需要,您應僅使用以下描述的與組態快取相容的 API,而不是 Java 和 Groovy 標準 API,或 Gradle 提供的 Project.execProject.javaexecExecOperations.execExecOperations.javaexec 方法。這些方法的靈活性使 Gradle 無法確定呼叫如何影響建置組態,因此難以確保可以安全地重複使用組態快取條目。

對於更簡單的情況,當擷取程序的輸出就足夠時,可以使用 providers.exec()providers.javaexec()

build.gradle.kts
val gitVersion = providers.exec {
    commandLine("git", "--version")
}.standardOutput.asText.get()
build.gradle
def gitVersion = providers.exec {
    commandLine("git", "--version")
}.standardOutput.asText.get()

對於更複雜的情況,可以使用自訂的 ValueSource 實作,並注入 ExecOperations。此 ExecOperations 實例可以在組態時不受限制地使用。

build.gradle.kts
abstract class GitVersionValueSource : ValueSource<String, ValueSourceParameters.None> {
    @get:Inject
    abstract val execOperations: ExecOperations

    override fun obtain(): String {
        val output = ByteArrayOutputStream()
        execOperations.exec {
            commandLine("git", "--version")
            standardOutput = output
        }
        return String(output.toByteArray(), Charset.defaultCharset())
    }
}
build.gradle
abstract class GitVersionValueSource implements ValueSource<String, ValueSourceParameters.None> {
    @Inject
    abstract ExecOperations getExecOperations()

    String obtain() {
        ByteArrayOutputStream output = new ByteArrayOutputStream()
        execOperations.exec {
            it.commandLine "git", "--version"
            it.standardOutput = output
        }
        return new String(output.toByteArray(), Charset.defaultCharset())
    }
}

您也可以在 ValueSource 中使用標準 Java/Kotlin/Groovy 程序 API,例如 java.lang.ProcessBuilder

然後可以使用 providers.of,使用 ValueSource 實作來建立提供者

build.gradle.kts
val gitVersionProvider = providers.of(GitVersionValueSource::class) {}
val gitVersion = gitVersionProvider.get()
build.gradle
def gitVersionProvider = providers.of(GitVersionValueSource.class) {}
def gitVersion = gitVersionProvider.get()

在這兩種方法中,如果在組態時使用提供者的值,它將成為建置組態輸入。外部程序將針對每個建置執行,以確定組態快取是否為最新狀態,因此建議僅在組態時呼叫快速執行的程序。如果值變更,則快取會失效,並且程序將在此建置期間再次執行,作為組態階段的一部分。

讀取系統屬性和環境變數

外掛程式和建置腳本可以在組態時直接使用標準 Java、Groovy 或 Kotlin API 或值供應器 API 讀取系統屬性和環境變數。這樣做會使此類變數或屬性成為建置組態輸入,因此變更值會使組態快取失效。組態快取報告包含這些建置組態輸入的列表,以協助追蹤它們。

一般而言,您應避免在組態時讀取系統屬性和環境變數的值,以避免值變更時發生快取未命中。相反,您可以將 providers.systemProperty()providers.environmentVariable() 傳回的 Provider 連接到任務屬性。

不鼓勵某些可能列舉所有環境變數或系統屬性的存取模式(例如,呼叫 System.getenv().forEach() 或使用其 keySet() 的迭代器)。在這種情況下,Gradle 無法找出哪些屬性是實際的建置組態輸入,因此每個可用的屬性都變成一個輸入。即使新增一個新的屬性,如果使用此模式,也會使快取失效。

使用自訂述詞來篩選環境變數是這種不鼓勵的模式的一個範例

build.gradle.kts
val jdkLocations = System.getenv().filterKeys {
    it.startsWith("JDK_")
}
build.gradle
def jdkLocations = System.getenv().findAll {
    key, _ -> key.startsWith("JDK_")
}

述詞中的邏輯對於組態快取是不透明的,因此所有環境變數都被視為輸入。減少輸入數量的一種方法是始終使用查詢具體變數名稱的方法,例如 getenv(String)getenv().get()

build.gradle.kts
val jdkVariables = listOf("JDK_8", "JDK_11", "JDK_17")
val jdkLocations = jdkVariables.filter { v ->
    System.getenv(v) != null
}.associate { v ->
    v to System.getenv(v)
}
build.gradle
def jdkVariables = ["JDK_8", "JDK_11", "JDK_17"]
def jdkLocations = jdkVariables.findAll { v ->
    System.getenv(v) != null
}.collectEntries { v ->
    [v, System.getenv(v)]
}

但是,上面修正的程式碼與原始程式碼並不完全等效,因為僅支援明確的變數列表。基於字首的篩選是一種常見的場景,因此有基於提供者的 API 可以存取系統屬性環境變數

build.gradle.kts
val jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")
build.gradle
def jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")

請注意,不僅當變數的值變更或變數被移除時,組態快取會失效,而且當另一個具有相符字首的變數新增到環境中時,組態快取也會失效。

對於更複雜的用例,可以使用自訂的 ValueSource 實作。在 ValueSource 程式碼中參考的系統屬性和環境變數不會成為建置組態輸入,因此可以應用任何處理。相反,ValueSource 的值會在每次建置執行時重新計算,並且僅當值變更時,組態快取才會失效。例如,ValueSource 可以用於取得名稱包含子字串 JDK 的所有環境變數

build.gradle.kts
abstract class EnvVarsWithSubstringValueSource : ValueSource<Map<String, String>, EnvVarsWithSubstringValueSource.Parameters> {
    interface Parameters : ValueSourceParameters {
        val substring: Property<String>
    }

    override fun obtain(): Map<String, String> {
        return System.getenv().filterKeys { key ->
            key.contains(parameters.substring.get())
        }
    }
}
val jdkLocationsProvider = providers.of(EnvVarsWithSubstringValueSource::class) {
    parameters {
        substring = "JDK"
    }
}
build.gradle
abstract class EnvVarsWithSubstringValueSource implements ValueSource<Map<String, String>, Parameters> {
    interface Parameters extends ValueSourceParameters {
        Property<String> getSubstring()
    }

    Map<String, String> obtain() {
        return System.getenv().findAll { key, _ ->
            key.contains(parameters.substring.get())
        }
    }
}
def jdkLocationsProvider = providers.of(EnvVarsWithSubstringValueSource.class) {
    parameters {
        substring = "JDK"
    }
}

未宣告的檔案讀取

外掛程式和建置腳本不應在組態時使用 Java、Groovy 或 Kotlin API 直接讀取檔案。相反,應使用值供應器 API 將檔案宣告為潛在的建置組態輸入。

此問題是由類似於以下的建置邏輯引起的

build.gradle.kts
val config = file("some.conf").readText()
build.gradle
def config = file('some.conf').text

若要修正此問題,請改為使用 providers.fileContents() 讀取檔案

build.gradle.kts
val config = providers.fileContents(layout.projectDirectory.file("some.conf"))
    .asText
build.gradle
def config = providers.fileContents(layout.projectDirectory.file('some.conf'))
    .asText

一般而言,您應避免在組態時讀取檔案,以避免在檔案內容變更時使組態快取條目失效。相反,您可以將 providers.fileContents() 傳回的 Provider 連接到任務屬性。

位元組碼修改和 Java Agent

為了偵測組態輸入,Gradle 會修改建置腳本類別路徑上類別的位元組碼,例如外掛程式及其依賴項。Gradle 使用 Java Agent 來修改位元組碼。由於變更的位元組碼或 Agent 的存在,某些程式庫的完整性自我檢查可能會失敗。

若要解決此問題,您可以使用具有類別載入器或程序隔離的 Worker API 來封裝程式庫程式碼。Worker 類別路徑的位元組碼未被修改,因此自我檢查應通過。當使用程序隔離時,Worker 動作會在沒有安裝 Gradle Java Agent 的單獨 Worker 程序中執行。

在簡單的情況下,當程式庫也提供命令列進入點(public static void main() 方法)時,您也可以使用 JavaExec 任務來隔離程式庫。

憑證和密碼的處理

組態快取目前沒有選項可以阻止儲存用作輸入的密碼,因此它們可能會最終出現在序列化的組態快取條目中,預設情況下,該條目儲存在您專案目錄下的 .gradle/configuration-cache 中。

為了降低意外洩露的風險,Gradle 會加密組態快取。Gradle 會在需要時透明地產生機器特定的密鑰,將其快取在 GRADLE_USER_HOME 目錄下,並使用它來加密專案特定快取中的資料。

為了進一步增強安全性,請確保

  • 保護對組態快取條目的存取;

  • 利用 GRADLE_USER_HOME/gradle.properties 來儲存密碼。該檔案的內容不是組態快取的一部分,只有其指紋。如果您將密碼儲存在該檔案中,則必須注意保護對檔案內容的存取。

請參閱 gradle/gradle#22618

透過 GRADLE_ENCRYPTION_KEY 環境變數提供加密金鑰

預設情況下,Gradle 會自動產生和管理加密金鑰,作為儲存在 GRADLE_USER_HOME 目錄下的 Java 金鑰庫。

對於不希望這樣做的環境(例如,當 GRADLE_USER_HOME 目錄在多個機器之間共享時),您可以透過 GRADLE_ENCRYPTION_KEY 環境變數向 Gradle 提供在讀取或寫入快取組態資料時要使用的確切加密金鑰。

您必須確保在多個 Gradle 執行中一致地提供相同的加密金鑰,否則 Gradle 將無法重複使用現有的快取組態。

產生與 GRADLE_ENCRYPTION_KEY 相容的加密金鑰

若要讓 Gradle 使用使用者指定的加密金鑰來加密組態快取,您必須在執行 Gradle 時,將 GRADLE_ENCRYPTION_KEY 環境變數設定為有效的 AES 金鑰,並以 Base64 字串編碼。

產生 Base64 編碼、與 AES 相容金鑰的一種方法是使用像這樣的命令

❯ openssl rand -base64 16

這個命令應該可以在 Linux、Mac OS 或 Windows 上運作,如果使用像 Cygwin 這樣的工具。

接著您可以使用該命令產生的 Base64 編碼金鑰,並將其設定為 GRADLE_ENCRYPTION_KEY 環境變數的值。

尚未實作

某些 Gradle 功能尚實作組態快取支援。這些功能的支援將在之後的 Gradle 版本中新增。

共用組態快取

組態快取目前僅在本機儲存。它可以被熱或冷的本機 Gradle 常駐程式重複使用。但它無法在開發人員或 CI 機器之間共用。

請參閱 gradle/gradle#13510

原始碼相依性

實作 原始碼相依性 的支援。啟用組態快取後,不會回報問題,但建置會失敗。

請參閱 gradle/gradle#13506

將 Java Agent 與使用 TestKit 執行的建置搭配使用

當使用 TestKit 執行建置時,組態快取可能會干擾應用於這些建置的 Java Agent,例如 Jacoco agent。

請參閱 gradle/gradle#25979

將 Gradle 屬性作為建置組態輸入進行細緻追蹤

目前,所有 Gradle 屬性的外部來源(專案目錄和 GRADLE_USER_HOME 中的 gradle.properties、設定屬性的環境變數和系統屬性,以及使用命令列旗標指定的屬性)都被視為建置組態輸入,無論在組態時實際使用了哪些屬性。但是,這些來源不包含在組態快取報告中。

請參閱 gradle/gradle#20969

Java 物件序列化

Gradle 允許支援 Java 物件序列化 協定的物件儲存在組態快取中。

實作目前僅限於可序列化類別,這些類別要麼實作 java.io.Externalizable 介面,要麼實作 java.io.Serializable 介面並定義以下方法組合之一

  • 結合 writeObject 方法和 readObject 方法,以精確控制要儲存哪些資訊;

  • 沒有對應 readObjectwriteObject 方法;writeObject 最終必須呼叫 ObjectOutputStream.defaultWriteObject

  • 沒有對應 writeObjectreadObject 方法;readObject 最終必須呼叫 ObjectInputStream.defaultReadObject

  • writeReplace 方法,允許類別指定要寫入的替換項;

  • readResolve 方法,允許類別指定剛讀取物件的替換項;

以下 Java 物件序列化 功能受支援

  • serialPersistentFields 成員,用於明確宣告哪些欄位可序列化;如果存在該成員,則會被忽略;組態快取會將除了 transient 欄位之外的所有欄位都視為可序列化;

  • ObjectOutputStream 的以下方法不受支援,並會擲回 UnsupportedOperationException

    • reset()writeFields()putFields()writeChars(String)writeBytes(String)writeUnshared(Any?)

  • ObjectInputStream 的以下方法不受支援,並會擲回 UnsupportedOperationException

    • readLine()readFully(ByteArray)readFully(ByteArray, Int, Int)readUnshared()readFields()transferTo(OutputStream)readAllBytes()

  • 透過 ObjectInputStream.registerValidation 註冊的驗證會被直接忽略;

  • readObjectNoData 方法(如果存在)永遠不會被調用;

請參閱 gradle/gradle#13588

在執行時期存取建置腳本的頂層方法和變數

在建置腳本中重複使用邏輯和資料的常見方法是將重複位元提取到頂層方法和變數中。但是,如果啟用組態快取,則目前不支援在執行時期呼叫這些方法。

對於以 Groovy 撰寫的建置腳本,任務會失敗,因為找不到該方法。以下程式碼片段在 listFiles 任務中使用了頂層方法

build.gradle
def dir = file('data')

def listFiles(File dir) {
    dir.listFiles({ file -> file.isFile() } as FileFilter).name.sort()
}

tasks.register('listFiles') {
    doLast {
        println listFiles(dir)
    }
}

啟用組態快取執行任務會產生以下錯誤

Execution failed for task ':listFiles'.
> Could not find method listFiles() for arguments [/home/user/gradle/samples/data] on task ':listFiles' of type org.gradle.api.DefaultTask.

為了防止任務失敗,請將引用的頂層方法轉換為類別中的靜態方法

build.gradle
def dir = file('data')

class Files {
    static def listFiles(File dir) {
        dir.listFiles({ file -> file.isFile() } as FileFilter).name.sort()
    }
}

tasks.register('listFilesFixed') {
    doLast {
        println Files.listFiles(dir)
    }
}

以 Kotlin 撰寫的建置腳本完全無法在組態快取中儲存參考執行時期頂層方法或變數的任務。此限制存在的原因是捕獲的腳本物件參考無法序列化。首次執行 Kotlin 版本的 listFiles 任務會因組態快取問題而失敗。

build.gradle.kts
val dir = file("data")

fun listFiles(dir: File): List<String> =
    dir.listFiles { file: File -> file.isFile }.map { it.name }.sorted()

tasks.register("listFiles") {
    doLast {
        println(listFiles(dir))
    }
}

為了使此任務的 Kotlin 版本與組態快取相容,請進行以下變更

build.gradle.kts
object Files { (1)
    fun listFiles(dir: File): List<String> =
        dir.listFiles { file: File -> file.isFile }.map { it.name }.sorted()
}

tasks.register("listFilesFixed") {
    val dir = file("data") (2)
    doLast {
        println(Files.listFiles(dir))
    }
}
1 在物件內定義方法。
2 在較小的範圍內定義變數。

請參閱 gradle/gradle#22879

使用建置服務使組態快取失效

目前,如果 ValueSource 的值在組態時被存取,則無法將 BuildServiceProvider 或從其衍生的提供者與 mapflatMap 一起用作 ValueSource 的參數。當在作為組態階段一部分執行的任務中取得此類 ValueSource 時,情況也是如此,例如 buildSrc 建置或包含貢獻外掛程式的建置的任務。請注意,使用 @ServiceReference 或將 BuildServiceProvider 儲存在任務的 @Internal 註解屬性中是安全的。一般來說,此限制使得無法使用 BuildService 使組態快取失效。

請參閱 gradle/gradle#24085