想学习顶级工程团队用来保持构建速度和性能的技巧和窍门?在此注册参加我们的构建缓存培训。

概述

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

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

除了任务之外,工件转换也可以利用构建缓存,并类似于任务输出缓存来重用其输出。

要以动手的方式学习如何使用构建缓存,请先阅读构建缓存的用例以及后续部分。它涵盖了缓存可以改进的不同场景,并详细讨论了在为构建启用缓存时需要了解的不同注意事项。

启用构建缓存

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

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

Gradle 将仅在此构建中使用构建缓存。

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

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 是 Copy 类任务,它们不可缓存,因为通常执行它们更快。

可缓存的任务

由于任务描述了其所有输入和输出,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 支持一个 local 和一个 remote 构建缓存,可以分别配置。当两个构建缓存都启用时,Gradle 首先尝试从本地构建缓存加载构建输出,如果未找到构建输出,则尝试远程构建缓存。如果在远程缓存中找到输出,它们也会存储在本地缓存中,因此下次将在本地找到它们。Gradle 将构建输出存储(“推送”)到任何启用的构建缓存中,并且BuildCache.isPush() 设置为 true

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

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

内置本地构建缓存

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

Gradle 会定期清理本地缓存目录,通过删除最近未使用过的条目来节省磁盘空间。Gradle 执行此清理的频率是可配置的,如下面的示例所示。请注意,无论生成缓存条目的项目是什么,都会清理缓存条目。如果不同的项目将此清理配置为在不同的时间段运行,则最短的时间段将清理所有项目的缓存条目。因此,建议在 初始化脚本 中全局配置此设置。 配置用例 部分有一个将缓存配置放在初始化脚本中的示例。

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

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

远程 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
    }
}

配置用例

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

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