简介
配置缓存是一项通过缓存配置阶段的结果并在后续构建中重用它来显着提高构建性能的功能。使用配置缓存,当没有影响构建配置的任何内容(例如构建脚本)发生更改时,Gradle 可以完全跳过配置阶段。Gradle 还对任务执行应用了性能改进。
配置缓存在概念上类似于构建缓存,但缓存不同的信息。构建缓存负责缓存构建的输出和中间文件,例如任务输出或工件转换输出。配置缓存负责缓存特定任务集的构建配置。换句话说,配置缓存保存配置阶段的输出,而构建缓存保存执行阶段的输出。
此功能目前默认未启用。此功能具有以下限制
|
它是如何工作的?
当启用配置缓存并且您为特定任务集运行 Gradle 时,例如通过运行 gradlew check
,Gradle 会检查是否有所请求任务集的配置缓存条目可用。如果可用,Gradle 将使用此条目而不是运行配置阶段。缓存条目包含有关要运行的任务集的信息,以及它们的配置和依赖信息。
第一次运行特定任务集时,配置缓存中将没有这些任务的条目,因此 Gradle 将像往常一样运行配置阶段
-
运行 init 脚本。
-
运行构建的 settings 脚本,应用任何请求的 settings 插件。
-
配置和构建
buildSrc
项目(如果存在)。 -
运行构建的 build 脚本,应用任何请求的项目插件。
-
计算请求任务的任务图,运行任何延迟配置操作。
在配置阶段之后,Gradle 将任务图的快照写入新的配置缓存条目,以供以后的 Gradle 调用使用。然后,Gradle 从配置缓存加载任务图,以便它可以对任务应用优化,然后像往常一样运行执行阶段。配置时间仍然会在您第一次运行特定任务集时花费。但是,您应该立即看到构建性能的提高,因为任务将并行运行。
当您随后使用相同的任务集运行 Gradle 时,例如再次运行 gradlew check
,Gradle 将直接从配置缓存加载任务及其配置,并完全跳过配置阶段。在使用配置缓存条目之前,Gradle 会检查条目的任何“构建配置输入”(例如构建脚本)是否已更改。如果构建配置输入已更改,Gradle 将不会使用该条目,并将再次运行配置阶段,如上所述,保存结果以供以后重用。
构建配置输入包括
-
Init 脚本
-
Settings 脚本
-
Build 脚本
-
配置阶段期间使用的系统属性
-
配置阶段期间使用的 Gradle 属性
-
配置阶段期间使用的环境变量
-
使用值提供程序(例如 provider)访问的配置文件
-
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
稳定的配置缓存
为了实现配置缓存的稳定化,我们在一个功能标志后面实施了一些严格性,因为早期采用者认为这具有破坏性。
您可以按如下方式启用该功能标志
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
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 对话框以配置 Run/Debug 配置。选择 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 插件都支持配置缓存。
JVM 语言和框架 |
原生语言 |
打包和分发 |
---|---|---|
代码分析 |
IDE 项目文件生成 |
Utility |
✓ |
支持的插件 |
⚠ |
部分支持的插件 |
✖ |
不支持的插件 |
社区插件
请参考 issue gradle/gradle#13490 以了解社区插件的状态。
故障排除
以下部分将介绍一些关于处理配置缓存问题的一般指南。这适用于您的构建逻辑和您的 Gradle 插件。
在序列化运行任务所需的状态失败后,将生成检测到的问题的 HTML 报告。Gradle 失败输出包含指向该报告的可点击链接。此报告很有用,可让您深入了解问题,了解导致问题的原因。
让我们看一个简单的构建脚本示例,其中包含几个问题
tasks.register("someTask") {
val destination = System.getProperty("someDestination") (1)
inputs.dir("source")
outputs.dir(destination)
doLast {
project.copy { (2)
from("source")
into(destination)
}
}
}
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://docs.gradle.org.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 https://docs.gradle.org.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 https://docs.gradle.org.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 报告中存在的链接,我们可以修复我们的问题。这是构建脚本的固定版本
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"))
}
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 再次运行配置阶段。修复此问题就像获取系统属性的 provider 并将其连接到任务输入一样简单,而无需在配置时读取它。
tasks.register<MyCopyTask>("someTask") {
val projectDir = layout.projectDirectory
source = projectDir.dir("source")
destination = projectDir.dir(providers.systemProperty("someDestination")) (1)
}
tasks.register('someTask', MyCopyTask) {
def projectDir = layout.projectDirectory
source = projectDir.dir('source')
destination = projectDir.dir(providers.systemProperty('someDestination')) (1)
}
1 | 我们直接连接了系统属性 provider,而无需在配置时读取它。 |
有了这个简单的更改,我们可以运行任务任意次数,更改系统属性值,并重用配置缓存
❯ 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 以禁用它,并在需要时传达需要禁用配置缓存的不支持的工作流程。
在构建中对配置缓存做出反应
构建逻辑或插件实现可以检测到是否为给定的构建启用了配置缓存,并做出相应的反应。active 配置缓存的状态在相应的构建功能中提供。您可以通过注入 BuildFeatures
服务在您的代码中访问它。
您可以使用此信息以不同的方式配置插件的功能,或禁用尚未兼容的可选功能。另一个示例包括为您的用户提供额外的指导,如果他们需要调整其设置或被告知临时限制。
采纳配置缓存行为的变更
Gradle 版本发布增强了配置缓存功能,使其能够检测到更多配置逻辑与环境交互的情况。这些变更通过消除潜在的错误缓存命中,提高了缓存的正确性。另一方面,它们也施加了更严格的规则,插件和构建逻辑需要遵循这些规则,以尽可能频繁地被缓存。
如果某些配置输入的结果不影响已配置的任务,则它们可能被认为是“良性的”。由于这些输入而导致新的配置未命中对于构建用户来说可能是不希望看到的,而消除它们的建议策略是
-
借助配置缓存报告,识别导致配置缓存失效的配置输入。
-
修复项目构建逻辑访问的未声明的配置输入。
-
向插件维护者报告由第三方插件引起的问题,并在插件修复后更新插件。
-
-
对于某些类型的配置输入,可以使用选择退出选项,使 Gradle 回退到较早的行为,从而忽略这些输入的检测。这种临时性的权宜之计旨在缓解过时的插件带来的性能问题。
在以下情况下,可以暂时选择退出配置输入检测
-
自 Gradle 8.1 起,使用许多与文件系统相关的 API 会被正确地跟踪为配置输入,包括文件系统检查,例如
File.exists()
或File.isFile()
。为了使输入跟踪忽略对特定路径的文件系统检查,可以使用 Gradle 属性
org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks
,其中包含路径列表,路径相对于根项目目录并用;
分隔。要忽略多个路径,可以使用*
匹配一个段内的任意字符串,或使用**
匹配跨段的任意字符串。以~/
开头的路径基于用户主目录。例如gradle.propertiesorg.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,或者使用启用配置缓存中描述的其他方法之一。
你需要运行你的任务两次。一次用于预热配置缓存。一次用于重用配置缓存。
@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
}
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 版本开始启用配置缓存。 如果不能立即完成,使用运行插件贡献的所有任务多次的测试,例如,断言 |
要求
为了将任务图的状态捕获到配置缓存并在以后的构建中重新加载它,Gradle 对任务和其他构建逻辑应用了某些要求。如果存在违反情况,则每个要求都被视为配置缓存“问题”并使构建失败。
在大多数情况下,这些要求实际上是在揭示一些未声明的输入。换句话说,使用配置缓存是对所有构建的更严格、更正确和更可靠性的选择加入。
以下各节描述了每个要求以及如何更改你的构建以解决问题。
任务不得引用的某些类型
任务实例不得从其字段中引用许多类型。这同样适用于任务动作,如闭包 doFirst {}
或 doLast {}
。
这些类型分为以下几类
-
Live JVM 状态类型
-
Gradle 模型类型
-
依赖管理类型
在所有情况下,禁止使用这些类型的原因是它们的状态不容易被配置缓存存储或重新创建。
Live JVM 状态类型(例如 ClassLoader
、Thread
、OutputStream
、Socket
等)是被明确禁止的。这些类型几乎从不代表任务输入或输出。唯一的例外是标准流:System.in
、System.out
和 System.err
。这些流可以用作参数,例如,用于 Exec
和 JavaExec
任务。
Gradle 模型类型(例如 Gradle
、Settings
、Project
、SourceSet
、Configuration
等)通常用于携带一些任务输入,这些输入应该被显式且精确地声明。
例如,如果你引用 Project
以在执行时获取 project.version
,你应该直接将项目版本声明为任务的输入,使用 Property<String>
。另一个例子是引用 SourceSet
以稍后获取源文件、编译类路径或源集的输出。你应该将这些声明为 FileCollection
输入,并仅引用它。
相同的要求适用于依赖管理类型,但有一些细微差别。
某些类型,例如 Configuration
或 SourceDirectorySet
,不能作为好的任务输入参数,因为它们包含许多不相关的状态,最好将这些输入建模为更精确的东西。我们不打算使这些类型可序列化。例如,如果你引用 Configuration
以稍后获取已解析的文件,你应该将 FileCollection
声明为任务的输入。同样,如果你引用 SourceDirectorySet
,你应该将 FileTree
声明为任务的输入。
引用依赖解析结果也是不允许的(例如 ArtifactResolutionQuery
、ResolvedArtifact
、ArtifactResult
等)。例如,如果你引用一些 ResolvedComponentResult
实例,你应该将 Provider<ResolvedComponentResult>
声明为任务的输入。可以通过调用 ResolutionResult.getRootComponent()
来获得这样的 provider。同样,如果你引用一些 ResolvedArtifactResult
实例,你应该使用 ArtifactCollection.getResolvedArtifacts()
,它返回一个 Provider<Set<ResolvedArtifactResult>>
,可以将其映射为任务的输入。经验法则是任务不得引用已解析的结果,而是懒加载的规范,以便在执行时进行依赖解析。
某些类型,例如 Publication
或 Dependency
是不可序列化的,但可能是可以序列化的。如果需要,我们可能会允许直接将这些类型用作任务输入。
这是一个引用 SourceSet
的有问题任务类型的示例
abstract class SomeTask : DefaultTask() {
@get:Input lateinit var sourceSet: SourceSet (1)
@TaskAction
fun action() {
val classpathFiles = sourceSet.compileClasspath.files
// ...
}
}
abstract class SomeTask extends DefaultTask {
@Input SourceSet sourceSet (1)
@TaskAction
void action() {
def classpathFiles = sourceSet.compileClasspath.files
// ...
}
}
1 | 这将报告为一个问题,因为不允许引用 SourceSet |
以下是应该如何做的替代方法
abstract class SomeTask : DefaultTask() {
@get:InputFiles @get:Classpath
abstract val classpath: ConfigurableFileCollection (1)
@TaskAction
fun action() {
val classpathFiles = classpath.files
// ...
}
}
abstract class SomeTask extends DefaultTask {
@InputFiles @Classpath
abstract ConfigurableFileCollection getClasspath() (1)
@TaskAction
void action() {
def classpathFiles = classpath.files
// ...
}
}
1 | 不再报告问题,我们现在引用受支持的类型 FileCollection |
同样,如果你在脚本中声明的临时任务中遇到同样的问题,如下所示
tasks.register("someTask") {
doLast {
val classpathFiles = sourceSets.main.get().compileClasspath.files (1)
}
}
tasks.register('someTask') {
doLast {
def classpathFiles = sourceSets.main.compileClasspath.files (1)
}
}
1 | 这将报告为一个问题,因为 doLast {} 闭包正在捕获对 SourceSet 的引用 |
你仍然需要满足相同的要求,即不引用不允许的类型。以下是如何修复上述任务声明的方法
tasks.register("someTask") {
val classpath = sourceSets.main.get().compileClasspath (1)
doLast {
val classpathFiles = classpath.files
}
}
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
,你应该使用 Task.logger
而不是 Project.logger
。
否则,你可以使用注入的服务而不是 Project
的方法。
这是一个在执行时使用 Project
对象的问题任务类型的示例
abstract class SomeTask : DefaultTask() {
@TaskAction
fun action() {
project.copy { (1)
from("source")
into("destination")
}
}
}
abstract class SomeTask extends DefaultTask {
@TaskAction
void action() {
project.copy { (1)
from 'source'
into 'destination'
}
}
}
1 | 这将报告为一个问题,因为任务动作在执行时正在使用 Project 对象 |
以下是应该如何做的替代方法
abstract class SomeTask : DefaultTask() {
@get:Inject abstract val fs: FileSystemOperations (1)
@TaskAction
fun action() {
fs.copy {
from("source")
into("destination")
}
}
}
abstract class SomeTask extends DefaultTask {
@Inject abstract FileSystemOperations getFs() (1)
@TaskAction
void action() {
fs.copy {
from 'source'
into 'destination'
}
}
}
1 | 不再报告问题,注入的 FileSystemOperations 服务被支持作为 project.copy {} 的替代方案 |
同样,如果你在脚本中声明的临时任务中遇到同样的问题,如下所示
tasks.register("someTask") {
doLast {
project.copy { (1)
from("source")
into("destination")
}
}
}
tasks.register('someTask') {
doLast {
project.copy { (1)
from 'source'
into 'destination'
}
}
}
1 | 这将报告为一个问题,因为任务动作在执行时正在使用 Project 对象 |
以下是如何修复上述任务声明的方法
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")
}
}
}
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
方法。
代替 | 使用 |
---|---|
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
|
|
|
|
|
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
|
|
|
|
|
|
|
|
|
|
任务输入或输出属性或脚本变量,用于捕获使用 |
|
|
|
|
|
|
|
|
|
Kotlin、Groovy 或 Java API,可用于你的构建逻辑。 |
|
|
|
|
|
|
|
共享可变对象
当将任务存储到配置缓存时,通过任务字段直接或间接引用的所有对象都会被序列化。在大多数情况下,反序列化会保留引用相等性:如果在配置时两个字段 a
和 b
引用相同的实例,那么在反序列化后,它们将再次引用相同的实例,因此 a == b
(或 Groovy 和 Kotlin 语法中的 a === b
)仍然成立。但是,出于性能原因,某些类,特别是 java.lang.String
、java.io.File
和许多 java.util.Collection
接口的实现,在序列化时不会保留引用相等性。在反序列化时,引用此类对象的字段可以引用不同但相等的对象。
让我们看一下一个在任务字段中存储用户定义的对象和一个 ArrayList
的任务。
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)
}
}
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 定义类型的引用相等性得到保留。
请注意,任务之间不保留引用相等性:每个任务都是其自己的“领域”,因此不可能在任务之间共享对象。相反,你可以使用构建服务来包装共享状态。
访问任务扩展或约定
任务不应在执行时访问约定和扩展,包括额外的属性。相反,与任务执行相关的任何值都应建模为任务属性。
使用构建监听器
插件和构建脚本不得注册任何构建监听器。即在配置时注册并在执行时收到通知的监听器。例如 BuildListener
或 TaskExecutionListener
。
运行外部进程
插件和构建脚本应避免在配置时运行外部进程。一般来说,最好在具有正确声明的输入和输出的任务中运行外部进程,以避免在任务是最新的时进行不必要的工作。但是,如果需要,你应该仅使用下面描述的与配置缓存兼容的 API,而不是 Java 和 Groovy 标准 API,或 Gradle 提供的 Project.exec
、Project.javaexec
、ExecOperations.exec
和 ExecOperations.javaexec
方法。这些方法的灵活性使 Gradle 无法确定调用如何影响构建配置,从而难以确保可以安全地重用配置缓存条目。
对于更简单的情况,当只需要获取进程的输出时,可以使用 providers.exec() 和 providers.javaexec()
val gitVersion = providers.exec {
commandLine("git", "--version")
}.standardOutput.asText.get()
def gitVersion = providers.exec {
commandLine("git", "--version")
}.standardOutput.asText.get()
对于更复杂的情况,可以使用具有注入的 ExecOperations
的自定义 ValueSource 实现。此 ExecOperations
实例可以在配置时不受限制地使用。
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())
}
}
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())
}
}
你还可以使用标准的 Java/Kotlin/Groovy 进程 API,如 java.lang.ProcessBuilder
在 ValueSource
中。
然后可以使用 providers.of 使用 ValueSource
实现创建一个 provider
val gitVersionProvider = providers.of(GitVersionValueSource::class) {}
val gitVersion = gitVersionProvider.get()
def gitVersionProvider = providers.of(GitVersionValueSource.class) {}
def gitVersion = gitVersionProvider.get()
在这两种方法中,如果在配置时使用 provider 的值,它将成为构建配置输入。外部进程将在每次构建时执行,以确定配置缓存是否是最新的,因此建议仅在配置时调用快速运行的进程。如果值发生更改,则缓存将失效,并且在此构建期间,该进程将作为配置阶段的一部分再次运行。
读取系统属性和环境变量
插件和构建脚本可以在配置时使用标准的 Java、Groovy 或 Kotlin API 或值提供器 API 直接读取系统属性和环境变量。这样做会使此类变量或属性成为构建配置输入,因此更改值会使配置缓存失效。配置缓存报告包含这些构建配置输入的列表,以帮助跟踪它们。
一般来说,你应该避免在配置时读取系统属性和环境变量的值,以避免在值更改时出现缓存未命中。相反,你可以将 providers.systemProperty() 或 providers.environmentVariable() 返回的 Provider
连接到任务属性。
某些可能枚举所有环境变量或系统属性的访问模式(例如,调用 System.getenv().forEach()
或使用其 keySet()
的迭代器)是不鼓励的。在这种情况下,Gradle 无法找出哪些属性是实际的构建配置输入,因此每个可用的属性都变成一个输入。即使添加新属性,如果使用此模式,也会使缓存失效。
使用自定义谓词过滤环境变量是这种不鼓励的模式的一个示例
val jdkLocations = System.getenv().filterKeys {
it.startsWith("JDK_")
}
def jdkLocations = System.getenv().findAll {
key, _ -> key.startsWith("JDK_")
}
谓词中的逻辑对于配置缓存是不透明的,因此所有环境变量都被视为输入。减少输入数量的一种方法是始终使用查询具体变量名称的方法,例如 getenv(String)
或 getenv().get()
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)
}
def jdkVariables = ["JDK_8", "JDK_11", "JDK_17"]
def jdkLocations = jdkVariables.findAll { v ->
System.getenv(v) != null
}.collectEntries { v ->
[v, System.getenv(v)]
}
val jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")
def jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")
请注意,配置缓存不仅在变量的值更改或变量被删除时失效,而且在另一个具有匹配前缀的变量添加到环境时也会失效。
对于更复杂的使用情况,可以使用自定义 ValueSource 实现。在 ValueSource
代码中引用的系统属性和环境变量不会成为构建配置输入,因此可以应用任何处理。相反,每次构建运行时都会重新计算 ValueSource
的值,并且仅当值发生更改时,配置缓存才会失效。例如,ValueSource
可用于获取名称包含子字符串 JDK
的所有环境变量
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"
}
}
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 将文件声明为潜在的构建配置输入。
此问题是由类似于以下的构建逻辑引起的
val config = file("some.conf").readText()
def config = file('some.conf').text
要解决此问题,请使用 providers.fileContents() 读取文件
val config = providers.fileContents(layout.projectDirectory.file("some.conf"))
.asText
def config = providers.fileContents(layout.projectDirectory.file('some.conf'))
.asText
一般来说,你应该避免在配置时读取文件,以避免在文件内容更改时使配置缓存条目失效。相反,你可以将 providers.fileContents() 返回的 Provider
连接到任务属性。
字节码修改和 Java 代理
为了检测配置输入,Gradle 修改了构建脚本类路径上类的字节码,如插件及其依赖项。Gradle 使用 Java 代理来修改字节码。由于更改的字节码或代理的存在,某些库的完整性自检可能会失败。
为了解决这个问题,你可以使用Worker API,并使用类加载器或进程隔离来封装库代码。worker 类路径的字节码不会被修改,因此自检应该通过。当使用进程隔离时,worker 动作在一个单独的 worker 进程中执行,该进程没有安装 Gradle Java 代理。
在简单的情况下,当库也提供命令行入口点(public static void main()
方法)时,你也可以使用 JavaExec 任务来隔离库。
凭据和密钥的处理
配置缓存目前没有阻止存储用作输入的密钥的选项,因此它们可能会最终出现在序列化的配置缓存条目中,默认情况下,该条目存储在项目目录下的 .gradle/configuration-cache
中。
为了降低意外泄露的风险,Gradle 对配置缓存进行加密。Gradle 透明地生成特定于机器的密钥(如果需要),将其缓存在 GRADLE_USER_HOME
目录下,并使用它来加密项目特定缓存中的数据。
为了进一步增强安全性,请确保
-
安全访问配置缓存条目;
-
利用
GRADLE_USER_HOME/gradle.properties
存储密钥。该文件的内容不是配置缓存的一部分,只有其指纹。如果将密钥存储在该文件中,则必须注意保护对文件内容的访问。
通过 GRADLE_ENCRYPTION_KEY
环境变量提供加密密钥
默认情况下,Gradle 自动生成和管理加密密钥,作为存储在 GRADLE_USER_HOME
目录下的 Java 密钥库。
对于不希望这样做的环境(例如,当 GRADLE_USER_HOME
目录在多台机器之间共享时),你可以通过 GRADLE_ENCRYPTION_KEY
环境变量向 Gradle 提供在读取或写入缓存的配置数据时要使用的确切加密密钥。
你必须确保在多次 Gradle 运行中始终如一地提供相同的加密密钥,否则 Gradle 将无法重用现有的缓存配置。 |
生成与 GRADLE_ENCRYPTION_KEY 兼容的加密密钥
为了使 Gradle 使用用户指定的加密密钥加密配置缓存,你必须在设置了 GRADLE_ENCRYPTION_KEY 环境变量(其中包含有效的 AES 密钥,编码为 Base64 字符串)的情况下运行 Gradle。
生成 Base64 编码的 AES 兼容密钥的一种方法是使用如下命令
❯ openssl rand -base64 16
如果使用 Cygwin 之类的工具,则此命令应在 Linux、Mac OS 或 Windows 上工作。
然后,你可以使用该命令生成的 Base64 编码密钥,并将其设置为 GRADLE_ENCRYPTION_KEY
环境变量的值。
尚未实现
尚不支持将配置缓存与某些 Gradle 功能一起使用。对这些功能的支持将在以后的 Gradle 版本中添加。
在使用 TestKit 运行的构建中使用 Java agent
当使用 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
方法结合使用,以精确控制要存储的信息; -
一个
writeObject
方法,没有对应的readObject
;writeObject
必须最终调用ObjectOutputStream.defaultWriteObject
; -
一个
readObject
方法,没有对应的writeObject
;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
任务中使用了顶层方法
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.
要防止任务失败,请将引用的顶层方法转换为类中的静态方法
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
任务会因配置缓存问题而失败。
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 版本与配置缓存兼容,请进行以下更改
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
或从中派生的提供程序与 map
或 flatMap
一起用作 ValueSource
的参数。当在作为配置阶段一部分执行的任务中获得这样的 ValueSource
时,情况也是如此,例如 buildSrc
构建或包含构建贡献插件的任务。请注意,使用 @ServiceReference
或将 BuildServiceProvider
存储在任务的 @Internal
注解属性中是安全的。一般来说,此限制使得无法使用 BuildService
来使配置缓存失效。
请参阅 gradle/gradle#24085。