介绍
配置缓存是一项通过缓存配置阶段的结果并在后续构建中重用它来显着提高构建性能的功能。使用配置缓存,当没有任何影响构建配置(例如构建脚本)发生更改时,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
稳定的配置缓存
为了稳定配置缓存,当我们认为功能标志对于早期采用者来说太具有破坏性时,我们在功能标志后面实施了一些严格的要求。
您可以按如下方式启用该功能标志:
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 对话框来配置运行/调试配置。选择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项目文件生成 |
公用事业 |
✓ |
支持的插件 |
⚠ |
部分支持的插件 |
✖ |
不支持的插件 |
社区插件
请参阅问题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 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 报告中的链接,我们可以解决问题。这是构建脚本的固定版本:
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
。
谨慎地忽略配置输入,并且仅当它们不影响配置逻辑生成的任务时才忽略。未来版本中将删除对这些选项的支持。
测试您的构建逻辑
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 {}
。
这些类型分为以下几类:
-
实时 JVM 状态类型
-
Gradle 模型类型
-
依赖管理类型
在所有情况下,不允许使用这些类型的原因是配置缓存无法轻松存储或重新创建它们的状态。
根本不允许使用实时 JVM 状态类型(例如ClassLoader
、Thread
、OutputStream
等...)。Socket
这些类型几乎从不代表任务输入或输出。唯一的例外是标准流:System.in
、System.out
和System.err
。例如,这些流可以用作任务的Exec
参数JavaExec
。
Gradle 模型类型(例如Gradle
、Settings
、Project
、SourceSet
等Configuration
...)通常用于携带一些应显式且精确声明的任务输入。
例如,如果您引用 aProject
来获取project.version
执行时的 ,您应该使用 a 直接将项目版本声明为任务的输入Property<String>
。另一个例子是引用 aSourceSet
以便稍后获取源文件、编译类路径或源集的输出。相反,您应该将它们声明为FileCollection
输入并引用它。
同样的要求也适用于依赖管理类型,但有一些细微差别。
某些类型(例如Configuration
或SourceDirectorySet
)不能构成良好的任务输入参数,因为它们包含许多不相关的状态,最好将这些输入建模为更精确的东西。我们根本不打算使这些类型可序列化。例如,如果您引用 aConfiguration
以便稍后获取已解析的文件,则您应该将 a 声明FileCollection
为任务的输入。同样,如果您引用 a,SourceDirectorySet
则应该将 a 声明FileTree
为任务的输入。
也不允许引用依赖关系解析结果(例如ArtifactResolutionQuery
,ResolvedArtifact
等等ArtifactResult
......)。例如,如果您引用某些ResolvedComponentResult
实例,则应该将 a 声明Provider<ResolvedComponentResult>
为任务的输入。这样的提供者可以通过调用来获得ResolutionResult.getRootComponent()
。同样,如果您引用某些ResolvedArtifactResult
实例,则应该使用ArtifactCollection.getResolvedArtifacts()
它返回Provider<Set<ResolvedArtifactResult>>
可以映射为任务的输入的值。经验法则是任务不得引用已解析的结果,而是引用惰性规范,以便在执行时进行依赖项解析。
某些类型(例如Publication
或 )Dependency
不可序列化,但可以。如有必要,我们可以允许它们直接用作任务输入。
以下是引用 a 的有问题的任务类型的示例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
您的任务操作中需要 a ,则应使用 ,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
(或在 Groovy 和 Kotlin 语法中)仍然成立。然而,出于性能原因,某些类(特别是、和接口的许多实现)在不保留引用相等性的情况下进行序列化。反序列化时,引用此类对象的字段可以引用不同但相同的对象。b
a == b
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 定义的类型保留引用相等性。
请注意,任务之间不保留引用相等性:每个任务都是其自己的“领域”,因此不可能在任务之间共享对象。相反,您可以使用构建服务来包装共享状态。
访问任务扩展或约定
任务不应在执行时访问约定和扩展,包括额外的属性。相反,与任务执行相关的任何值都应建模为任务属性。
使用构建监听器
插件和构建脚本不得注册任何构建侦听器。即在配置时注册的侦听器在执行时收到通知。例如 aBuildListener
或 a 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()
对于更复杂的情况,可以使用注入的自定义ValueSourceExecOperations
实现。该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()
或使用 it 的迭代器)。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
每次构建运行时都会重新计算的值,并且仅当该值更改时配置缓存才会失效。例如, aValueSource
可用于获取名称包含子字符串 的所有环境变量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 自动生成并管理加密密钥,作为存储在该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 版本中添加。
使用 Java 代理和使用 TestKit 运行的构建
使用TestKit运行构建时,配置缓存可能会干扰应用于这些构建的 Java 代理,例如 Jacoco 代理。
作为构建配置输入的 Gradle 属性的细粒度跟踪
目前,Gradle 属性的所有外部源(gradle.properties
在项目目录中以及设置属性的环境变量和系统属性中GRADLE_USER_HOME
,以及使用命令行标志指定的属性)都被视为构建配置输入,无论配置时实际使用哪些属性。但是,这些源不包含在配置缓存报告中。
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 编写的构建脚本根本无法将执行时引用顶级方法或变量的任务存储在配置缓存中。存在此限制是因为捕获的脚本对象引用无法序列化。 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 | 将变量定义在较小的范围内。 |
使用构建服务使配置缓存失效
目前,如果在配置时访问的值,则不可能将BuildServiceProvider
派生自它的 或 提供程序与map
或flatMap
作为 的参数一起使用。当在作为配置阶段的一部分执行的任务中获得这样的 a 时,例如构建或包含的构建贡献插件的任务,同样适用。请注意,使用或存储在任务的带注释的属性中是安全的。一般来说,这个限制使得无法使用a来使配置缓存失效。ValueSource
ValueSource
ValueSource
buildSrc
@ServiceReference
BuildServiceProvider
@Internal
BuildService