Gradle 包含一个高度复杂的依赖缓存机制,旨在最大限度地减少依赖解析中的远程请求数量,同时努力确保依赖解析的结果正确且可重现。

  1. 本地缓存:Gradle 在本地缓存依赖项以避免重复下载。缓存位于用户主文件夹下的 .gradle 目录中(例如,~/.gradle/caches/modules-2)。当请求一个依赖项时,Gradle 会首先检查此本地缓存,然后才尝试从远程仓库获取它。

  2. 更改依赖项:默认情况下,Gradle 会区别对待标记为“可变”的依赖项(例如,SNAPSHOT 或动态依赖项),并更频繁地刷新它们。这些依赖项的缓存时间可以编程方式更改。

  3. 离线模式:Gradle 可以在离线模式下运行,只使用缓存的依赖项,而不尝试从远程仓库下载任何内容。您可以使用 --offline 标志启用离线模式,确保您的构建仅使用缓存的工件。

  4. 刷新依赖项:要强制 Gradle 更新其依赖项,请使用 --refresh-dependencies 标志。此选项指示 Gradle 绕过缓存并检查远程仓库中更新的工件。Gradle 会下载它们,但只有在检测到更改时才会下载,并使用哈希值避免不必要的下载。

1. 依赖缓存

Gradle 依赖缓存由位于 $GRADLE_USER_HOME/caches 下的两种存储类型组成

  1. 一个基于文件的下载工件存储,包括像 jar 这样的二进制文件以及像 POM 文件和 Ivy 文件这样的原始下载元数据。工件以校验和存储,因此名称冲突不会导致问题。

  2. 一个已解析模块元数据的二进制存储,包括解析动态版本、模块描述符和工件的结果。

单独的元数据缓存

Gradle 以二进制格式在元数据缓存中记录依赖解析的各个方面。

元数据缓存中存储的信息包括

  • 将动态版本(例如 1.+)解析为具体版本(例如 1.2)的结果。

  • 特定模块的已解析模块元数据,包括模块工件和模块依赖项。

  • 特定工件的已解析工件元数据,包括指向已下载工件文件的指针。

  • 特定仓库中特定模块或工件的缺失,从而避免重复尝试访问不存在的资源。

元数据缓存中的每个条目都包含提供信息的仓库记录以及可用于缓存过期的时间戳。

仓库缓存是独立的

如上所述,每个仓库都有一个单独的元数据缓存。仓库由其 URL、类型和布局标识。

如果模块或工件以前未从此仓库解析,Gradle 将尝试针对该仓库解析该模块。这将始终涉及对仓库的远程查找,但在许多情况下不需要下载

如果所需工件在最初解析它们的仓库中不可用,依赖解析将失败。一旦从特定仓库解析,工件就会变得“粘性”,这意味着 Gradle 将避免从其他仓库解析它们,以防止工件源中出现意外或潜在不安全的更改。这确保了跨环境的一致性,但如果机器之间的仓库不同,也可能导致失败。

仓库独立性允许构建彼此隔离。这是创建在任何环境中都可靠和可重现的构建的关键特性。

工件重用

在下载工件之前,Gradle 尝试通过下载关联的 .sha512.sha256.sha1.md5 文件(按顺序尝试每个文件)来检索工件的校验和。

如果校验和可用,并且具有相同 ID 和校验和的工件已存在,Gradle 将跳过下载。但是,如果无法从远程服务器检索校验和,Gradle 将继续下载工件,但如果它与现有工件匹配,则会忽略它。

Gradle 还尝试重用本地 Maven 仓库中的工件。如果 Maven 以前下载的工件匹配,Gradle 将使用它,前提是可以根据远程服务器的校验和进行验证。

基于校验和的存储

不同的仓库可能会针对相同的工件标识符提供不同的二进制工件。

Maven SNAPSHOT 工件通常是这种情况,但对于任何未经更改标识符而重新发布的工件也可能是如此。通过根据其校验和缓存工件,Gradle 能够维护相同工件的多个版本。这意味着当针对一个仓库解析时,Gradle 绝不会覆盖来自不同仓库的缓存工件文件。这无需为每个仓库单独的文件存储。

