为了使用配置缓存捕获和重新加载任务图状态,Gradle 对任务和构建逻辑强制执行特定的要求。任何违反这些要求的行为都将报告为配置缓存“问题”,从而导致构建失败。

在大多数情况下,这些要求暴露出未声明的输入,使构建更加严格、正确和可靠。使用配置缓存实际上是选择性地使用这些改进。

以下各节描述了每个要求,并提供了解决构建中问题的指导。

某些类型不得被任务引用

某些类型不得被任务字段或任务动作(如 doFirst {}doLast {})引用。

这些类型分为以下几类:

  • Live JVM state types(实时 JVM 状态类型)

  • Gradle 模型类型

  • 依赖管理类型

存在这些限制是因为这些类型不能轻易地被配置缓存存储或重建。

Live JVM State Types(实时 JVM 状态类型)

Live JVM state types(例如 ClassLoaderThreadOutputStreamSocket)是不允许的,因为它们不代表任务输入或输出。

唯一的例外是标准流(System.inSystem.outSystem.err),例如,它们可以用作 ExecJavaExec 任务的参数。

Gradle 模型类型

Gradle 模型类型(例如 GradleSettingsProjectSourceSetConfiguration)通常用于传递应明确声明的任务输入。

例如,与其在执行时引用 Project 以检索 project.version,不如将项目版本声明为 Property<String> 输入。同样,与其引用 SourceSet 来获取源文件或类路径解析,不如将这些声明为 FileCollection 输入。

依赖管理类型

同样的要求也适用于依赖管理类型,但有一些细微差别。

一些依赖管理类型,如 ConfigurationSourceDirectorySet,不应作为任务输入使用,因为它们包含不必要的状态且不够精确。请使用提供必要功能的、不太具体的类型代替。

  • 如果引用 Configuration 以获取已解析的文件,请声明一个 FileCollection 输入。

  • 如果引用 SourceDirectorySet,请声明一个 FileTree 输入。

此外,引用已解析的依赖结果是不允许的(例如 ArtifactResolutionQueryResolvedArtifactArtifactResult)。相反,请执行以下操作:

  • 使用 ResolutionResult.getRootComponent() 返回的 Provider<ResolvedComponentResult>

  • 使用 ArtifactCollection.getResolvedArtifacts(),它返回一个 Provider<Set<ResolvedArtifactResult>>

任务应避免引用*已解析*的结果,而是依赖于延迟规范,将依赖解析推迟到执行时。

某些类型,如 PublicationDependency,目前不可序列化,但将来可能会实现。如有必要,Gradle 可能会允许它们作为任务输入。

以下任务引用了 SourceSet,这是不允许的:

build.gradle.kts
abstract class SomeTask : DefaultTask() {

    @get:Input lateinit var sourceSet: SourceSet (1)

