配置缓存
配置缓存通过缓存配置阶段的结果并在后续构建中重用,从而提高构建性能。
启用后,如果影响构建配置(例如构建脚本)的内容没有改变,配置缓存允许 Gradle 完全跳过配置阶段。此外,Gradle 还对任务执行应用了性能优化。
配置缓存类似于构建缓存,但它们存储不同类型的数据
-
构建缓存:存储构建的输出和中间文件(例如,任务输出、制品转换输出)。
-
配置缓存:存储特定任务集的构建配置,捕获配置阶段的输出。

此功能默认未启用,并具有以下限制
|
配置缓存的工作原理
当配置缓存启用并运行特定任务集(例如 gradlew check
)时,Gradle 会检查是否存在配置缓存条目。如果存在,Gradle 会使用它跳过配置阶段。
缓存条目包含
-
要运行的任务集
-
它们的配置详情
-
依赖信息
首次执行

首次运行一组任务时,没有缓存条目。Gradle 照常执行配置阶段
-
运行初始化脚本。
-
运行 settings 脚本,应用任何请求的 settings 插件。
-
配置和构建
buildSrc
项目(如果存在)。 -
运行构建脚本,应用任何请求的项目插件。如果插件来自包含的构建,Gradle 会先构建它们。
-
计算任务图,执行延迟配置操作。
然后,Gradle 将任务图的快照存储在配置缓存中,供将来使用。之后,Gradle 从缓存中加载任务图并继续执行任务。
后续运行

对相同任务的后续执行(例如,再次运行 gradlew check
),Gradle 会
-
完全跳过配置阶段。
-
转而从配置缓存加载任务图。
在使用缓存之前,Gradle 会验证没有构建配置输入发生更改。如果任何输入发生更改,Gradle 会重新运行配置阶段并更新缓存。
构建配置输入
以下元素决定配置缓存条目是否有效
-
Gradle 环境
-
GRADLE_USER_HOME
-
Gradle Daemon JVM
-
-
初始化脚本
-
buildSrc
和包含的构建逻辑构建内容(构建脚本、源文件和中间构建输出) -
构建脚本和 Settings 脚本,包括包含的脚本(
apply from: foo.gradle
) -
Gradle 配置文件(版本目录、依赖验证文件、依赖锁定文件、
gradle.properties
文件) -
配置时读取的文件内容
-
配置时检查的文件系统状态(文件存在、目录内容等)
-
配置时获得的自定义
ValueSource
值(这也包括内置的提供者,例如providers.exec
和providers.fileContents
)。 -
配置阶段使用的系统属性
-
配置阶段使用的环境变量
序列化
Gradle 使用优化的序列化机制来存储配置缓存条目。它会自动序列化包含简单状态或支持类型的对象图。
虽然配置缓存序列化不依赖于 Java 序列化,但它理解它的一些特性。这可用于自定义序列化行为,但这会带来性能损失,应尽量避免。
性能改进
除了跳过配置阶段,配置缓存还通过以下方式提高性能
-
并行任务执行:默认情况下,所有任务并行运行,但受依赖约束。
-
缓存依赖解析:依赖解析结果被存储并重用。
-
优化内存使用:将任务图写入缓存后,Gradle 会从内存中丢弃配置和依赖解析状态,从而降低峰值堆内存使用。