缓存锁定

Gradle 依赖缓存使用基于文件的锁定来确保它可以安全地被多个 Gradle 进程并发使用。只要正在读取或写入二进制元数据存储,就会持有锁,但在下载远程工件等慢速操作时会释放锁。

仅当不同的 Gradle 进程可以相互通信时才支持这种并发访问。对于容器化构建,这通常不是这种情况

缓存清理

Gradle 会跟踪依赖缓存中访问的工件。根据此信息,缓存会定期扫描(每天不超过一次),以识别 30 多天未使用的工件。然后删除这些过时工件,以防止缓存无限增长。

您可以在Gradle 管理的目录中了解有关缓存清理的更多信息。

2. 更改依赖

Gradle 会区别对待标记为“可变”的依赖项(例如 SNAPSHOT 依赖项),并更频繁地刷新它们,以确保您始终使用最新版本。

要将依赖项声明为可变,您可以在依赖项声明中设置 changing = true 属性。这对于预期频繁更改而没有新版本号的依赖项很有用

dependencies {
    implementation("com.example:some-library:1.0-SNAPSHOT") // Automatically gets treated as changing
    implementation("com.example:my-library:1.0") {  // Must be explicitly set as changing
        changing = true
    }
}

缓存可变依赖项

默认情况下,Gradle 会缓存这些依赖项(包括动态版本和可变模块)24 小时,这意味着在此期间它不会联系远程仓库以获取新版本。

要让 Gradle 更频繁地检查新版本或在每次构建时检查,您可以相应地调整缓存阈值或生存时间 (TTL) 设置。

对动态或可变版本使用短 TTL 阈值可能会由于增加远程仓库访问而导致构建时间更长。

您可以使用配置的 ResolutionStrategy 以编程方式微调缓存的某些方面。如果您想永久更改设置,编程方法很有用。

要更改 Gradle 将动态版本的已解析版本缓存多长时间,请使用

build.gradle.kts
configurations.all {
    resolutionStrategy.cacheDynamicVersionsFor(10, "minutes")
}
build.gradle
configurations.all {
    resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes'
}

要更改 Gradle 将可变模块的元数据和工件缓存多长时间,请使用

build.gradle.kts
configurations.all {
    resolutionStrategy.cacheChangingModulesFor(4, "hours")
}
build.gradle
configurations.all {
    resolutionStrategy.cacheChangingModulesFor 4, 'hours'
}

3. 使用离线模式

--offline 命令行开关指示 Gradle 使用缓存中的依赖模块,无论它们是否需要再次检查。在运行 offline 时,Gradle 不会尝试访问网络进行依赖解析。如果所需的模块不在依赖缓存中,构建将失败。

4. 强制刷新依赖

您可以从命令行控制特定构建调用的依赖缓存行为。命令行选项有助于为单个构建执行做出选择性的临时选择。

有时,Gradle 依赖缓存可能会与配置的仓库的实际状态不同步。也许仓库最初配置错误,或者“不可变”模块发布不正确。要刷新依赖缓存中的所有依赖项,请在命令行上使用 --refresh-dependencies 选项。

--refresh-dependencies 选项告诉 Gradle 忽略已解析模块和工件的所有缓存条目。将对所有配置的仓库执行新的解析,并重新计算动态版本,刷新模块并下载工件。但是,在可能的情况下,Gradle 会在再次下载之前检查以前下载的工件是否有效。这是通过比较仓库中发布的校验和值与现有下载工件的校验和值来完成的。

刷新依赖项将导致 Gradle 使其列表缓存失效。但是

  • 它将对元数据文件执行 HTTP HEAD 请求,但如果它们相同,则不会重新下载它们

  • 它将对工件文件执行 HTTP HEAD 请求,但如果它们相同,则不会重新下载它们

换句话说,刷新依赖项在您实际使用动态依赖项您有您不知道的可变依赖项(在这种情况下,您有责任向 Gradle 正确声明它们为可变依赖项)时才有效。

