概述

Gradle 构建缓存是一种缓存机制,旨在通过重用其他构建产生的输出来节省时间。构建缓存通过存储(本地或远程)构建输出,并在确定输入未更改时允许构建从缓存中获取这些输出,从而避免了重新生成它们的昂贵工作。

第一个使用构建缓存的功能是任务输出缓存。本质上,任务输出缓存利用与 Gradle 用于避免在先前的本地构建已经生成了一组任务输出时的工作的最新检查相同的智能。但任务输出缓存不仅限于同一工作空间中的先前构建,它还允许 Gradle 重用本地机器上任何位置的任何先前构建中的任务输出。当使用共享构建缓存进行任务输出缓存时,这甚至适用于开发人员机器和构建代理。

除了任务,工件转换也可以利用构建缓存并类似任务输出缓存地重用它们的输出。

要动手学习如何使用构建缓存,请从阅读构建缓存的用例和后续章节开始。它涵盖了缓存可以改进的不同场景,并详细讨论了在为构建启用缓存时需要注意的不同注意事项。

启用构建缓存

默认情况下,构建缓存未启用。您可以通过几种方式启用构建缓存

在命令行中使用 --build-cache 运行

Gradle 将仅为此构建使用构建缓存。

在您的 gradle.properties 中放入 org.gradle.caching=true

Gradle 将尝试重用所有构建的先前构建的输出,除非使用 --no-build-cache 显式禁用。

启用构建缓存后,它会将构建输出存储在 Gradle 用户主目录中。要配置此目录或不同类型的构建缓存,请参阅配置构建缓存

任务输出缓存

除了最新检查中描述的增量构建之外,Gradle 还可以通过将输入与任务匹配来重用任务先前执行的输出来节省时间。任务输出可以在一台计算机上的构建之间重复使用,甚至可以通过构建缓存跨不同计算机上运行的构建重复使用。

我们专注于用户拥有一个组织范围的远程构建缓存的用例,该缓存由持续集成构建定期填充。开发人员和其他持续集成代理应从远程构建缓存加载缓存条目。我们预计不允许开发人员填充远程构建缓存,并且所有持续集成构建在运行 clean 任务后都会填充构建缓存。

为了您的构建能够很好地与任务输出缓存配合,它必须与增量构建功能配合良好。例如,当您连续运行两次构建时,所有具有输出的任务都应该是 UP-TO-DATE。如果不满足此先决条件,则在启用任务输出缓存时,您无法期望更快的构建或正确的构建。

当您启用构建缓存时,任务输出缓存会自动启用,请参阅启用构建缓存

它看起来像什么

让我们从一个使用 Java 插件的项目开始,该项目有几个 Java 源文件。我们第一次运行构建。

> gradle --build-cache compileJava
:compileJava
:processResources
:classes
:jar
:assemble

BUILD SUCCESSFUL

我们在输出中看到本地构建缓存使用的目录。除此之外,构建与没有构建缓存时相同。让我们清理并再次运行构建。

> gradle clean
:clean

BUILD SUCCESSFUL
> gradle --build-cache assemble
:compileJava FROM-CACHE
:processResources
:classes
:jar
:assemble

BUILD SUCCESSFUL

现在我们看到,不是执行 :compileJava 任务,而是从构建缓存加载了任务的输出。其他任务没有从构建缓存加载,因为它们不可缓存。这是因为 :classes:assemble生命周期任务,而 :processResources:jar 是类似复制的任务,它们不可缓存,因为执行它们通常更快。

可缓存任务

由于任务描述了它的所有输入和输出,Gradle 可以计算一个构建缓存键,它根据任务的输入唯一地定义任务的输出。该构建缓存键用于从构建缓存请求以前的输出或将新的输出存储到构建缓存中。如果以前的构建输出已由其他人(例如您的持续集成服务器或其他开发人员)存储在缓存中,您可以避免在本地执行大多数任务。

