介绍

配置缓存是一项通过缓存配置阶段的结果并在后续构建中重用它来显着提高构建性能的功能。使用配置缓存,当没有任何影响构建配置(例如构建脚本)发生更改时,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/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 http://gradle.github.net.cn/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 报告中找到:

问题报告

该报告将这组问题显示两次。首先按问题消息分组,然后按任务分组。前者可以让您快速查看您的构建所面临的问题类别。后者可以让您快速查看哪些任务有问题。在这两种情况下,您都可以展开树以发现罪魁祸首在对象图中的位置。

该报告还包括检测到的构建配置输入的列表,例如在配置阶段读取的环境变量、系统属性和值提供者:

输入报告

报告中显示的问题具有指向相应要求的链接,您可以在其中找到有关如何解决问题的指导或相应的尚未实现的功能。

当更改构建或插件来解决问题时,您应该考虑使用 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 http://gradle.github.net.cn/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 http://gradle.github.net.cn/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-serializationtrue

谨慎地忽略配置输入,并且仅当它们不影响配置逻辑生成的任务时才忽略。未来版本中将删除对这些选项的支持。

测试您的构建逻辑

Gradle TestKit(又名 TestKit)是一个帮助测试 Gradle 插件和构建逻辑的库。有关如何使用 TestKit 的一般指南,请参阅专门章节

要在测试中启用配置缓存,您可以将参数传递--configuration-cacheGradleRunner或使用启用配置缓存中描述的其他方法之一。

您需要运行任务两次。一次填充配置缓存。一次重用配置缓存。

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 状态类型(例如ClassLoaderThreadOutputStream等...​)。Socket这些类型几乎从不代表任务输入或输出。唯一的例外是标准流:System.inSystem.outSystem.err。例如,这些流可以用作任务的Exec参数JavaExec

Gradle 模型类型(例如GradleSettingsProjectSourceSetConfiguration...​)通常用于携带一些应显式且精确声明的任务输入。

例如,如果您引用 aProject来获取project.version执行时的 ,您应该使用 a 直接将项目版本声明为任务的输入Property<String>。另一个例子是引用 aSourceSet以便稍后获取源文件、编译类路径或源集的输出。相反,您应该将它们声明为FileCollection输入并引用它。

同样的要求也适用于依赖管理类型,但有一些细微差别。

某些类型(例如ConfigurationSourceDirectorySet)不能构成良好的任务输入参数,因为它们包含许多不相关的状态,最好将这些输入建模为更精确的东西。我们根本不打算使这些类型可序列化。例如,如果您引用 aConfiguration以便稍后获取已解析的文件,则您应该将 a 声明FileCollection为任务的输入。同样,如果您引用 a,SourceDirectorySet则应该将 a 声明FileTree为任务的输入。

也不允许引用依赖关系解析结果(例如ArtifactResolutionQueryResolvedArtifact等等ArtifactResult......)。例如,如果您引用某些ResolvedComponentResult实例,则应该将 a 声明Provider<ResolvedComponentResult>为任务的输入。这样的提供者可以通过调用来获得ResolutionResult.getRootComponent()。同样,如果您引用某些ResolvedArtifactResult实例,则应该使用ArtifactCollection.getResolvedArtifacts()它返回Provider<Set<ResolvedArtifactResult>>可以映射为任务的输入的值。经验法则是任务不得引用已解析的结果,而是引用惰性规范,以便在执行时进行依赖项解析。

某些类型(例如Publication或 )Dependency不可序列化,但可以。如有必要,我们可以允许它们直接用作任务输入。

以下是引用 a 的有问题的任务类型的示例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()在任务运行时调用。

某些情况可以采用与不允许的类型相同的方式进行修复。

Project通常,类似的东西在和上都可用Task。例如,如果Logger您的任务操作中需要 a ,则应使用 ,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()

从另一个实例访问任务实例

任务不应直接访问另一个任务实例的状态。相反,任务应该使用输入和输出关系来连接。

请注意,此要求使得不支持编写在执行时配置其他任务的任务。

共享可变对象

将任务存储到配置缓存时,将序列化通过任务字段直接或间接引用的所有对象。在大多数情况下,反序列化保留引用相等性:如果两个字段在配置时引用同一个实例,那么在反序列化时它们将再次引用同一个实例,因此a(或在 Groovy 和 Kotlin 语法中)仍然成立。然而,出于性能原因,某些类(特别是、和接口的许多实现)在不保留引用相等性的情况下进行序列化。反序列化时,引用此类对象的字段可以引用不同但相同的对象。ba == ba === bjava.lang.Stringjava.io.Filejava.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 定义的类型保留引用相等性。

请注意,任务之间不保留引用相等性:每个任务都是其自己的“领域”,因此不可能在任务之间共享对象。相反,您可以使用构建服务来包装共享状态。

访问任务扩展或约定

任务不应在执行时访问约定和扩展,包括额外的属性。相反,与任务执行相关的任何值都应建模为任务属性。

使用构建监听器

插件和构建脚本不得注册任何构建侦听器。即在配置时注册的侦听器在执行时收到通知。例如 aBuildListener或 a TaskExecutionListener