尝试配置缓存
建议从最简单的任务调用开始。
启用配置缓存运行 help
是一个很好的第一步
❯ ./gradlew --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.
首次运行此命令时,将执行配置阶段,计算任务图。
然后,再次运行相同的命令。
这会重用缓存的配置
❯ ./gradlew --configuration-cache help
Reusing configuration cache.
...
BUILD SUCCESSFUL in 500ms
1 actionable task: 1 executed
Configuration cache entry reused.
如果它在你的构建中成功了,恭喜你,现在你可以尝试更有用的任务了。你应该针对你的开发循环。一个很好的例子是在进行增量更改后运行测试。
如果在缓存或重用配置时发现任何问题,会生成 HTML 报告,帮助你诊断和解决问题。报告还会显示配置阶段读取的检测到的构建配置输入,例如系统属性、环境变量和值提供者。
请参阅下面的故障排除部分了解更多信息。
启用配置缓存
默认情况下,Gradle 不使用配置缓存。
要在构建时启用它,请使用 configuration-cache
标志
❯ ./gradlew --configuration-cache
要持久启用缓存,请在 gradle.properties
中设置 org.gradle.configuration-cache
属性
org.gradle.configuration-cache=true
如果在 gradle.properties
中启用,你可以在构建时使用 no-configuration-cache
标志覆盖并禁用缓存
❯ ./gradlew --no-configuration-cache
忽略配置缓存问题
默认情况下,如果发生配置缓存问题,Gradle 会使构建失败。但是,在逐步更新插件或构建逻辑以支持配置缓存时,暂时将问题转换为警告会很有用。
这不保证构建会成功。 |
要在构建时更改此行为,请使用以下标志
❯ ./gradlew --configuration-cache-problems=warn
或者,在 gradle.properties
中配置它
org.gradle.configuration-cache.problems=warn
允许的最大问题数
当配置缓存问题被视为警告时,如果默认发现 512
个问题,Gradle 将会使构建失败。
你可以通过在命令行上指定允许的最大问题数来调整此限制
❯ ./gradlew -Dorg.gradle.configuration-cache.max-problems=5
或者在 gradle.properties
文件中配置它
org.gradle.configuration-cache.max-problems=5
启用并行配置缓存
默认情况下,配置缓存的存储和加载是顺序进行的。启用并行存储和加载可以提高性能,但并非所有构建都兼容此功能。
要在构建时启用并行配置缓存,请使用
❯ ./gradlew -Dorg.gradle.configuration-cache.parallel=true
或者在 gradle.properties
文件中持久配置
org.gradle.configuration-cache.parallel=true
为配置缓存稳定性做准备
为确保在 Gradle 稳定配置缓存时的顺利过渡,已在特性标志后实现了一种严格模式。
你可以在你的构建中如下启用特性标志
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
enableFeaturePreview "STABLE_CONFIGURATION_CACHE"
STABLE_CONFIGURATION_CACHE
特性标志强制执行更严格的验证并引入以下行为
- 未声明的共享构建服务使用
-
使用共享构建服务但未通过
Task.usesService
方法明确声明要求的任务将触发弃用警告。
此外,即使配置缓存未启用,如果此特性标志存在,也将强制执行以下配置缓存要求的弃用规则
建议尽快启用此标志,为最终的默认强制执行做准备。
IDE 支持
如果你在 gradle.properties
文件中启用和配置了配置缓存,当你的 IDE 将构建委托给 Gradle 时,它将自动启用。无需额外设置。
由于 gradle.properties
通常被纳入版本控制,以这种方式启用配置缓存将适用于你的整个团队。如果你只希望在本地环境中启用它,可以直接在你的 IDE 中配置。
在 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 arguments
。
结合使用这些方法,你可以全局启用配置缓存,同时对某些运行配置禁用它,反之亦然。
支持的插件
配置缓存对插件实现引入了新的要求。因此,核心 Gradle 插件和社区插件都需要进行调整以确保兼容性。
本节提供有关当前支持的详细信息
核心 Gradle 插件
当前大多数核心 Gradle 插件支持配置缓存
JVM 语言和框架 |
本机语言 |
打包和分发 |
---|---|---|
代码分析 |
IDE 项目文件生成 |
工具 |
✓ |
支持的插件 |
⚠ |
部分支持的插件 |
✖ |
不支持的插件 |
社区插件
请参阅 issue gradle/gradle#13490 以了解社区插件的状态。
故障排除
本节提供了解决配置缓存问题的通用指南,无论问题出现在你的构建逻辑还是 Gradle 插件中。
当 Gradle 在序列化执行任务所需的状态时遇到问题,它会生成一个 HTML 报告,详细说明检测到的问题。控制台输出包含指向此报告的可点击链接,方便你调查根本原因。
考虑以下构建脚本,它包含两个问题
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
}
}
}
运行任务失败并出现以下输出
❯ ./gradlew --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.
由于检测到问题,Gradle 会丢弃配置缓存条目,防止在未来构建中重用。
链接的 HTML 报告提供了有关检测到的问题的详细信息

报告以两种方式呈现问题
-
按消息分组 → 快速识别重复出现的问题类型。
-
按任务分组 → 识别哪些任务导致问题。
展开问题树有助于在对象图中定位根本原因。
此外,报告列出了检测到的构建配置输入,例如配置期间访问的系统属性、环境变量和值提供者

