概述

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

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

除了 task 之外,构件转换 也可以利用构建缓存,并以类似于 task 输出缓存的方式重用其输出。

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

启用构建缓存

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

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

Gradle 将仅针对此构建使用构建缓存。

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

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

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

Task 输出缓存

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

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

为了使您的构建与 task 输出缓存良好配合,它必须与 增量构建 功能良好配合。例如,当连续运行两次构建时,所有具有输出的 task 都应为 UP-TO-DATE。当未满足此先决条件时,您不能期望在启用 task 输出缓存时获得更快的构建或正确的构建。

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

它看起来像什么

让我们从一个使用 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

现在我们看到,task 的输出已从构建缓存加载,而不是执行 :compileJava task。其他 task 没有从构建缓存加载,因为它们不可缓存。这是由于 :classes:assemble生命周期 task,而 :processResources:jar 是类 Copy 的 task,它们不可缓存,因为通常执行它们会更快。

可缓存的 task

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

以下输入以与 最新检查 相同的方式影响 task 的构建缓存键

  • task 类型及其类路径

  • 输出属性的名称

  • 根据 “自定义 task 类型” 部分的描述注释的属性的名称和值

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

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

  • 当构建脚本影响 task 执行时,构建脚本的内容

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

内置可缓存 task

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

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

某些 task,例如 CopyJar,通常不适合使其可缓存,因为 Gradle 只是将文件从一个位置复制到另一个位置。使不生成输出或没有 task 操作的 task 可缓存也没有意义。

第三方插件

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

声明 task 输入和输出

非常重要的是,可缓存的 task 要完整地了解其输入和输出,以便可以安全地在其他地方重用一个构建的结果。

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

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

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

为了处理 task 的易失性输入,请考虑 配置输入规范化

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

有些 task 从使用构建缓存中获益不多。一个例子是仅在文件系统周围移动数据的 task,例如 Copy task。您可以通过向 task 添加 @DisableCachingByDefault 注解来表明 task 不会被缓存。您还可以给出不默认缓存 task 的人类可读的原因。该注解可以单独使用,也可以与 @CacheableTask 一起使用。

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

启用不可缓存 task 的缓存

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

此 task 的输入和输出很容易弄清楚。输入是包含 JavaScript 文件的目录和 NPM 配置文件。输出是此 task 生成的捆绑包文件。

使用注解

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

如果可能,最好使用委托而不是创建子类。内置的 JavaExecExecCopySync task 就是这种情况,它们在 Project 上有一个方法来执行实际工作。

如果您是现代 JavaScript 开发人员,您就会知道捆绑可能需要很长时间,并且值得缓存。为了实现这一点,我们需要告诉 Gradle 允许缓存该 task 的输出,方法是使用 @CacheableTask 注解。

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

请注意,可以通过覆盖基类的 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 以启用 task 的缓存。

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

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

使用运行时 API

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

要为 task 启用缓存,您需要使用 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 支持 localremote 构建缓存,可以单独配置。当同时启用构建缓存时,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 执行此清理的频率以及条目保留多长时间可以通过 init 脚本配置,如 本节 中所示。

远程 HTTP 构建缓存

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

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

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 请求。根据 RFC 7231,所有其他重定向响应都将跟随 GET 请求,而没有条目有效负载作为正文。

网络错误处理

在建立 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
    }
}

配置用例

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

也可以从 init 脚本 配置构建缓存,可以从命令行使用,添加到您的 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 包括一个高性能、易于安装和操作的共享构建缓存后端。