    @TaskAction
    fun action() {
        val classpathFiles = sourceSet.compileClasspath.files
        // ...
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {

    @Input SourceSet sourceSet (1)

    @TaskAction
    void action() {
        def classpathFiles = sourceSet.compileClasspath.files
        // ...
    }
}
1 这将报告为一个问题,因为不允许引用 SourceSet

以下是修复后的版本:

build.gradle.kts
abstract class SomeTask : DefaultTask() {

    @get:InputFiles @get:Classpath
    abstract val classpath: ConfigurableFileCollection (1)

    @TaskAction
    fun action() {
        val classpathFiles = classpath.files
        // ...
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {

    @InputFiles @Classpath
    abstract ConfigurableFileCollection getClasspath() (1)

    @TaskAction
    void action() {
        def classpathFiles = classpath.files
        // ...
    }
}
1 不再报告问题,我们现在引用了支持的类型 FileCollection

如果脚本中的一个临时任务在 doLast {} 闭包中捕获了不允许的引用:

build.gradle.kts
tasks.register("someTask") {
    doLast {
        val classpathFiles = sourceSets.main.get().compileClasspath.files (1)
    }
}
build.gradle
tasks.register('someTask') {
    doLast {
        def classpathFiles = sourceSets.main.compileClasspath.files (1)
    }
}
1 这将报告为一个问题,因为 doLast {} 闭包正在捕获对 SourceSet 的引用。

您仍然需要满足相同的要求,即不引用不允许的类型。

任务声明的修复方法如下:

build.gradle.kts
tasks.register("someTask") {
    val classpath = sourceSets.main.get().compileClasspath (1)
    doLast {
        val classpathFiles = classpath.files
    }
}
build.gradle
tasks.register('someTask') {
    def classpath = sourceSets.main.compileClasspath (1)
    doLast {
        def classpathFiles = classpath.files
    }
}
1 不再报告问题,doLast {} 闭包现在只捕获 classpath,它是受支持的 FileCollection 类型。

有时,不允许的类型会通过另一种类型间接引用。例如,一个任务可能引用一个允许的类型,而该类型又引用一个不允许的类型。HTML 问题报告中的层次结构视图可以帮助您追踪此类问题并识别违规引用。

在执行时使用 Project 对象

任务在执行期间不得使用任何 Project 对象。这包括在任务运行时调用 Task.getProject()

某些情况可以按照 不允许的类型 中描述的方式解决。

通常,ProjectTask 都提供等效的功能。例如:

  • 如果需要 Logger,请使用 Task.logger 而不是 Project.logger

  • 对于文件操作,请使用 注入的服务 而不是 Project 方法。

以下任务在执行时错误地引用了 Project 对象:

build.gradle.kts
abstract class SomeTask : DefaultTask() {
    @TaskAction
    fun action() {
        project.copy { (1)
            from("source")
            into("destination")
        }
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {
    @TaskAction
    void action() {
        project.copy { (1)
            from 'source'
            into 'destination'
        }
    }
}
1 这将报告为一个问题,因为任务操作在执行时使用了 Project 对象。

修复后的版本:

build.gradle.kts
abstract class SomeTask : DefaultTask() {

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

    @TaskAction
    fun action() {
        fs.copy {
            from("source")
            into("destination")
        }
    }
}
build.gradle
abstract class SomeTask extends DefaultTask {

    @Inject abstract FileSystemOperations getFs() (1)

    @TaskAction
    void action() {
        fs.copy {
            from 'source'
            into 'destination'
        }
    }
}
1 不再报告问题,注入的 FileSystemOperations 服务被支持作为 project.copy {} 的替代方案。

如果脚本中的临时任务出现相同问题:

build.gradle.kts
tasks.register("someTask") {
    doLast {
        project.copy { (1)
            from("source")
            into("destination")
        }
    }
}
build.gradle
tasks.register('someTask') {
    doLast {
        project.copy { (1)
            from 'source'
            into 'destination'
        }
    }
}
1 这将报告为一个问题,因为任务操作在执行时使用了 Project 对象。

修复后的版本:

build.gradle.kts
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")
        }
    }
}
build.gradle
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 {} 的替代。

修复脚本中的临时任务需要额外的努力,这使其成为将其重构为正确的任务类的良好机会。

下表列出了常用 Project 方法的推荐替代方案:

代替 使用

project.rootDir

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.rootDir 计算实际参数的结果。

project.projectDir

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.projectDir 计算实际参数的结果。

project.buildDir

project.buildDir 已被弃用。您应该使用 project.layout.buildDirectory。使用任务输入或输出属性或脚本变量捕获使用 project.layout.buildDirectory 计算实际参数的结果。

project.name

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.name 计算实际参数的结果。

project.description

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.description 计算实际参数的结果。

project.group

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.group 计算实际参数的结果。

project.version

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.version 计算实际参数的结果。

project.propertiesproject.property(name)project.hasProperty(name)project.getProperty(name)project.findProperty(name)

project.logger

project.provider {}

project.file(path)

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.file(file) 计算实际参数的结果。

project.uri(path)

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.uri(path) 计算实际参数的结果。否则,可以使用 File.toURI() 或其他 JVM API。

project.relativePath(path)

project.files(paths)

project.fileTree(paths)

project.zipTree(path)

project.tarTree(path)

project.resources

一个任务输入或输出属性,或一个脚本变量,用于捕获使用 project.resource 计算实际参数的结果。

project.copySpec {}

project.copy {}

project.sync {}

project.delete {}

project.mkdir(path)

构建逻辑中可用的 Kotlin、Groovy 或 Java API。

project.exec {}

project.javaexec {}

project.ant {}

project.createAntBuilder()

从另一个实例访问任务实例

任务不得直接访问另一个任务实例的状态。相反,它们应该使用 输入和输出关系 进行连接。

此要求确保任务保持隔离并可正确缓存。因此,不支持编写在执行时配置其他任务的任务。

共享可变对象

将任务存储在配置缓存中时,通过任务字段引用的所有对象都将序列化。

在大多数情况下,反序列化会保留引用相等性——如果在配置时两个字段 ab 引用同一实例,则在反序列化后它们仍将引用同一实例(在 Groovy/Kotlin 语法中,a == ba === b)。

然而,出于性能原因,某些类(例如 java.lang.Stringjava.io.File 和许多 java.util.Collection 实现)在序列化时不会保留引用相等性。反序列化后,引用这些对象的字段可能会引用不同但相等的实例。

考虑一个任务,它将用户定义的对象和 ArrayList 存储为任务字段。

build.gradle.kts
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)
    }
}
build.gradle
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 检查存储列表和捕获列表的相等性。