修改构建或插件时,考虑使用 TestKit 测试你的构建逻辑来验证更改。 |
在此阶段,你可以选择忽略问题(将它们转换为警告)以继续探索配置缓存行为,或者立即修复问题。
要在观察问题的同时继续使用配置缓存,请运行
❯ ./gradlew --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.
❯ ./gradlew --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.
Gradle 将成功存储和重用配置缓存,同时继续报告问题。
报告和控制台日志提供了有关解决检测到的问题的指导链接。
以下是示例构建脚本的更正版本
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 | 我们将临时任务转换为 proper 任务类, |
2 | 并声明了输入和输出, |
3 | 并注入了 FileSystemOperations 服务,这是 project.copy {} 的一个受支持的替代方案。 |
修复这些问题后,再次运行任务成功重用了配置缓存
❯ ./gradlew --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.
❯ ./gradlew --configuration-cache someTask -DsomeDestination=dest
Reusing configuration cache.
> Task :someTask
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Configuration Cache entry reused.
如果构建输入发生更改(例如,系统属性值),配置缓存条目将失效,需要新的配置阶段
❯ ./gradlew --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,而没有在配置时读取它。 |
现在,即使更改系统属性,缓存条目仍然可重用
❯ ./gradlew --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. ❯ ./gradlew --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()
方法明确将任务标记为与配置缓存不兼容
tasks.register("resolveAndLockAll") {
notCompatibleWithConfigurationCache("Filters configurations at execution time")
doFirst {
require(gradle.startParameter.isWriteDependencyLocks) { "$path must be run from the command line with the `--write-locks` flag" }
}
doLast {
configurations.filter {
// Add any custom filtering on the configurations to be resolved
it.isCanBeResolved
}.forEach { it.resolve() }
}
}
tasks.register('resolveAndLockAll') {
notCompatibleWithConfigurationCache("Filters configurations at execution time")
doFirst {
assert gradle.startParameter.writeDependencyLocks : "$path must be run from the command line with the `--write-locks` flag"
}
doLast {
configurations.findAll {
// Add any custom filtering on the configurations to be resolved
it.canBeResolved
}.each { it.resolve() }
}
}
当一个任务被标记为不兼容时
-
配置缓存问题在该任务中将不再导致构建失败。
-
如果执行了不兼容的任务,Gradle 会在构建结束时丢弃配置状态。
此机制在迁移期间非常有用,允许你暂时选择退出需要进行更广泛更改才能与配置缓存兼容的任务。
有关更多详细信息,请参阅方法文档。
使用完整性检查调试存储和加载问题
为了减小条目大小并提高性能,Gradle 在写入和读取数据时执行最少的完整性检查。但是,这种方法会使故障排除更加困难,尤其是在处理并发问题或序列化错误时。错误存储的对象可能不会立即被检测到,但在稍后读取缓存数据时可能导致误导性或归因错误。
为了使调试更容易,Gradle 提供了一个选项来启用更严格的完整性检查。此设置有助于更早地识别不一致性,但可能会减慢缓存操作并显着增加缓存条目大小。要启用更严格的完整性检查,请将以下行添加到你的 gradle.properties
文件中
org.gradle.configuration-cache.integrity-check=true
例如,让我们看一个错误实现自定义序列化协议的类型
public class User implements Serializable {
private transient String name;
private transient int age;
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(name); (1)
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.name = (String) in.readObject();
// this.age = in.readInt(); (2)
}
// ...
1 | writeObject 序列化两个字段。 |
2 | readObject 只读取第一个字段,将剩余数据留在流中。 |
这种类型在用作任务状态的一部分时会引起问题,因为配置缓存会尝试将对象中剩余未读取的部分解释为某个新值
public abstract class GreetTask extends DefaultTask {
User user = new User("John", 23);
@TaskAction
public void greet() {
System.out.println("Hello, " + user.getName() + "!");
System.out.println("Have a wonderful " + (user.getAge() + 1) +"th birthday!");
}
}
在没有完整性检查的情况下运行时,你可能会遇到隐晦的失败消息,可能伴随着由配置缓存引起的问题
❯ gradle --configuration-cache greet ... * What went wrong: Index 4 out of bounds for length 3
这些错误可能不会立即指向根本原因,使调试更具挑战性。例如,可能很难将无效索引错误与序列化问题关联起来。
启用完整性检查后重新运行构建,可以提供更精确的诊断,帮助您更快地找出问题的根源
❯ gradle --configuration-cache -Dorg.gradle.configuration-cache.integrity-check=true greet ... FAILURE: Build failed with an exception. * What went wrong: Configuration cache state could not be cached: field `user` of task `:greet` of type `GreetTask`: The value cannot be decoded properly with 'JavaObjectSerializationCodec'. It may have been written incorrectly or its data is corrupted.
您可以立即看到导致问题的任务名称以及包含损坏数据的字段。
请记住,这种归因是尽力而为的:在大多数情况下它应该准确,但在少数情况下,某些字节模式可能会干扰它。
完整性检查依赖于存储在缓存中的额外元数据。因此,它不能用于诊断在启用完整性检查之前已经损坏的条目。 |
当前的完整性检查主要侧重于识别序列化协议问题,而不是一般的数据损坏。因此,它们对硬件相关的问题(如位腐败或存储扇区损坏)效果较差。由于这些限制以及完整性检查引入的性能开销,我们建议选择性地将它们作为故障排除措施启用,而不是永久启用。
Configuration Cache 采用步骤
一个重要的前提条件是保持您的 Gradle 和插件处于最新状态。以下步骤概述了成功采用的推荐方法。这些步骤适用于构建和插件。
在遵循此过程时,请参阅 HTML 报告以及需求部分中解释的解决方案。
-
从
help
开始始终从测试构建或插件的最简单任务开始:
help
。这会演练构建或插件的最小配置阶段。 -
逐步针对有用的任务
避免立即运行
build
。而是-
首先使用
--dry-run
识别配置时问题。 -
在处理构建时,专注于您的开发反馈循环,例如修改源代码后运行测试。
-
在处理插件时,逐步针对贡献的或配置的任务。
-
-
通过将问题转化为警告来探索
不要止步于第一次构建失败——将问题转化为警告,以便更好地了解您的构建和插件的行为方式。如果构建失败
-
使用 HTML 报告分析报告的问题。
-
继续测试更多任务,以全面了解影响您的构建和插件的问题。
-
请记住,将问题转化为警告时,如果出现意外行为,您可能需要手动失效缓存。
-
-
退后一步,迭代地修复问题
一旦您对问题有了清晰的了解,就开始迭代地修复它们。使用 HTML 报告和本文档来指导您的过程
-
从报告 Configuration Cache 存储时出现的问题开始。
-
修复这些问题后,继续解决加载 Configuration Cache 时遇到的任何问题。
-
-
报告遇到的问题
如果您遇到本文档未涵盖的Gradle 特性或核心 Gradle 插件问题,请将其报告给
gradle/gradle
。对于社区 Gradle 插件,请检查问题是否已列在gradle/gradle#13490,并在必要时向插件的问题跟踪器报告。
一个好的 Bug 报告应包含
-
此文档的链接。
-
您测试的插件版本。
-
任何自定义插件配置,或者理想情况下是可重现的构建。
-
失败描述(例如,特定任务的问题)。
-
构建失败输出的副本。
-
自包含的
configuration-cache-report.html
文件。
-
-
测试,测试,再测试
为您的构建逻辑添加测试以尽早发现问题。有关测试 Configuration Cache 兼容性的详细信息,请参阅测试您的构建逻辑。这将有助于迭代过程并防止回归。
-
将其推广到您的团队
一旦您的开发人员工作流程(例如,从 IDE 运行测试)稳定后,请考虑为您的团队启用 Configuration Cache
-
最初,将其作为可选功能引入。
-
如有必要,将问题转化为警告,并在您的
gradle.properties
文件中设置允许的最大问题数量。 -
默认情况下保持 Configuration Cache 禁用,并鼓励团队成员通过配置其 IDE 运行设置来选择启用。
-
当更多工作流程稳定后,反转此方法
-
默认启用 Configuration Cache。
-
配置 CI 在需要时禁用它。
-
沟通任何需要禁用 Configuration Cache 的不支持的工作流程。
-
-
在构建中响应 Configuration Cache
构建逻辑或插件实现可以检测特定构建是否启用了 Configuration Cache,并相应地调整行为。
Configuration Cache 的启用状态在相应的构建特性中提供。您可以通过注入 BuildFeatures
服务到您的代码中来访问它。
此信息可用于
-
在启用 Configuration Cache 时,以不同的方式配置插件特性。
-
禁用尚未与 Configuration Cache 兼容的可选特性。
-
为用户提供额外指导,例如告知他们临时限制或建议调整他们的设置。
采用 Configuration Cache 行为的变化
Gradle 发布版通过检测配置逻辑与环境交互的更多情况来持续增强 Configuration Cache。这些改进通过防止错误的缓存命中来提高缓存的正确性,但也引入了插件和构建逻辑必须遵循的更严格的规则以实现最优缓存。
一些新检测到的配置输入可能不会影响配置的任务,但仍可能导致缓存失效。为了最大程度地减少不必要的缓存未命中,请遵循以下步骤
-
使用Configuration Cache 报告识别有问题的配置输入。
-
修复项目构建逻辑访问的未声明配置输入。
-
将第三方插件导致的问题报告给其维护者,并在问题修复后更新插件。
-
-
对于特定情况使用退出选项,以暂时恢复到早期行为并阻止 Gradle 跟踪某些输入。这有助于缓解由过时插件引起的性能问题。
在以下情况下,可以暂时退出配置输入检测
-
Gradle 现在将文件系统交互(包括
File.exists()
或File.isFile()
等检查)作为配置输入进行跟踪。为了防止输入跟踪因这些文件系统检查而导致缓存失效,请在
gradle.properties
中使用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 之前,当 Configuration Cache 序列化任务图时,一些从未在配置期间使用的未声明配置输入仍然可以被读取。然而,这些更改并未导致缓存失效。
从 Gradle 8.4 开始,这些未声明的输入被正确跟踪,现在会导致缓存失效。
要暂时恢复到之前的行为,请将 Gradle 属性
org.gradle.configuration-cache.inputs.unsafe.ignore.in-serialization
设置为true
。
谨慎使用这些退出选项,并且仅在它们不影响任务执行结果时使用。这些选项旨在作为临时解决方法,并将在未来的 Gradle 版本中移除。
测试您的构建逻辑
Gradle TestKit 是一个旨在促进测试 Gradle 插件和构建逻辑的库。有关使用 TestKit 的一般指导,请参阅专门章节。
要在启用 Configuration Cache 的情况下测试您的构建逻辑,请将 --configuration-cache
参数传递给GradleRunner
,或使用启用 Configuration Cache中描述的其他方法之一。
为了正确测试 Configuration Cache 的行为,任务必须执行两次
@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 | 第一次运行预热 Configuration Cache。 |
2 | 第二次运行重用 Configuration Cache。 |
3 | 断言 Configuration Cache 被重用。 |
如果 Gradle 遇到 Configuration Cache 问题,它将导致构建失败并报告问题,从而导致测试失败。
对于 Gradle 插件作者来说,推荐的方法是在启用 Configuration Cache 的情况下运行整个测试套件。这确保了与支持的 Gradle 版本的兼容性。
|
Configuration Cache 要求
为了使用 Configuration Cache 捕获和重新加载任务图状态,Gradle 对任务和构建逻辑强制执行特定要求。任何违反这些要求的行为都会被报告为 Configuration Cache 的“问题”,这会导致构建失败。
在大多数情况下,这些要求会暴露未声明的输入,使构建更加严格、正确和可靠。使用 Configuration Cache 实际上是选择启用这些改进。
以下部分描述了每个要求,并提供了解决构建中问题的指导。
某些类型不得被任务引用
任务字段或任务操作(例如 doFirst {}
或 doLast {}
)中不得引用某些类型。
这些类型属于以下类别
-
活动 JVM 状态类型
-
Gradle 模型类型
-
依赖管理类型
存在这些限制是因为 Configuration Cache 不易存储或重建这些类型。
活动 JVM 状态类型
禁止使用活动 JVM 状态类型(例如 ClassLoader
, Thread
, OutputStream
, Socket
),因为它们不代表任务输入或输出。
Gradle 模型类型
Gradle 模型类型(例如 Gradle
, Settings
, Project
, SourceSet
, Configuration
)通常用于传递任务输入,但这些输入本应被显式声明。
例如,与其在执行时引用 Project 来检索 project.version
,不如将项目版本声明为 Property<String>
输入。类似地,与其引用 SourceSet
来获取源文件或类路径解析,不如将它们声明为 FileCollection
输入。
依赖管理类型
相同的要求也适用于依赖管理类型,但有一些细微差别。
某些依赖管理类型,例如 Configuration
和 SourceDirectorySet
,不应作为任务输入,因为它们包含不必要的状态且不够精确。请改用提供必要功能的限制较少的类型
-
如果引用
Configuration
来获取解析后的文件,请声明一个FileCollection
输入。 -
如果引用
SourceDirectorySet
,请声明一个FileTree
输入。
此外,禁止引用已解析的依赖项结果(例如 ArtifactResolutionQuery
, ResolvedArtifact
, ArtifactResult
)。请改用
-
使用
ResolutionResult.getRootComponent()
的Provider<ResolvedComponentResult>
。 -
使用
ArtifactCollection.getResolvedArtifacts()
,它返回一个Provider<Set<ResolvedArtifactResult>>
。
任务应避免引用已解析的结果,而是依赖于惰性规范将依赖项解析推迟到执行时。
某些类型,例如 Publication
或 Dependency
,不可序列化,但将来可能会实现。如有必要,Gradle 可能会允许它们作为任务输入。
以下任务引用了 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 |
如果脚本中的一个临时任务在 doLast {}
闭包中捕获了不允许的引用
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 | 不再报告问题,任务操作引用了提供 FileSystemOperations 服务的 injected ,它作为 project.copy {} 的替代得到支持 |
修复脚本中的临时任务需要额外的努力,这是一个将它们重构为适当任务类的好机会。
下表列出了常用 Project
方法的推荐替代项
而非 | 使用 |
---|---|
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
|
|
|
|
|
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
|
|
|
|
|
|
|
|
|
|
任务输入或输出属性,或脚本变量,用于捕获使用 |
|
|
|
|
|
|
|
|
|
可用于您的构建逻辑的 Kotlin, Groovy 或 Java API。 |
|
|
|
|
|
|
|
共享可变对象
将任务存储在 Configuration Cache 中时,通过任务字段引用的所有对象都会被序列化。
在大多数情况下,反序列化会保留引用相等性——如果在配置时两个字段 a
和 b
引用同一个实例,反序列化后它们仍然会引用同一个实例(在 Groovy/Kotlin 语法中是 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 操作捕获封闭范围的引用。这些捕获的引用也会被序列化到 Configuration Cache。 |
2 | 比较存储在任务字段中的用户定义类对象的引用与在 doLast 操作中捕获的引用。 |
3 | 比较存储在任务字段中的 ArrayList 实例的引用与在 doLast 操作中捕获的引用。 |
4 | 检查存储和捕获的列表的相等性。 |
没有 Configuration Cache 时,两种情况都保留引用相等性
❯ ./gradlew --no-configuration-cache checkEquality > Task :checkEquality POJO reference equality: true Collection reference equality: true Collection equality: true
启用 Configuration Cache 后,只有用户定义的对象引用保持相同。列表引用不同,尽管列表本身仍然相等
❯ ./gradlew --configuration-cache checkEquality > Task :checkEquality POJO reference equality: true Collection reference equality: false Collection equality: true
最佳实践
-
避免在配置和执行阶段之间共享可变对象。
-
如果需要共享状态,请将其包装在用户定义的类中。
-
不要依赖标准 Java, Groovy, Kotlin 或 Gradle 定义类型的引用相等性。
任务之间不会保留引用相等性——每个任务都是一个独立的“领域”。要在任务之间共享对象,请使用构建服务来包装共享状态。
运行外部进程
插件和构建脚本应避免在配置时运行外部进程。
通常,建议在任务中运行具有正确声明输入和输出的外部进程,以避免任务 UP-TO-DATE
时进行不必要的工作。但是,如果需要,您应仅使用下述与 configuration cache 兼容的 API,而不是 Java 和 Groovy 标准 API,或 Gradle 提供的方法 Project.exec
, Project.javaexec
, ExecOperations.exec
和 ExecOperations.javaexec
。这些方法的灵活性使 Gradle 无法确定调用如何影响构建配置,从而难以确保 Configuration Cache 条目可以安全地重用。
对于简单情况,当获取进程输出就足够时,可以使用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
中使用标准的 Java/Kotlin/Groovy 进程 API,例如 java.lang.ProcessBuilder
。
然后可以使用 ValueSource
实现通过providers.of
创建一个提供者
val gitVersionProvider = providers.of(GitVersionValueSource::class) {}
val gitVersion = gitVersionProvider.get()
def gitVersionProvider = providers.of(GitVersionValueSource.class) {}
def gitVersion = gitVersionProvider.get()
在这两种方法中,如果在配置时使用提供者的值,它将成为构建配置输入。外部进程将在每次构建时执行,以确定 Configuration Cache 是否是 UP-TO-DATE
的,因此建议仅在配置时调用快速运行的进程。如果值发生变化,缓存就会失效,并且进程将在本次构建期间作为配置阶段的一部分再次运行。
读取系统属性和环境变量
插件和构建脚本可以在配置时直接使用标准的 Java, Groovy 或 Kotlin API 或值提供者 API 读取系统属性和环境变量。这样做会使这些变量或属性成为构建配置输入。因此,更改它们的值会使 Configuration Cache 失效。
Configuration Cache 报告包含这些构建配置输入的列表,以帮助跟踪它们。
通常,您应避免在配置时读取系统属性和环境变量的值,以避免在这些值更改时导致缓存未命中。相反,您可以将 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_")
}
谓词中的逻辑对于 Configuration Cache 是不透明的,因此所有环境变量都被视为输入。减少输入数量的一种方法是始终使用查询具体变量名称的方法,例如 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_")
请注意,Configuration Cache 不仅在变量值改变或变量被移除时会失效,而且在环境中添加了另一个具有匹配前缀的变量时也会失效。
对于更复杂的用例,可以使用自定义 ValueSource
实现。在 ValueSource
代码中引用的系统属性和环境变量不会成为构建配置输入,因此可以应用任何处理。相反,ValueSource
的值会在每次构建运行时重新计算,并且只有在值更改时 Configuration Cache 才会失效。例如,可以使用 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
通常,您应避免在配置时读取文件,以避免在文件内容更改时使 Configuration Cache 条目失效。相反,您可以将 providers.fileContents()
返回的 Provider
连接到任务属性。
字节码修改和 Java Agent
为了检测配置输入,Gradle 会修改构建脚本类路径上的类字节码,例如插件及其依赖项。Gradle 使用 Java agent 修改字节码。某些库的完整性自检可能会因字节码更改或 agent 的存在而失败。
为了解决这个问题,您可以使用Worker API并结合类加载器或进程隔离来封装库代码。 Worker 的类路径字节码不会被修改,因此自检应该通过。当使用进程隔离时,Worker 操作会在一个没有安装 Gradle Java agent 的单独 Worker 进程中执行。
在简单情况下,如果库也提供了命令行入口点(public static void main()
方法),您还可以使用 JavaExec
任务来隔离库。
凭据和秘密的处理
当前,Configuration Cache 没有内置机制来防止存储作为输入使用的秘密。因此,秘密可能会存储在序列化的 Configuration Cache 条目中,默认情况下,该条目存储在您的项目目录下的 .gradle/configuration-cache
中。
为了减轻意外暴露的风险,Gradle 会加密 Configuration Cache。在需要时,Gradle 会透明地生成一个机器特定的密钥,将其缓存到 GRADLE_USER_HOME
目录下,并用它来加密项目特定的缓存中的数据。
为了进一步增强安全性,请遵循以下建议
-
保护对 Configuration Cache 条目的访问。
-
使用
GRADLE_USER_HOME/gradle.properties
文件存储密钥。此文件的内容**不**包含在 Configuration Cache 中——仅包含其指纹。如果在此文件中存储密钥,请确保访问权限已得到**适当限制**。
请参阅 gradle/gradle#22618。
使用 GRADLE_ENCRYPTION_KEY
环境变量提供加密密钥
默认情况下,Gradle 会自动生成并管理加密密钥,作为 Java 密钥库存储在 GRADLE_USER_HOME
目录下。
对于不希望出现此行为的环境(例如 GRADLE_USER_HOME
目录在多台机器之间共享),您可以使用 GRADLE_ENCRYPTION_KEY
环境变量明确提供加密密钥。
必须在多次 Gradle 运行中**一致提供**相同的加密密钥;否则,Gradle 将无法重用现有的缓存配置。 |
生成与 GRADLE_ENCRYPTION_KEY
兼容的加密密钥
要使用用户指定的加密密钥加密 Configuration Cache,Gradle 要求将 GRADLE_ENCRYPTION_KEY
环境变量设置为有效的 AES 密钥,并将其编码为 Base64 字符串。
您可以使用以下命令生成与 Base64 编码的 AES 兼容的密钥:
❯ openssl rand -base64 16
此命令适用于 Linux 和 macOS,如果在 Windows 上使用 Cygwin 等工具,也同样适用。
生成密钥后,将其 Base64 编码的值设置为 GRADLE_ENCRYPTION_KEY
环境变量的值。
❯ export GRADLE_ENCRYPTION_KEY="your-generated-key-here"
尚未实现
某些 Gradle 功能尚不支持配置缓存。这些功能将在未来的 Gradle 版本中得到支持。
共享 Configuration Cache
Configuration Cache 目前仅存储在本地。它可以被热和冷的本地 Gradle 守护进程重用,但不能在开发人员或 CI 机器之间共享。
请参阅 gradle/gradle#13510。
使用 TestKit 运行构建时使用 Java Agent
当使用 TestKit 运行构建时,Configuration Cache 可能会干扰应用于这些构建的 Java agent,例如 Jacoco agent。
请参阅 gradle/gradle#25979。
将 Gradle 属性作为构建配置输入的细粒度跟踪
目前,Gradle 属性的所有外部来源(例如 gradle.properties
文件(项目目录和 GRADLE_USER_HOME
中)、环境变量、设置属性的系统属性以及通过命令行标志指定的属性)都被视为构建配置输入,无论它们在配置期间是否实际使用。
但是,这些来源不包含在 Configuration Cache 报告中。
请参阅 gradle/gradle#20969。
Java 对象序列化
Gradle 允许支持Java 对象序列化协议的对象存储在 Configuration Cache 中。
当前的实现仅限于支持序列化的类,这些类要么实现 java.io.Externalizable
接口,要么实现 java.io.Serializable
接口并定义以下方法组合之一:
-
writeObject
方法与readObject
方法结合使用,以精确控制要存储哪些信息; -
仅有
writeObject
方法而没有对应的readObject
方法;writeObject
最终必须调用ObjectOutputStream.defaultWriteObject
; -
仅有
readObject
方法而没有对应的writeObject
方法;readObject
最终必须调用ObjectInputStream.defaultReadObject
; -
writeReplace
方法,允许类指定一个替换对象用于写入; -
readResolve
方法,允许类指定一个替换对象用于刚刚读取的对象;
**不**支持以下 Java 对象序列化 特性:
-
serialPersistentFields
成员,用于明确声明哪些字段是可序列化的;如果存在此成员,则会被忽略;Configuration Cache 将除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。
在执行时访问构建脚本的顶层方法和变量
在构建脚本中重用逻辑和数据的常见方法是将重复的部分提取到顶层方法和变量中。但是,如果启用了 Configuration Cache,目前不支持在执行时调用此类方法。
对于用 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)
}
}
启用 Configuration Cache 运行该任务会产生以下错误:
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 编写的构建脚本根本无法将引用顶层方法或变量的任务在执行时存储到 Configuration Cache 中。存在此限制是因为捕获的脚本对象引用无法序列化。listFiles
任务的 Kotlin 版本在首次运行时就会因 Configuration Cache 问题而失败。
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 版本与 Configuration Cache 兼容,请进行以下更改:
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。
使用 Build Services 使 Configuration Cache 失效
目前,如果 ValueSource
的值在配置时被访问,则无法将 BuildServiceProvider
或其派生的提供者与 map
或 flatMap
一起用作 ValueSource
的参数。当此类 ValueSource
在配置阶段作为一部分执行的任务中获得时,例如 buildSrc
构建或贡献插件的 included builds 中的任务,也适用同样的限制。
请注意,在任务的 @Internal
注解属性中使用 @ServiceReference
或存储 BuildServiceProvider
是安全的。
一般来说,此限制使得无法使用 BuildService
来使 Configuration Cache 失效。
请参阅 gradle/gradle#24085。