介绍

配置缓存是一项功能,可通过缓存 配置阶段 的结果并将其重新用于后续构建来显着提高构建性能。使用配置缓存,Gradle 可以在没有任何影响构建配置的内容(例如构建脚本)发生更改时完全跳过配置阶段。 Gradle 还将性能改进应用于任务执行。

配置缓存在概念上类似于 建立缓存 ,但缓存不同的信息。构建缓存负责缓存构建的输出和中间文件,例如任务输出或构件转换输出。配置缓存负责缓存一组特定任务的构建配置。换句话说,配置缓存保存配置阶段的输出,构建缓存保存执行阶段的输出。

默认情况下当前未启用此功能。此功能具有以下限制:

  • 配置缓存不支持所有 核心 Gradle 插件特征 。全面支持正在进行中。

  • 您的构建和您依赖的插件可能需要更改才能满足 requirements

  • 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 configuration cache 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

您还可以使用 org.gradle.configuration-cache 属性在 gradle.properties 文件中永久启用缓存:

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 天未使用,它们将被删除。

稳定的配置缓存

为了稳定配置缓存,我们在功能标志后面实施了一些严格措施,因为它被认为对早期采用者来说太具有破坏性。

您可以按如下方式启用该功能标志:

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

STABLE_CONFIGURATION_CACHE 功能标志启用以下功能:

未声明的共享构建服务使用

启用后,使用 共享构建服务 而未通过 Task.usesService 方法声明要求的任务将发出弃用警告。

此外,当未启用配置缓存但存在功能标志时,也会启用以下 配置缓存要求 的弃用:

建议尽快启用它,以便在我们删除标志并将链接的功能设置为默认功能时做好准备。

集成开发环境支持

如果您从 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 集成开发环境

在 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 插件社区插件 中当前支持的信息。

社区插件

请参阅问题 渐变/渐变#13490 以了解社区插件的状态。

故障排除

以下部分将介绍一些处理配置缓存问题的一般准则。这适用于您的构建逻辑和 Gradle 插件。

如果未能序列化运行任务所需的状态,则会生成检测到的问题的 HTML 报告。 Gradle 失败输出包括指向报告的可点击链接。此报告很有用,可让您深入了解问题,了解问题的成因。

让我们看一个包含几个问题的简单示例构建脚本:

KotlinGroovy
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)
        }
    }
}

运行该任务失败并在控制台中打印以下内容:

❯ 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://docs.gradle.org/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 configuration cache 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://docs.gradle.org/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://docs.gradle.org/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 报告中的链接,我们可以解决我们的问题。这是构建脚本的固定版本:

KotlinGroovy
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.set(projectDir.dir("source"))
    destination.set(projectDir.dir(System.getProperty("someDestination")))
}
1 我们将我们的临时任务变成了一个合适的任务类,
2 带有输入和输出声明,
3 并注入了 FileSystemOperations 服务,这是 project.copy {} 的支持替代品。

现在运行任务两次成功,没有报告任何问题,并在第二次运行时重用配置缓存:

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

0 problems were found storing the configuration cache.

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.
❯ 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

0 problems were found storing the configuration cache.

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.

之前的configuration cache entry无法复用,task graph不得不重新计算存储。这是因为我们在配置时读取系统属性,因此要求 Gradle 在该属性的值更改时再次运行配置阶段。修复它就像获取系统属性的提供者并将其连接到任务输入一样简单,而无需在配置时读取它。

KotlinGroovy
build.gradle.kts
tasks.register<MyCopyTask>("someTask") {
    val projectDir = layout.projectDirectory
    source.set(projectDir.dir("source"))
    destination.set(projectDir.dir(providers.systemProperty("someDestination"))) (1)
}
1 我们直接连接系统属性提供者,而不是在配置时读取它。

通过这个简单的更改,我们可以多次运行任务,更改系统属性值,并重用配置缓存:

❯ gradle --configuration-cache someTask -DsomeDestination=dest
Calculating task graph as no configuration cache 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 报告和下面 requirements 章节中解释的解决方案。

:help 开始

始终从使用最简单的任务 :help 尝试您的构建或插件开始。这将执行构建或插件的最小配置阶段。

逐步瞄准有用的任务

不要马上运行 build。你也可以使用--dry-run先发现更多的配置时间问题。

在进行构建时,逐步瞄准您的开发反馈循环。例如,在对源代码进行一些更改后运行测试。

在使用插件时,逐步定位贡献的或配置的任务。

通过将问题转化为警告进行探索

不要在第一次构建失败和 化问题为警示 时停下来发现您的构建和插件的行为方式。如果构建失败,请使用 HTML 报告来推断所报告的与失败相关的问题。继续运行更有用的任务。