以下输入以与最新检查相同的方式对任务的构建缓存键做出贡献

  • 任务类型及其类路径

  • 输出属性的名称

  • “自定义任务类型”一节中所述,注释的属性的名称和值

  • 通过 DSL 通过TaskInputs添加的属性的名称和值

  • Gradle 发行版、buildSrc 和插件的类路径

  • 构建脚本的内容(当它影响任务的执行时)

任务类型需要使用@CacheableTask注解选择加入任务输出缓存。请注意,@CacheableTask不会被子类继承。自定义任务类型默认不可缓存

内置可缓存任务

目前,以下内置 Gradle 任务是可缓存的

所有其他内置任务目前都不可缓存。

某些任务,如CopyJar,通常没有缓存的意义,因为 Gradle 只是将文件从一个位置复制到另一个位置。使不产生输出或没有任务动作的任务可缓存也没有意义。

第三方插件

有一些第三方插件与构建缓存配合良好。最突出的例子是Android 插件 3.1+Kotlin 插件 1.2.21+。对于其他第三方插件,请查阅其文档以了解它们是否支持构建缓存。

声明任务输入和输出

可缓存任务完整地了解其输入和输出非常重要,这样就可以安全地在其他地方重用一次构建的结果。

缺少任务输入可能会导致不正确的缓存命中,即不同的结果被视为相同,因为两个执行都使用相同的缓存键。缺少任务输出可能会导致构建失败,如果 Gradle 没有完全捕获给定任务的所有输出。错误声明的任务输入可能导致缓存未命中,尤其是在包含易失性数据或绝对路径时。(请参阅“任务输入和输出”一节关于应声明为输入和输出的内容。)

任务路径不是构建缓存键的输入。这意味着具有不同任务路径的任务可以重用彼此的输出,只要 Gradle 确定执行它们会产生相同的结果。

为了确保正确声明输入和输出,请使用集成测试(例如使用 TestKit)来检查任务是否为相同的输入产生相同的输出并捕获任务的所有输出文件。我们建议添加测试以确保任务输入是可重定位的,即任务可以从缓存加载到不同的构建目录中(请参阅@PathSensitive)。

为了处理任务的易失性输入,请考虑配置输入标准化

默认情况下将任务标记为不可缓存

有些任务从使用构建缓存中受益不大。一个例子是只在文件系统之间移动数据的任务,例如 Copy 任务。您可以通过向任务添加 @DisableCachingByDefault 注解来表示任务不应被缓存。您还可以提供一个人类可读的理由,说明默认情况下不缓存任务的原因。该注解可以单独使用,也可以与 @CacheableTask 一起使用。

此注解仅用于记录默认情况下不缓存任务的原因。构建逻辑可以通过运行时 API(见下文)覆盖此决定。

启用不可缓存任务的缓存

如我们所见,内置任务或插件提供的任务,如果其类用 Cacheable 注解注释,则可缓存。但是,如果您想使一个其类不可缓存的任务可缓存怎么办?让我们举一个具体的例子:您的构建脚本使用一个通用的 NpmTask 任务通过委托给 NPM(并运行 npm run bundle)来创建 JavaScript 包。此过程类似于复杂的编译任务,但 NpmTask 太通用,默认情况下不可缓存:它只接受参数并使用这些参数运行 npm。

此任务的输入和输出很容易确定。输入是包含 JavaScript 文件的目录和 NPM 配置文件。输出是此任务生成的捆绑文件。

使用注解

我们创建 NpmTask 的子类并使用注解来声明输入和输出

如果可能,最好使用委托而不是创建子类。对于内置的 JavaExecExecCopySync 任务,Project 上有一个方法可以完成实际工作。

如果您是一位现代 JavaScript 开发人员,您会知道打包可能非常耗时,并且值得缓存。为了实现这一点,我们需要告诉 Gradle 允许缓存该任务的输出,使用@CacheableTask注解。

