介绍
配置缓存是一个功能,它通过缓存 配置阶段 的结果并将其用于后续构建来显著提高构建性能。使用配置缓存,Gradle 可以完全跳过配置阶段,只要没有影响构建配置的内容(例如构建脚本)发生更改。Gradle 还对任务执行应用了性能改进。
配置缓存从概念上类似于 构建缓存,但缓存不同的信息。构建缓存负责缓存构建的输出和中间文件,例如任务输出或工件转换输出。配置缓存负责缓存特定任务集的构建配置。换句话说,配置缓存保存配置阶段的输出,而构建缓存保存执行阶段的输出。
此功能目前默认情况下未启用。此功能具有以下限制
|
它是如何工作的?
当启用配置缓存并且您为特定任务集运行 Gradle 时,例如通过运行 gradlew check
,Gradle 会检查是否为请求的任务集提供配置缓存条目。如果可用,Gradle 将使用此条目而不是运行配置阶段。缓存条目包含有关要运行的任务集的信息,以及它们的配置和依赖项信息。
第一次运行特定任务集时,这些任务的配置缓存中将没有条目,因此 Gradle 将照常运行配置阶段
-
运行初始化脚本。
-
运行构建的设置脚本,应用任何请求的设置插件。
-
配置和构建
buildSrc
项目(如果存在)。 -
运行构建的构建脚本,应用任何请求的项目插件。
-
计算请求任务的任务图,运行任何延迟配置操作。
在配置阶段之后,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 构建的根目录中的名为 .gradle/configuration-cache
的目录中。如果您需要使缓存失效,只需删除该目录即可
❯ rm -rf .gradle/configuration-cache
配置缓存条目会定期(最多每 24 小时)检查一次,以查看它们是否仍在使用。如果它们 7 天内未使用,则会被删除。
稳定配置缓存
为了稳定配置缓存,我们在一个功能标志后面实施了一些严格性,因为在早期采用者看来,它过于具有破坏性。
您可以按如下方式启用该功能标志
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 中,可以通过两种方式实现,全局或按运行配置。
要为整个构建启用它,请转到 运行 > 编辑配置…
。这将打开 IntelliJ IDEA 或 Android Studio 对话框以配置运行/调试配置。选择 模板 > Gradle
并将必要的系统属性添加到 VM 选项
字段。
例如,要启用配置缓存并将问题转换为警告,请添加以下内容
-Dorg.gradle.configuration-cache=true -Dorg.gradle.configuration-cache.problems=warn
您也可以选择仅为给定的运行配置启用它。在这种情况下,请保持 模板 > 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 项目文件生成 |
实用程序 |
✓ |
支持的插件 |
⚠ |
部分支持的插件 |
✖ |
不支持的插件 |
社区插件
请参考问题 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 在该属性的值发生变化时再次运行配置阶段。修复方法很简单,只需获取系统属性的提供者并将其连接到任务输入,而无需在配置时读取它。
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 | 我们将系统属性提供者直接连接起来,而无需在配置时读取它。 |
有了这个简单的更改,我们可以运行任意次数的任务,更改系统属性的值,并重用配置缓存。
❯ 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.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
。
谨慎地忽略配置输入,并且仅当它们不影响配置逻辑生成的 task 时才忽略。对这些选项的支持将在将来的版本中删除。
测试您的构建逻辑
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 版本开始启用配置缓存。 如果无法立即做到这一点,使用多次运行插件贡献的所有任务的测试,例如断言 `UP_TO_DATE` 和 `FROM_CACHE` 行为,也是一个不错的策略。 |
要求
为了将任务图的状态捕获到配置缓存中并在以后的构建中重新加载,Gradle 对任务和其他构建逻辑施加了一些要求。每个要求都被视为配置缓存的“问题”,如果存在违规行为,则会导致构建失败。
在大多数情况下,这些要求实际上是在揭示一些未声明的输入。换句话说,使用配置缓存是所有构建更严格、更正确和更可靠的一种选择。
以下部分描述了每个要求以及如何更改构建以解决问题。
任务不允许引用某些类型
任务实例的字段不允许引用许多类型。这同样适用于任务操作,例如 `doFirst {}` 或 `doLast {}` 中的闭包。
这些类型可以分为以下几类:
-
实时 JVM 状态类型
-
Gradle 模型类型
-
依赖管理类型
在所有情况下,这些类型被禁止的原因是它们的狀態无法轻松地由配置缓存存储或重新创建。
实时 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()
获取这样的提供者。同样,如果您引用了一些 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 或现有的 ExecOperations
、Project.exec
、Project.javaexec
以及它们在设置和初始化脚本中的类似项。对于更简单的情况,当获取进程的输出就足够时,可以使用 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())
}
}
然后,可以使用 ValueSource
实现使用 providers.of 创建提供者。
val gitVersionProvider = providers.of(GitVersionValueSource::class) {}
val gitVersion = gitVersionProvider.get()
def gitVersionProvider = providers.of(GitVersionValueSource.class) {}
def gitVersion = gitVersionProvider.get()
在这两种方法中,如果提供者的值在配置时使用,那么它将成为构建配置输入。外部进程将在每次构建时执行,以确定配置缓存是否是最新的,因此建议仅在配置时调用快速运行的进程。如果值发生变化,则缓存将失效,并且该进程将在本次构建期间作为配置阶段的一部分再次运行。
读取系统属性和环境变量
插件和构建脚本可以在配置时使用标准 Java、Groovy 或 Kotlin API 或值提供者 API 直接读取系统属性和环境变量。这样做会使该变量或属性成为构建配置输入,因此更改其值会使配置缓存失效。配置缓存报告包含这些构建配置输入的列表,以帮助跟踪它们。
通常,您应该避免在配置时读取系统属性和环境变量的值,以避免在值更改时发生缓存未命中。相反,您可以将 providers.systemProperty() 或 providers.environmentVariable() 返回的 Provider
连接到任务属性。
一些可能枚举所有环境变量或系统属性的访问模式(例如,调用 System.getenv().forEach()
或使用其 keySet()
的迭代器)是不鼓励的。在这种情况下,Gradle 无法找出哪些属性是实际的构建配置输入,因此所有可用属性都成为构建配置输入。即使添加新属性,如果使用这种模式,也会使缓存失效。
使用自定义谓词来过滤环境变量是这种不鼓励模式的一个例子。
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 以及类加载器或进程隔离来封装库代码。工作程序类路径的字节码不会被修改,因此自检应该通过。当使用进程隔离时,工作程序操作将在没有安装 Gradle Java 代理的单独工作程序进程中执行。
在简单情况下,当库也提供命令行入口点(public static void main()
方法)时,您也可以使用 JavaExec 任务来隔离库。
凭据和密钥的处理
配置缓存目前没有选项可以阻止存储用作输入的密钥,因此它们最终可能会出现在序列化配置缓存条目中,该条目默认存储在项目目录下的 .gradle/configuration-cache
中。
为了减轻意外暴露的风险,Gradle 会对配置缓存进行加密。Gradle 会根据需要透明地生成机器特定的密钥,将其缓存到 GRADLE_USER_HOME
目录下,并使用它来加密项目特定缓存中的数据。
为了进一步增强安全性,请确保
-
安全访问配置缓存条目;
-
利用
GRADLE_USER_HOME/gradle.properties
来存储密钥。该文件的内容不是配置缓存的一部分,只有其指纹是。如果您将密钥存储在该文件中,则必须注意保护对文件内容的访问。
通过 `GRADLE_ENCRYPTION_KEY` 环境变量提供加密密钥
默认情况下,Gradle 会自动生成并管理加密密钥,将其存储为 Java 密钥库,位于 `GRADLE_USER_HOME` 目录下。
对于某些环境(例如,当 `GRADLE_USER_HOME` 目录在多台机器之间共享时),您可能需要为 Gradle 提供确切的加密密钥,以便在读取或写入缓存的配置数据时使用,可以通过 `GRADLE_ENCRYPTION_KEY` 环境变量来提供。
您必须确保在多个 Gradle 运行中始终提供相同的加密密钥,否则 Gradle 将无法重用现有的缓存配置。 |
生成与 GRADLE_ENCRYPTION_KEY 兼容的加密密钥
为了让 Gradle 使用用户指定的加密密钥加密配置缓存,您必须在设置了 `GRADLE_ENCRYPTION_KEY` 环境变量(值为有效的 AES 密钥,以 Base64 字符串编码)的情况下运行 Gradle。
生成 Base64 编码的 AES 兼容密钥的一种方法是使用以下命令:
❯ openssl rand -base64 16
此命令应该在 Linux、Mac OS 或 Windows 上运行,如果使用的是 Cygwin 之类的工具。
然后,您可以使用该命令生成的 Base64 编码密钥,并将其设置为 `GRADLE_ENCRYPTION_KEY` 环境变量的值。
尚未实现
某些 Gradle 功能的配置缓存支持尚未实现。这些功能的支持将在以后的 Gradle 版本中添加。
使用 TestKit 运行构建时使用 Java 代理
当使用 TestKit 运行构建时,配置缓存可能会干扰应用于这些构建的 Java 代理,例如 Jacoco 代理。
细粒度跟踪 Gradle 属性作为构建配置输入
目前,所有 Gradle 属性的外部来源(项目目录中的 gradle.properties
和 GRADLE_USER_HOME
中的 gradle.properties
,设置属性的环境变量和系统属性,以及使用命令行标志指定的属性)都被视为构建配置输入,无论在配置时实际使用了哪些属性。但是,这些来源不包含在配置缓存报告中。
Java 对象序列化
Gradle 允许支持 Java 对象序列化 协议的对象存储在配置缓存中。
当前的实现仅限于实现 java.io.Serializable
接口并定义以下方法组合之一的可序列化类
-
一个
writeObject
方法与一个readObject
方法相结合,以精确控制要存储的信息; -
一个
writeObject
方法,没有相应的readObject
;writeObject
最终必须调用ObjectOutputStream.defaultWriteObject
; -
一个
readObject
方法,没有相应的writeObject
;readObject
最终必须调用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
方法,则永远不会调用它;
在执行时访问构建脚本的顶级方法和变量
在构建脚本中重用逻辑和数据的常见方法是将重复的部分提取到顶层方法和变量中。但是,如果启用了配置缓存,则当前不支持在执行时调用此类方法。
对于用 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 编写的构建脚本根本无法在配置缓存中存储引用顶层方法或变量的任务。此限制存在是因为捕获的脚本对象引用无法序列化。listFiles
任务的 Kotlin 版本的第一次运行会遇到配置缓存问题。
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 | 在更小的范围内定义变量。 |
使用构建服务使配置缓存失效
目前,如果 ValueSource
的值在配置时被访问,则无法使用 BuildServiceProvider
或从中派生的提供者,并将 map
或 flatMap
作为 ValueSource
的参数。当在作为配置阶段的一部分执行的任务中获得此类 ValueSource
时,同样适用,例如 buildSrc
构建的任务或包含的构建贡献插件。请注意,使用 @ServiceReference
或将 BuildServiceProvider
存储在任务的 @Internal
注释属性中是安全的。一般来说,此限制使得无法使用 BuildService
使配置缓存失效。