在没有配置缓存的情况下,引用相等性在两种情况下都保留:

❯ ./gradlew --no-configuration-cache checkEquality
> Task :checkEquality
POJO reference equality: true
Collection reference equality: true
Collection equality: true

启用配置缓存后,只有用户定义的对象的引用保持相同。列表引用不同,尽管列表本身保持相等。

❯ ./gradlew --configuration-cache checkEquality
> Task :checkEquality
POJO reference equality: true
Collection reference equality: false
Collection equality: true

最佳实践

  • 避免在配置和执行阶段之间共享可变对象。

  • 如果必须共享状态,请将其包装在用户定义的类中。

  • 不要依赖标准 Java、Groovy、Kotlin 或 Gradle 定义类型的引用相等性。

任务之间永远不会保留引用相等性——每个任务都是一个独立的“领域”。要在任务之间共享对象,请使用 构建服务 来包装共享状态。

访问任务扩展或约定

任务在执行时**不得**访问约定、扩展或额外属性。

相反,任何与任务执行相关的值都应明确地建模为任务属性,以确保适当的缓存和可重现性。

使用构建监听器

插件和构建脚本**不得**注册在配置时创建并在执行时触发的构建监听器。这包括 BuildListenerTaskExecutionListener 等监听器。

推荐的替代方案

运行外部进程

插件和构建脚本应避免在配置时运行外部进程。

您应该避免在配置期间使用这些 API 来运行进程:

  • Java/KotlinProcessBuilderRuntime.exec(…​) 等…

  • Groovy*.execute() 等…

  • GradleExecOperations.execExecOperations.javaexec 等…

这些方法的灵活性使得 Gradle 无法确定调用如何影响构建配置,从而难以确保可以安全地重用配置缓存条目。

但是,如果需要在配置时运行进程,您可以使用下面详细介绍的配置缓存兼容 API。

对于更简单的情况,当获取进程的输出就足够时,可以使用 providers.exec()providers.javaexec()

build.gradle.kts
val gitVersion = providers.exec {
    commandLine("git", "--version")
}.standardOutput.asText.get()
build.gradle
def gitVersion = providers.exec {
    commandLine("git", "--version")
}.standardOutput.asText.get()

对于更复杂的情况,可以使用注入了 ExecOperations 的自定义 ValueSource 实现。这个 ExecOperations 实例可以在配置时不受限制地使用。

build.gradle.kts
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())
    }
}
build.gradle
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 创建一个提供者。

build.gradle.kts
val gitVersionProvider = providers.of(GitVersionValueSource::class) {}
val gitVersion = gitVersionProvider.get()
build.gradle
def gitVersionProvider = providers.of(GitVersionValueSource.class) {}
def gitVersion = gitVersionProvider.get()

在这两种方法中,如果提供者的值在配置时使用,那么它将成为构建配置输入。外部进程将为每次构建执行,以确定配置缓存是否为 UP-TO-DATE,因此建议仅在配置时调用快速运行的进程。如果值发生变化,则缓存将失效,并且该进程将在本次构建期间作为配置阶段的一部分再次运行。

读取系统属性和环境变量

插件和构建脚本可以在配置时直接使用标准 Java、Groovy 或 Kotlin API 或值供应商 API 读取系统属性和环境变量。这样做会将此类变量或属性作为构建配置输入。因此,更改其值会使配置缓存失效。

配置缓存报告包含这些构建配置输入的列表,以帮助跟踪它们。

通常,您应该避免在配置时读取系统属性和环境变量的值,以避免当这些值发生变化时导致缓存未命中。相反,您可以将 providers.systemProperty()providers.environmentVariable() 返回的 Provider 连接到任务属性。

某些可能枚举所有环境变量或系统属性的访问模式(例如,调用 System.getenv().forEach() 或使用其 keySet() 的迭代器)是不鼓励的。在这种情况下,Gradle 无法找出哪些属性是实际的构建配置输入,因此每个可用的属性都成为一个输入。即使添加一个新属性,如果使用此模式,也会使缓存失效。

使用自定义谓词过滤环境变量是这种不鼓励模式的一个例子:

build.gradle.kts
val jdkLocations = System.getenv().filterKeys {
    it.startsWith("JDK_")
}
build.gradle
def jdkLocations = System.getenv().findAll {
    key, _ -> key.startsWith("JDK_")
}

谓词中的逻辑对配置缓存来说是不透明的,因此所有环境变量都被视为输入。减少输入数量的一种方法是始终使用查询具体变量名的方法,例如 getenv(String)getenv().get()