这足以使任务在您自己的机器上可缓存。但是,输入文件默认通过其绝对路径识别。因此,如果缓存需要在几个开发人员或使用不同路径的机器之间共享,那将无法按预期工作。因此,我们还需要设置路径敏感度。在这种情况下,可以使用输入文件的相对路径来识别它们。

请注意,可以通过覆盖基类的 getter 并注释该方法来覆盖基类的属性注解。

build.gradle.kts
@CacheableTask                                       (1)
abstract class BundleTask : NpmTask() {

    @get:Internal                                    (2)
    override val args
        get() = super.args


    @get:InputDirectory
    @get:SkipWhenEmpty
    @get:PathSensitive(PathSensitivity.RELATIVE)     (3)
    abstract val scripts: DirectoryProperty

    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)     (4)
    abstract val configFiles: ConfigurableFileCollection

    @get:OutputFile
    abstract val bundle: RegularFileProperty

    init {
        args.addAll("run", "bundle")
        bundle = projectLayout.buildDirectory.file("bundle.js")
        scripts = projectLayout.projectDirectory.dir("scripts")
        configFiles.from(projectLayout.projectDirectory.file("package.json"))
        configFiles.from(projectLayout.projectDirectory.file("package-lock.json"))
    }
}

tasks.register<BundleTask>("bundle")
build.gradle
@CacheableTask                                       (1)
abstract class BundleTask extends NpmTask {

    @Override @Internal                              (2)
    ListProperty<String> getArgs() {
        super.getArgs()
    }

    @InputDirectory
    @SkipWhenEmpty
    @PathSensitive(PathSensitivity.RELATIVE)         (3)
    abstract DirectoryProperty getScripts()

    @InputFiles
    @PathSensitive(PathSensitivity.RELATIVE)         (4)
    abstract ConfigurableFileCollection getConfigFiles()

    @OutputFile
    abstract RegularFileProperty getBundle()

    BundleTask() {
        args.addAll("run", "bundle")
        bundle = projectLayout.buildDirectory.file("bundle.js")
        scripts = projectLayout.projectDirectory.dir("scripts")
        configFiles.from(projectLayout.projectDirectory.file("package.json"))
        configFiles.from(projectLayout.projectDirectory.file("package-lock.json"))
    }
}

tasks.register('bundle', BundleTask)
  • (1) 添加 @CacheableTask 以启用任务缓存。

  • (2) 覆盖基类的属性的 getter 以将输入注解更改为 @Internal

  • (3) (4) 声明路径敏感度。

使用运行时 API

如果出于某种原因您无法创建新的自定义任务类,也可以使用运行时 API使任务可缓存,以声明输入和输出。

要启用任务缓存,您需要使用TaskOutputs.cacheIf()方法。

通过运行时 API 的声明与上面描述的注解具有相同的效果。请注意,您不能通过运行时 API 覆盖文件输入和输出。输入属性可以通过指定相同的属性名称来覆盖。

