想要了解顶级工程团队用来保持快速构建和高性能的提示和技巧吗?在此注册我们的构建缓存培训。

概述

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

可缓存的任务

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

以下输入以与最新检查相同的方式为任务提供构建缓存键:

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

内置可缓存任务

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

所有其他内置任务当前均不可缓存。

某些任务(例如CopyJar)通常没有意义进行缓存,因为 Gradle 仅将文件从一个位置复制到另一个位置。让不产生输出或没有任务操作的任务可缓存也是没有意义的。

第三方插件

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

声明任务输入和输出

可缓存任务拥有其输入和输出的完整图片非常重要,以便可以在其他地方安全地重用一次构建的结果。

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

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

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

为了处理任务的不稳定输入,请考虑配置输入规范化

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

有些任务不会从使用构建缓存中受益。一个示例是仅在文件系统中移动数据的任务,就像任务一样Copy。您可以通过向任务添加注释来表示不缓存该任务@DisableCachingByDefault。您还可以给出默认情况下不缓存任务的人类可读的原因。该注释可以单独使用,也可以与 一起使用@CacheableTask

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

启用不可缓存任务的缓存

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

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

使用注释

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

如果可能,最好使用委托而不是创建子类。内置 、 和 任务就是这种情况JavaExecExec它们有一个方法来Copy完成实际工作。SyncProject

如果您是一名现代 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")
}

配置构建缓存

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

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

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

内置本地构建缓存

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

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

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/'
    }
}

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

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

指定访问凭据

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服务器在仅重定向请求时必须小心307,并且308重定向响应将跟随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 期望继续

可以启用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包括一个高性能、易于安装和操作的共享构建缓存后端。