认为使用 --refresh-dependencies 会强制下载依赖项是一个常见的误解。情况并非如此:Gradle 只会执行严格需要刷新动态依赖项的操作。这可能涉及下载新的列表、元数据文件甚至工件,但如果没有任何更改,影响是最小的。

处理临时构建

在临时容器中运行构建是一种常见的做法。容器通常只为执行单个构建而生成,然后被销毁。当构建依赖于大量依赖项时,每个容器都必须重新下载这些依赖项,这可能成为一个实际问题。为了解决这种情况,Gradle 提供了几个选项

复制和重用缓存

依赖缓存,包括文件和元数据部分,都使用相对路径完全编码。这意味着完全有可能复制缓存并看到 Gradle 从中受益。

可以复制的路径是 $GRADLE_USER_HOME/caches/modules-<version>。唯一的限制是使用相同的结构放置在目标位置,其中 GRADLE_USER_HOME 的值可以不同。

如果存在 *.lockgc.properties 文件,请勿复制它们。

请注意,创建和使用缓存应使用兼容的 Gradle 版本完成,如下表所示。否则,构建可能仍然需要与远程仓库进行一些交互以完成缺失信息,这些信息可能在不同版本中可用。如果存在多个不兼容的 Gradle 版本,则在播种缓存时应使用所有版本。

模块缓存版本 文件缓存版本 元数据缓存版本 Gradle 版本

modules-2

files-2.1

metadata-2.95

Gradle 6.1 到 Gradle 6.3

modules-2

files-2.1

metadata-2.96

Gradle 6.4 到 Gradle 6.7

modules-2

files-2.1

metadata-2.97

Gradle 6.8 到 Gradle 7.4

modules-2

files-2.1

metadata-2.99

Gradle 7.5 到 Gradle 7.6.1

modules-2

files-2.1

metadata-2.101

Gradle 7.6.2

modules-2

files-2.1

metadata-2.100

Gradle 8.0

modules-2

files-2.1

metadata-2.105

Gradle 8.1

modules-2

files-2.1

metadata-2.106

Gradle 8.2 到 Gradle 8.10.2

modules-2

files-2.1

metadata-2.107

Gradle 8.11 及以上

与其他 Gradle 实例共享依赖缓存

除了将依赖缓存复制到每个容器中之外,还可以挂载一个共享的只读目录,该目录将充当所有容器的依赖缓存。此缓存与经典的依赖缓存不同,它在没有锁定的情况下访问,使得多个构建可以并发地从缓存中读取。重要的是,当其他构建可能正在从只读缓存中读取时,不应对其进行写入。

使用共享只读缓存时,Gradle 会在本地 Gradle 用户主目录中的可写缓存和共享只读缓存中查找依赖项(工件或元数据)。如果依赖项存在于只读缓存中,则不会下载。如果只读缓存中缺少依赖项,则会下载并将其添加到可写缓存中。实际上,这意味着可写缓存将只包含只读缓存中不可用的依赖项。

只读缓存应源自已包含某些所需依赖项的 Gradle 依赖缓存。缓存可以不完整;但是,空的共享缓存只会增加开销。

共享只读依赖缓存是一项孵化功能。

使用共享依赖缓存的第一步是通过复制现有本地缓存来创建它。为此,您需要按照上面的说明进行操作。

然后设置 GRADLE_RO_DEP_CACHE 环境变量以指向包含缓存的目录

$GRADLE_RO_DEP_CACHE
   |-- modules-2 : the read-only dependency cache, should be mounted with read-only privileges

$GRADLE_HOME
   |-- caches
         |-- modules-2 : the container specific dependency cache, should be writable
         |-- ...
   |-- ...

在 CI 环境中,最好有一个构建“播种”Gradle 依赖缓存,然后将其复制到不同的目录或分发,例如作为 Docker 卷。然后,此目录可以用作其他构建的只读缓存。您不应使用现有的 Gradle 安装缓存作为只读缓存,因为此目录可能包含锁并且可能被播种构建修改。