build.gradle.kts
tasks.register<NpmTask>("bundle") {
    args = listOf("run", "bundle")

    outputs.cacheIf { true }

    inputs.dir(file("scripts"))
        .withPropertyName("scripts")
        .withPathSensitivity(PathSensitivity.RELATIVE)

    inputs.files("package.json", "package-lock.json")
        .withPropertyName("configFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)

    outputs.file(layout.buildDirectory.file("bundle.js"))
        .withPropertyName("bundle")
}
build.gradle
tasks.register('bundle', NpmTask) {
    args = ['run', 'bundle']

    outputs.cacheIf { true }

    inputs.dir(file("scripts"))
        .withPropertyName("scripts")
        .withPathSensitivity(PathSensitivity.RELATIVE)

    inputs.files("package.json", "package-lock.json")
        .withPropertyName("configFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)

    outputs.file(layout.buildDirectory.file("bundle.js"))
        .withPropertyName("bundle")
}

配置构建缓存

您可以通过在 settings.gradle 中使用 Settings.buildCache(org.gradle.api.Action) 块来配置构建缓存。

Gradle 支持 本地远程 构建缓存,可以单独配置。当两个构建缓存都启用时,Gradle 首先尝试从本地构建缓存加载构建输出,如果没有找到构建输出,则尝试远程构建缓存。如果在远程缓存中找到输出,它们也会存储在本地缓存中,因此下次将在本地找到它们。Gradle 将构建输出存储(“推送”)到任何已启用且将 BuildCache.isPush() 设置为 true 的构建缓存中。

默认情况下,本地构建缓存已启用推送,远程构建缓存已禁用推送。

本地构建缓存已预配置为 DirectoryBuildCache 并默认启用。可以通过指定要连接的构建缓存类型(BuildCacheConfiguration.remote(java.lang.Class))来配置远程构建缓存。

内置本地构建缓存

内置的本地构建缓存,DirectoryBuildCache,使用一个目录来存储构建缓存工件。默认情况下,此目录位于 Gradle 用户主目录中,但其位置可配置。

有关配置选项的更多详细信息,请参阅DirectoryBuildCache的 DSL 文档。以下是配置示例。

settings.gradle.kts
buildCache {
    local {
        directory = File(rootDir, "build-cache")
    }
}
settings.gradle
buildCache {
    local {
        directory = new File(rootDir, 'build-cache')
    }
}

Gradle 将定期清理本地缓存目录,通过删除最近未使用的条目来节省磁盘空间。Gradle 执行此清理的频率以及条目保留多长时间可通过初始化脚本配置,如本节所示。

远程 HTTP 构建缓存

HttpBuildCache 提供了通过 HTTP 从远程缓存读写的N能力。

通过以下配置,本地构建缓存将用于存储构建输出,而本地和远程构建缓存将用于检索构建输出。

settings.gradle.kts
buildCache {
    remote<HttpBuildCache> {
        url = uri("https://example.com:8123/cache/")
    }
}
settings.gradle
buildCache {
    remote(HttpBuildCache) {
        url = 'https://example.com:8123/cache/'
    }
}

当尝试加载条目时,会向 https://example.com:8123/cache/«cache-key» 发送 GET 请求。响应必须具有 2xx 状态和缓存条目作为主体,或者在条目不存在时具有 404 Not Found 状态。

当尝试存储条目时,会向 https://example.com:8123/cache/«cache-key» 发送 PUT 请求。任何 2xx 响应状态都将被解释为成功。可以返回 413 Payload Too Large 响应,表示有效负载大于服务器将接受的范围,这不会被视为错误。

指定访问凭据

支持HTTP 基本认证,凭据将预先发送。

settings.gradle.kts
buildCache {
    remote<HttpBuildCache> {
        url = uri("https://example.com:8123/cache/")
        credentials {
            username = "build-cache-user"
            password = "some-complicated-password"
        }
    }
}
settings.gradle
buildCache {
    remote(HttpBuildCache) {
        url = 'https://example.com:8123/cache/'
        credentials {
            username = 'build-cache-user'
            password = 'some-complicated-password'
        }
    }
}

重定向

3xx 重定向响应将被自动跟踪。

服务器在重定向 PUT 请求时必须小心,因为只有 307308 重定向响应会伴随 PUT 请求。所有其他重定向响应将伴随 GET 请求,根据RFC 7231,不带条目有效负载作为主体。

网络错误处理

在建立 TCP 连接后,在请求传输过程中失败的请求将自动重试。

这可以防止临时问题(例如连接断开、读写超时以及连接重置等低级网络故障)导致缓存操作失败并禁用远程缓存的剩余构建过程。

请求将重试最多 3 次。如果问题仍然存在,缓存操作将失败,远程缓存将在剩余构建过程中被禁用。

使用 SSL

默认情况下,使用 HTTPS 要求服务器提供一个被构建的 Java 运行时信任的证书。如果您的服务器证书不受信任,您可以

  1. 更新您的 Java 运行时的信任存储以使其受信任

  2. 更改构建环境以使用替代的信任存储进行构建运行时

  3. 禁用对受信任证书的要求

可以通过将 HttpBuildCache.isAllowUntrustedServer() 设置为 true 来禁用信任要求。启用此选项存在安全风险,因为它允许任何缓存服务器模拟预期服务器。它应仅作为临时措施或在严格控制的网络环境中使用。

settings.gradle.kts
buildCache {
    remote<HttpBuildCache> {
        url = uri("https://example.com:8123/cache/")
        isAllowUntrustedServer = true
    }
}
settings.gradle
buildCache {
    remote(HttpBuildCache) {
        url = 'https://example.com:8123/cache/'
        allowUntrustedServer = true
    }
}

HTTP Expect-Continue

可以启用HTTP Expect-Continue的使用。这会导致上传请求分为两部分:首先检查是否接受主体,然后如果服务器指示将接受主体则传输主体。

这对于上传到经常重定向或拒绝上传请求的缓存服务器非常有用,因为它避免了仅仅为了被拒绝(例如,缓存条目大于缓存允许的大小)或重定向而上传缓存条目。当服务器接受请求时,此额外检查会增加延迟,但当请求被拒绝或重定向时,它会减少延迟。

并非所有 HTTP 服务器和代理都可靠地实现了 Expect-Continue。请务必在启用之前检查您的缓存服务器是否支持它。

要启用,请将 HttpBuildCache.isUseExpectContinue() 设置为 true

settings.gradle.kts
buildCache {
    remote<HttpBuildCache> {
        url = uri("https://example.com:8123/cache/")
        isUseExpectContinue = true
    }
}
settings.gradle
buildCache {
    remote(HttpBuildCache) {
        url = 'https://example.com:8123/cache/'
        useExpectContinue = true
    }
}

配置用例

远程构建缓存的推荐用例是您的持续集成服务器从干净构建中填充它,而开发人员只从它加载。配置将如下所示。

也可以从初始化脚本配置构建缓存,该脚本可以从命令行使用,添加到您的 Gradle 用户主目录或成为您的自定义 Gradle 分发的一部分。

init.gradle.kts
gradle.settingsEvaluated {
    buildCache {
        // vvv Your custom configuration goes here
        remote<HttpBuildCache> {
            url = uri("https://example.com:8123/cache/")
        }
        // ^^^ Your custom configuration goes here
    }
}
init.gradle
gradle.settingsEvaluated { settings ->
    settings.buildCache {
        // vvv Your custom configuration goes here
        remote(HttpBuildCache) {
            url = 'https://example.com:8123/cache/'
        }
        // ^^^ Your custom configuration goes here
    }
}

构建缓存、复合构建和 buildSrc

Gradle 的复合构建功能允许将其他完整的 Gradle 构建包含到另一个构建中。此类包含的构建将继承顶级构建的构建缓存配置,无论包含的构建是否自己定义构建缓存配置。

任何包含的构建中存在的构建缓存配置实际上都被忽略,而倾向于使用顶级构建的配置。这也适用于任何包含的构建的 buildSrc 项目。

buildSrc 目录被视为一个包含的构建,因此它继承了顶层构建的构建缓存配置。

此配置优先级不适用于通过 pluginManagement 包含的插件构建,因为这些构建在缓存配置本身之前加载。

如何设置 HTTP 构建缓存后端

Gradle 提供了一个 构建缓存节点的 Docker 镜像,它可以连接到 Develocity 进行集中管理。缓存节点也可以在没有 Develocity 安装的情况下使用,但功能受限。

实现您自己的构建缓存

使用不同的构建缓存后端来存储构建输出(内置支持连接到 HTTP 后端未涵盖)需要实现您自己的逻辑来连接到您的自定义构建缓存后端。为此,可以通过 BuildCacheConfiguration.registerBuildCacheService(java.lang.Class, java.lang.Class) 注册自定义构建缓存类型。

Develocity 包含一个高性能、易于安装和操作的共享构建缓存后端。