这些应该被构建服务取代,如果需要的话,注册以接收有关任务执行的信息。使用数据流操作而不是侦听器来处理构建结果buildFinished​​。

运行外部进程

插件和构建脚本应避免在配置时运行外部进程。一般来说,最好在具有正确声明的输入和输出的任务中运行外部进程,以避免任务是最新的时不必要的工作。如有必要,应仅使用与配置缓存兼容的 API,而不是使用 Java 和 Groovy 标准 API 或设置和初始化脚本中现有的ExecOperationsProject.execProject.javaexec等。对于更简单的情况,当抓取进程的输出就足够时, 可以使用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()

对于更复杂的情况,可以使用注入的自定义ValueSourceExecOperations实现。该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实现可用于通过providers.of创建提供程序:

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()或使用 it 的迭代器)。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每次构建运行时都会重新计算的值,并且仅当该值更改时配置缓存才会失效。例如, aValueSource可用于获取名称包含子字符串 的所有环境变量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代理

为了检测配置输入,Gradle 修改构建脚本类路径上的类的字节码,例如插件及其依赖项。 Gradle 使用 Java 代理来修改字节码。由于字节码更改或代理的存在,某些库的完整性自检可能会失败。

要解决此问题,您可以使用带有类加载器或进程隔离的Worker API来封装库代码。工作线程类路径的字节码未修改,因此自检应该通过。使用进程隔离时,工作操作将在未安装 Gradle Java 代理的单独工作进程中执行。

在简单的情况下,当库还提供命令行入口点(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 提供确切的加密密钥,以便在通过GRADLE_ENCRYPTION_KEY环境变量读取或写入缓存的配置数据时使用。

您必须确保在多次 Gradle 运行中一致提供相同的加密密钥,否则 Gradle 将无法重用现有的缓存配置。

生成与 GRADLE_ENCRYPTION_KEY 兼容的加密密钥

为了让 Gradle 使用用户指定的加密密钥加密配置缓存,您必须运行 Gradle,同时使用有效的 AES 密钥(编码为 Base64 字符串)设置 GRADLE_ENCRYPTION_KEY 环境变量。

生成 Base64 编码的 AES 兼容密钥的一种方法是使用如下命令:

❯ openssl rand -base64 16

如果使用 Cygwin 等工具,此命令应该适用于 Linux、Mac OS 或 Windows。

然后,您可以使用该命令生成的 Base64 编码密钥并将其设置为环境变量的值 GRADLE_ENCRYPTION_KEY

尚未实现

尚未实现对某些 Gradle 功能使用配置缓存的支持。对这些功能的支持将在以后的 Gradle 版本中添加。

共享配置缓存

配置缓存当前仅存储在本地。它可以被热或冷的本地 Gradle 守护进程重用。但它不能在开发人员或 CI 机器之间共享。

请参阅gradle/gradle#13510

源依赖

对源依赖项的支持尚未实现。启用配置缓存后,不会报告任何问题,并且构建会失败。

请参阅gradle/gradle#13506

使用 Java 代理和使用 TestKit 运行的构建

使用TestKit运行构建时,配置缓存可能会干扰应用于这些构建的 Java 代理,例如 Jacoco 代理。

请参阅gradle/gradle#25979

作为构建配置输入的 Gradle 属性的细粒度跟踪

目前,Gradle 属性的所有外部源(gradle.properties在项目目录中以及设置属性的环境变量和系统属性中GRADLE_USER_HOME,以及使用命令行标志指定的属性)都被视为构建配置输入,无论配置时实际使用哪些属性。但是,这些源不包含在配置缓存报告中。

请参阅gradle/gradle#20969

Java对象序列化

Gradle 允许将支持Java 对象序列化协议的对象存储在配置缓存中。

目前,该实现仅限于实现该java.io.Serializable接口并定义以下方法组合之一的可序列化类:

  • 一种writeObject与方法相结合的方法readObject来精确控制要存储哪些信息;

  • writeObject没有对应的方法readObjectwriteObject最终必须调用ObjectOutputStream.defaultWriteObject

  • readObject没有对应的方法writeObjectreadObject最终必须调用ObjectInputStream.defaultReadObject

  • writeReplace允许类指定要编写的替换的方法;

  • readResolve允许类指定刚刚读取的对象的替换的方法;

支持以下Java 对象序列化功能:

  • 实现java.io.Externalizable接口的可序列化类;此类的对象在序列化期间被配置缓存丢弃并报告为问题;

  • 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

使用构建服务使配置缓存失效

目前,如果在配置时访问的值,则不可能将BuildServiceProvider派生自它的 或 提供程序与mapflatMap作为 的参数一起使用。当在作为配置阶段的一部分执行的任务中获得这样的 a 时,例如构建或包含的构建贡献插件的任务,同样适用。请注意,使用或存储在任务的带注释的属性中是安全的。一般来说,这个限制使得无法使用a来使配置缓存失效。ValueSourceValueSourceValueSourcebuildSrc@ServiceReferenceBuildServiceProvider@InternalBuildService

请参阅gradle/gradle#24085