概述

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

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

除了任务外,制品转换也可以利用构建缓存并重用其输出项,其方式类似于任务输出缓存。

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

启用构建缓存

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

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

Gradle 将仅对本次构建使用构建缓存。

在您的 gradle.properties 中添加 org.gradle.caching=true

除非使用 --no-build-cache 明确禁用,否则 Gradle 将尝试对所有构建重用先前构建的输出项。

启用构建缓存后,它会将构建输出项存储在 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 plugin 3.1+Kotlin plugin 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 开发人员,您知道打包可能需要相当长的时间,并且值得缓存。为了实现这一点,我们需要通过使用 @CacheableTask 注解来告诉 Gradle 允许缓存该任务的输出项。

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

请注意,可以通过覆盖基类的 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 支持 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
    }
}

配置用例

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

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