build.gradle.kts
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)
}
build.gradle
def jdkVariables = ["JDK_8", "JDK_11", "JDK_17"]
def jdkLocations = jdkVariables.findAll { v ->
    System.getenv(v) != null
}.collectEntries { v ->
    [v, System.getenv(v)]
}

然而,上面修复的代码与原始代码并不完全等效,因为只支持明确的变量列表。基于前缀的过滤是一种常见场景,因此有基于提供者的 API 来访问系统属性环境变量

build.gradle.kts
val jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")
build.gradle
def jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")

请注意,配置缓存不仅在变量值更改或变量被删除时失效,而且在环境中添加另一个具有匹配前缀的变量时也会失效。

对于更复杂的用例,可以使用自定义 ValueSource 实现。在 ValueSource 代码中引用的系统属性和环境变量不会成为构建配置输入,因此可以应用任何处理。相反,ValueSource 的值在每次构建运行时都会重新计算,并且只有当值发生变化时,配置缓存才会失效。例如,可以使用 ValueSource 获取所有名称中包含子字符串 JDK 的环境变量。

build.gradle.kts
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"
    }
}
build.gradle
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 将文件声明为潜在的构建配置输入。

这个问题是由类似的构建逻辑引起的:

build.gradle.kts
val config = file("some.conf").readText()
build.gradle
def config = file('some.conf').text

要解决此问题,请改用 providers.fileContents() 读取文件。

build.gradle.kts
val config = providers.fileContents(layout.projectDirectory.file("some.conf"))
    .asText
build.gradle
def config = providers.fileContents(layout.projectDirectory.file('some.conf'))
    .asText

一般来说,您应该避免在配置时读取文件,以免在文件内容发生变化时导致配置缓存条目失效。相反,您可以将 providers.fileContents() 返回的 Provider 连接到任务属性。

字节码修改和 Java Agent

为了检测配置输入,Gradle 会修改构建脚本类路径上的类的字节码,例如插件及其依赖项。Gradle 使用 Java Agent 来修改字节码。一些库的完整性自检可能会因为修改后的字节码或 Agent 的存在而失败。

为了解决这个问题,您可以使用 Worker API,通过类加载器或进程隔离来封装库代码。Worker 的类路径的字节码不会被修改,因此自检应该通过。当使用进程隔离时,Worker 动作会在一个独立的 Worker 进程中执行,该进程没有安装 Gradle Java Agent。

在简单的情况下,如果库还提供命令行入口点(public static void main() 方法),您也可以使用 JavaExec 任务来隔离库。

凭据和秘密的处理

目前,配置缓存没有内置机制来阻止存储用作输入的秘密。因此,秘密可能会最终存储在序列化的配置缓存条目中,默认情况下,这些条目存储在项目目录中的 .gradle/configuration-cache 下。

为了减轻意外泄露的风险,Gradle 会对配置缓存进行加密。当需要时,Gradle 会透明地生成一个机器特定的密钥,将其缓存到 GRADLE_USER_HOME 目录中,并使用它来加密项目特定的缓存中的数据。

为了进一步增强安全性,请遵循以下建议:

  • 安全访问配置缓存条目。

  • 使用 GRADLE_USER_HOME/gradle.properties 存储秘密。此文件的内容**不**包含在配置缓存中——只有其指纹。如果在此文件中存储秘密,请确保访问**受到适当限制**。

请参阅 gradle/gradle#22618

使用 GRADLE_ENCRYPTION_KEY 环境变量提供加密密钥

默认情况下,Gradle 会自动生成并管理加密密钥,将其作为 Java 密钥库存储在 GRADLE_USER_HOME 目录下。

对于不希望这种行为的环境(例如,当 GRADLE_USER_HOME 目录在多台机器之间共享时),您可以使用 GRADLE_ENCRYPTION_KEY 环境变量显式提供加密密钥。

在多次 Gradle 运行中**必须始终提供**相同的加密密钥;否则,Gradle 将无法重用现有的缓存配置。

生成与 GRADLE_ENCRYPTION_KEY 兼容的加密密钥

为了使用用户指定的加密密钥加密配置缓存,Gradle 要求将 GRADLE_ENCRYPTION_KEY 环境变量设置为有效的 AES 密钥,并编码为 Base64 字符串。

您可以使用以下命令生成 Base64 编码的 AES 兼容密钥:

❯ openssl rand -base64 16

此命令适用于 Linux 和 macOS,如果使用 Cygwin 等工具,也适用于 Windows。

生成后,将 Base64 编码的密钥设置为 GRADLE_ENCRYPTION_KEY 环境变量的值:

❯ export GRADLE_ENCRYPTION_KEY="your-generated-key-here"