这将使您对构建和插件面临的问题的性质有一个很好的了解。请记住,将问题转化为警告时,您可能需要 手动使缓存失效 以防出现问题。

退一步迭代解决问题

当您觉得自己对需要解决的问题了解得足够多时,请退后一步,开始迭代解决最重要的问题。使用 HTML 报告和此文档来帮助您完成此旅程。

启动storing 配置缓存时报告的问题。修复后,您可以依赖有效的缓存配置阶段并继续修复 loading 配置缓存(如果有)时报告的问题。

报告遇到的问题

如果您遇到本文档未涵盖的 Gradle 功能Gradle核心插件 问题,请在 gradle/gradle 上报告问题。

如果您遇到社区 Gradle 插件的问题,请查看它是否已列在 渐变/渐变#13490 并考虑将问题报告给插件的问题跟踪器。

报告此类问题的一个好方法是提供以下信息:

  • 这个文档的链接,

  • 你试过的插件版本,

  • 插件的自定义配置(如果有),或者理想情况下是复制器构建,

  • 失败原因的描述,例如给定任务的问题

  • 构建失败的副本,

  • 独立的 configuration-cache-report.html 文件。

测试,测试,测试

考虑为您的构建逻辑添加测试。有关配置缓存,请参阅以下有关 测试你的构建逻辑 的部分。这将帮助您迭代所需的更改并防止未来的回归。

将其推广给您的团队

一旦您的开发人员工作流程正常运行,例如从 IDE 运行测试,您就可以考虑为您的团队启用它。更改代码和运行测试时更快的周转可能是值得的。您可能希望首先选择加入。

如果需要,将问题转化为警告并在构建 gradle.properties 文件中设置允许的最大问题数。默认情况下禁用配置缓存。让您的团队知道他们可以选择加入,例如,在支持的工作流的 IDE 运行配置上启用配置缓存。

稍后,当更多的工作流程开始工作时,您可以翻转它。默认情况下启用配置缓存,配置 CI 以禁用它,如果需要,则传达需要禁用配置缓存的不受支持的工作流。

测试构建逻辑

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

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

你需要运行你的任务两次。一次准备配置缓存。一次重用配置缓存。

KotlinGroovy
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
}
1 首次运行会启动配置缓存。
2 第二次运行重用配置缓存。
3 断言配置缓存被重用。

如果发现配置缓存存在问题,那么 Gradle 将导致构建失败并报告问题,并且测试将失败。

Gradle 插件的一个好的测试策略是在启用配置缓存的情况下运行其整个测试套件。这需要使用支持的 Gradle 版本测试插件。

如果插件已经支持一系列 Gradle 版本,它可能已经对多个 Gradle 版本进行了测试。在这种情况下,我们建议从支持它的 Gradle 版本开始启用配置缓存。

如果这不能立即完成,使用多次运行插件贡献的所有任务的测试,例如断言 UP_TO_DATEFROM_CACHE 行为,也是一个很好的策略。

要求

为了将任务图的状态捕获到配置缓存并在以后的构建中重新加载它,Gradle 将某些要求应用于任务和其他构建逻辑。这些要求中的每一个都被视为配置缓存“问题”,如果存在违规,构建将失败。

在大多数情况下,这些要求实际上是在浮现一些未声明的输入。换句话说,使用配置缓存是对所有构建的更严格、正确性和可靠性的选择。

以下部分描述了每个要求以及如何更改构建以解决问题。

某些类型不能被任务引用

任务实例不得从其字段中引用许多类型。这同样适用于作为闭包的任务操作,例如 doFirst {}doLast {}

这些类型分为以下几类:

  • 实时 JVM 状态类型

  • Gradle 模型类型

  • 依赖管理类型

在所有情况下,不允许使用这些类型的原因是配置缓存无法轻松存储或重新创建它们的状态。

Live JVM 状态类型(例如 ClassLoaderThreadOutputStreamSocket 等…)是不允许的。这些类型几乎从不代表任务输入或输出。

Gradle 模型类型(例如 GradleSettingsProjectSourceSetConfiguration 等……)通常用于携带一些应该明确和精确声明的任务输入。

例如,如果您引用 Project 以便在执行时获取 project.version,则您应该使用 Property<String> 直接将 project version 声明为任务的输入。另一个示例是引用 SourceSet 以稍后获取源文件、编译类路径或源集的输出。您应该改为将它们声明为 FileCollection 输入并仅引用它。

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

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

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

某些类型,如 PublicationDependency 不可序列化,但可以序列化。如有必要,我们可能会允许将这些直接用作任务输入。

