本节提供解决配置缓存问题的通用指南,无论是你的构建逻辑还是 Gradle 插件中的问题。

使用配置报告

当 Gradle 在序列化执行任务所需的必要状态时遇到问题,它会生成一份 HTML 报告,详细说明检测到的问题。控制台输出中包含一个可点击的链接,指向此报告,允许你调查根本原因。

考虑以下包含两个问题的构建脚本:

build.gradle.kts
tasks.register("someTask") {
    val destination = System.getProperty("someDestination") (1)
    inputs.dir("source")
    outputs.dir(destination)
    doLast {
        project.copy { (2)
            from("source")
            into(destination)
        }
    }
}
build.gradle
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
...
Calculating task graph as no cached configuration is available for tasks: someTask
> Task :someTask FAILED

1 problem was found storing the configuration cache.
- Build file 'build.gradle': line 6: invocation of 'Task.project' at execution time is unsupported with the configuration cache.
  See https://docs.gradle.org.cn/0.0.0/userguide/configuration_cache_requirements.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
1 actionable task: 1 executed
Configuration cache entry discarded with 1 problem.

FAILURE: Build failed with an exception.

* Where:
Build file '/home/user/gradle/samples/build.gradle' line: 6

* What went wrong:
Execution failed for task ':someTask'.
> Invocation of 'Task.project' by task ':someTask' at execution time is unsupported with the configuration cache.

* 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 generate a Build Scan (Powered by Develocity).
> Get more help at https://help.gradle.org.

BUILD FAILED in 0s
Configuration Cache entry discarded with 1 problem.

由于检测到问题,Gradle 会丢弃配置缓存条目,阻止在未来的构建中重用。

链接的 HTML 报告提供了检测到的问题的详细信息:

problems report

报告以两种方式呈现问题:

  • 按消息分组 → 快速识别重复的问题类型。

  • 按任务分组 → 识别哪些任务导致问题。

展开问题树有助于在对象图中找到根本原因。

此外,报告列出了检测到的构建配置输入,例如在配置期间访问的系统属性、环境变量和值供应商:

inputs report

报告中的每个问题条目都包含指向相关配置缓存要求的链接,以提供解决问题的指导,以及任何相关的尚未实现的功能

修改构建或插件时,考虑使用 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 with the configuration cache.
  See https://docs.gradle.org.cn/0.0.0/userguide/configuration_cache_requirements.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 with the configuration cache.
  See https://docs.gradle.org.cn/0.0.0/userguide/configuration_cache_requirements.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 将成功存储和重用配置缓存,同时继续报告问题。

报告和控制台日志提供了指向解决检测到的问题的指导的链接。

这是示例构建脚本的修正版本:

build.gradle.kts
abstract class MyCopyTask : DefaultTask() { (1)

    @get:InputDirectory abstract val source: DirectoryProperty (2)

    @get:OutputDirectory abstract val destination: DirectoryProperty (2)

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

    @TaskAction
    fun action() {
        fs.copy { (3)
            from(source)
            into(destination)
        }
    }
}

tasks.register<MyCopyTask>("someTask") {
    val projectDir = layout.projectDirectory
    source = projectDir.dir("source")
    destination = projectDir.dir(System.getProperty("someDestination"))
}
build.gradle
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 {}` 的受支持替代方案。

修复这些问题后,两次运行任务会成功重用配置缓存:

❯ ./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 会强制重新运行配置。

更好的方法是使用提供者将读取系统属性的时间推迟到执行时:

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

现在,即使更改系统属性,缓存条目也仍然可重用:

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

通过这些修复,此任务与配置缓存完全兼容。

启用警告模式

为了方便迁移,你可以将配置缓存问题视为警告而不是失败:

$ ./gradlew --configuration-cache-problems=warn

或者在 `gradle.properties` 中设置:

org.gradle.configuration-cache.problems=warn

警告模式是迁移和故障排除辅助工具,不应作为忽略不兼容性的长期方式。它也不会阻止以后意外地将新的不兼容性添加到构建中。

相反,我们建议明确将有问题任务标记为不兼容

默认情况下,Gradle 允许最多 512 个警告,然后才会使构建失败。你可以降低此限制:

$ ./gradlew -Dorg.gradle.configuration-cache.max-problems=5

声明不兼容任务

你可以使用 `Task.notCompatibleWithConfigurationCache()` 方法明确将任务标记为与配置缓存不兼容:

build.gradle.kts
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() }
    }
}
build.gradle
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` 文件中:

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.

你可以立即看到出错任务的名称以及包含损坏数据的字段。

请记住,这种归因是尽力而为的:在大多数情况下应该准确,但在极少数情况下,它可能会被某些字节模式混淆。

完整性检查依赖于存储在缓存中的额外元数据。因此,它不能用于诊断在启用完整性检查之前已经损坏的条目。

当前的完整性检查主要侧重于识别序列化协议问题,而不是一般的数据损坏。因此,它们对硬件相关问题(例如位腐烂或损坏的存储扇区)的效果较差。由于这些限制以及完整性检查引入的性能开销,我们建议选择性地将其作为故障排除措施启用,而不是永久启用。

检查缓存条目

Gradle 开发的 `gcc2speedscope` 工具通过将调试日志转换为与 speedscope.app 兼容的交互式火焰图来分析 Gradle 配置缓存的空间使用情况。

此可视化有助于识别缓存中大或不必要的对象。

调试配置阶段

`gradle-trace-converter` 是 Gradle 开发的命令行工具,用于分析和将构建操作跟踪转换为 Chrome 的 Perfetto 跟踪和 CSV 时间线等格式。

此可视化描绘了 Gradle 配置阶段期间执行的步骤。

测试你的构建逻辑

Gradle TestKit 是一个旨在促进测试 Gradle 插件和构建逻辑的库。有关使用 TestKit 的通用指南,请参阅专用章节

要使用配置缓存启用测试你的构建逻辑,请将 `--configuration-cache` 参数传递给 `GradleRunner`,或使用启用配置缓存中描述的其他方法之一。

为了正确测试配置缓存行为,任务必须执行两次:

src/test/kotlin/org/example/BuildLogicFunctionalTest.kt
@Test
fun `my task can be loaded from the configuration cache`() {

    buildFile.writeText("""
        plugins {
            id 'org.example.my-plugin'
        }
    """)

    runner()
        .withArguments("--configuration-cache", "myTask")        (1)
        .build()

    val result = runner()
        .withArguments("--configuration-cache", "myTask")        (2)
        .build()

    require(result.output.contains("Reusing configuration cache.")) (3)
    // ... more assertions on your task behavior
}
src/test/groovy/org/example/BuildLogicFunctionalTest.groovy
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 版本并具有特定版本测试,请从第一个支持的版本开始启用配置缓存测试。

  • 如果立即完全支持不可行,请考虑编写以下测试:

    • 多次运行所有插件贡献的任务。

    • 断言预期的任务状态,例如 `UP_TO_DATE` 和 `FROM_CACHE` 行为。