以下是引用 SourceSet 的有问题任务类型的示例:

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

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

    @TaskAction
    fun action() {
        val classpathFiles = sourceSet.compileClasspath.files
        // ...
    }
}
1 这将被报告为一个问题,因为不允许引用 SourceSet

以下是应该如何完成:

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

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

    @TaskAction
    fun action() {
        val classpathFiles = classpath.files
        // ...
    }
}
1 没有更多问题报告,我们现在引用支持的类型 FileCollection

同样,如果您在脚本中声明的临时任务遇到相同的问题,如下所示:

KotlinGroovy
build.gradle.kts
tasks.register("someTask") {
    doLast {
        val classpathFiles = sourceSets.main.get().compileClasspath.files (1)
    }
}
1 这将被报告为一个问题,因为 doLast {} 闭包正在捕获对 SourceSet 的引用

您仍然需要满足相同的要求,即不引用不允许的类型。以下是如何修复上面的任务声明:

KotlinGroovy
build.gradle.kts
tasks.register("someTask") {
    val classpath = sourceSets.main.get().compileClasspath (1)
    doLast {
        val classpathFiles = classpath.files
    }
}
1 没有更多的问题报告,doLast {} 闭包现在只捕获 classpath 这是受支持的 FileCollection 类型

请注意,有时会间接引用不允许的类型。例如,您可以让任务从允许的插件中引用某种类型。该类型可以引用另一个允许的类型,而该类型又引用一个不允许的类型。 HTML 问题报告中提供的对象图的层次结构视图应该可以帮助您查明违规者。

Using the Project object

任务在执行时不得使用任何 Project 对象。这包括在任务运行时调用 Task.getProject()

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

通常,ProjectTask 上都有类似的东西。例如,如果您在任务操作中需要 Logger,则应使用 Task.logger 而不是 Project.logger

否则,您可以使用 注入服务 而不是 Project 的方法。

以下是在执行时使用 Project 对象的有问题任务类型的示例:

KotlinGroovy
build.gradle.kts
abstract class SomeTask : DefaultTask() {
    @TaskAction
    fun action() {
        project.copy { (1)
            from("source")
            into("destination")
        }
    }
}
1 这将被报告为一个问题,因为任务操作在执行时使用了 Project 对象

以下是应该如何完成:

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

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

    @TaskAction
    fun action() {
        fs.copy {
            from("source")
            into("destination")
        }
    }
}
1 不再报告问题,支持注入的 FileSystemOperations 服务作为 project.copy {} 的替代品

同样,如果您在脚本中声明的临时任务遇到相同的问题,如下所示:

KotlinGroovy
build.gradle.kts
tasks.register("someTask") {
    doLast {
        project.copy { (1)
            from("source")
            into("destination")
        }
    }
}
1 这将被报告为一个问题,因为任务操作在执行时使用了 Project 对象

以下是如何修复上面的任务声明:

KotlinGroovy
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")
        }
    }
}
1 服务不能直接在脚本中注入,我们需要一个额外的类型来传达注入点
2 在任务操作之外使用 project.object 创建额外类型的实例
3 不再报告问题,任务操作引用提供 FileSystemOperations 服务的 injected,支持作为 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.properties , project.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.copySpec {} 计算实际参数的结果。

project.copy {}

project.sync {}

project.delete {}

project.mkdir(path)

可用于构建逻辑的 Kotlin、Groovy 或 Java API。

project.exec {}

project.javaexec {}

project.ant {}

project.createAntBuilder()

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

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

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

访问任务扩展或约定

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

使用构建监听器

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

这些应由 构建服务 替换,注册以在需要时接收有关 任务执行 的信息。使用 数据流操作 来处理构建结果而不是 buildFinished 侦听器。

运行外部进程

插件和构建脚本应避免在配置时运行外部进程。通常,最好在具有正确声明的输入和输出的任务中运行外部进程,以避免在任务为最新时进行不必要的工作。如有必要,应仅使用与配置缓存兼容的 API,而不是 Java 和 Groovy 标准 API 或现有的 ExecOperationsProject.execProject.javaexec 以及设置和初始化脚本中的类似 API。对于更简单的情况,当获取进程的输出就足够时,可以使用 providers.exec()providers.javaexec()

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

对于更复杂的情况,可以使用带有注入 ExecOperations 的自定义 ValueSource 实现。这个 ExecOperations 实例可以在配置时不受限制地使用。

KotlinGroovy
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())
    }
}

然后可以使用 ValueSource 实现来创建带有 providers.of 的提供程序:

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

在这两种方法中,如果在配置时使用提供者的值,那么它将成为构建配置输入。每次构建都会执行外部进程以确定配置缓存是否是最新的,因此建议只在配置时调用快速运行的进程。如果该值发生变化,则缓存将失效,并且该过程将作为配置阶段的一部分在此构建期间再次运行。

读取系统属性和环境变量

插件和构建脚本可以在配置时使用标准 Java、Groovy 或 Kotlin API 或使用值供应商 API 直接读取系统属性和环境变量。这样做会使此类变量或属性成为构建配置输入,因此更改值会使配置缓存无效。配置缓存报告包括这些构建配置输入的列表以帮助跟踪它们。

通常,您应该避免在配置时读取系统属性和环境变量的值,以避免值更改时缓存未命中。相反,您可以将 providers.systemProperty()providers.environmentVariable() 返回的 Provider 连接到任务属性。

不鼓励某些可能枚举所有环境变量或系统属性的访问模式(例如,调用 System.getenv().forEach() 或使用其 keySet() 的迭代器)。在这种情况下,Gradle 无法找出哪些属性是实际构建配置输入,因此每个可用属性都成为一个。如果使用此模式,即使添加新属性也会使缓存无效。

使用自定义谓词来过滤环境变量是这种不鼓励的模式的一个例子:

KotlinGroovy
build.gradle.kts
val jdkLocations = System.getenv().filterKeys {
    it.startsWith("JDK_")
}

谓词中的逻辑对配置缓存是不透明的,因此所有环境变量都被视为输入。减少输入数量的一种方法是始终使用查询具体变量名称的方法,例如 getenv(String)getenv().get()

KotlinGroovy
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)
}

但是,上面的固定代码并不完全等同于原始代码,因为仅支持显式变量列表。基于前缀的过滤是一种常见的场景,因此有基于提供者的 API 来访问 系统属性环境变量

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

请注意,配置缓存不仅会在变量值更改或变量被删除时失效,而且在将具有匹配前缀的另一个变量添加到环境中时也会失效。

对于更复杂的用例,可以使用自定义 ValueSource 实现。 ValueSource 代码中引用的系统属性和环境变量不会成为构建配置输入,因此可以应用任何处理。相反,每次构建运行时都会重新计算 ValueSource 的值,并且仅当值更改时配置缓存才会失效。例如,ValueSource 可用于获取名称中包含子字符串 JDK 的所有环境变量:

KotlinGroovy
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.set("JDK")
    }
}

未声明的文件读取

插件和构建脚本不应在配置时使用 Java、Groovy 或 Kotlin API 直接读取文件。相反,使用价值供应商 API 将文件声明为潜在的构建配置输入。

这个问题是由类似这样的构建逻辑引起的:

KotlinGroovy
build.gradle.kts
val config = file("some.conf").readText()

要解决此问题,请改为使用 providers.fileContents() 读取文件:

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

通常,您应该避免在配置时读取文件,以避免在文件内容更改时使配置缓存条目失效。相反,您可以将 providers.fileContents() 返回的 Provider 连接到任务属性。

尚未实现

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

凭证和秘密的处理

配置缓存目前没有选项来防止存储用作输入的秘密,因此它们可能最终出现在序列化的配置缓存条目中,默认情况下,该条目存储在项目目录中的 .gradle/configuration-cache 下。

为了降低意外暴露的风险,Gradle 对配置缓存进行了加密。 Gradle 透明地根据需要生成机器特定的密钥,将其缓存在 GRADLE_USER_HOME 目录下,并使用它来加密项目特定缓存中的数据。

要进一步增强安全性,请确保:

  • 安全访问配置缓存条目;

  • 利用 GRADLE_USER_HOME/gradle.properties 来存储秘密。该文件的内容不是配置缓存的一部分,只是它的指纹。如果您在该文件中存储秘密,则必须小心保护对文件内容的访问。

共享配置缓存

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

源依赖

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

将 Java 代理与使用 TestKit 运行的构建结合使用

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

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

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

Java 对象序列化

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

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

  • writeObject 方法结合 readObject 方法来精确控制要存储的信息;

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

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

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

  • 一个 readResolve 方法,允许类为刚刚读取的对象指定一个替代品;

以下 Java Object Serialization 功能是不是支持的:

  • 实现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 方法(如果存在)永远不会被调用;

在执行时访问构建脚本的顶级方法和变量

在构建脚本中重用逻辑和数据的一种常见方法是将重复位提取到顶级方法和变量中。但是,如果启用了配置缓存,则当前不支持在执行时调用此类方法。

对于用 Groovy 编写的构建脚本,任务失败,因为找不到方法。以下代码段使用 listFiles 任务中的顶级方法:

在启用配置缓存的情况下运行任务会产生以下错误:

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.

为防止任务失败,将引用的顶级方法转换为类中的静态方法:

用 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 在较小的范围内定义变量。

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

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