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

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

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

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

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

1. 依赖缓存

Gradle 依赖缓存包含两种存储类型,位于 $GRADLE_USER_HOME/caches 目录下

  1. 一种基于文件的存储,用于存放下载的 artifact,包括 jars 等二进制文件以及 POM 文件和 Ivy 文件等原始下载的元数据。Artifact 按校验和存储,因此名称冲突不会导致问题。

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

独立的元数据缓存

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

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

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

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

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

  • 特定仓库中特定模块或 artifact 的不存在记录,从而避免重复尝试访问不存在的资源。

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

仓库缓存是独立的

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

如果模块或 artifact 之前尚未从此仓库解析过,Gradle 将尝试对照此仓库解析该模块。这总是涉及对仓库的远程查找,但在许多情况下不需要下载

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

仓库独立性使得构建之间可以相互隔离。这是创建在任何环境中都可靠和可重现的构建的关键特性。

Artifact 重用

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

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

Gradle 还尝试重用本地 Maven 仓库中的 artifact。如果 Maven 之前下载的 artifact 与之匹配,并且可以通过远程服务器的校验和进行验证,Gradle 将使用它。

基于校验和的存储

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

这通常发生在 Maven SNAPSHOT artifact 上,但也适用于任何在不更改其标识符的情况下重新发布的 artifact。通过基于校验和缓存 artifact,Gradle 能够维护同一 artifact 的多个版本。这意味着在对照一个仓库进行解析时,Gradle 永远不会覆盖来自不同仓库的缓存 artifact 文件。这样做无需为每个仓库设置单独的 artifact 文件存储。

缓存锁定

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

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

缓存清理

Gradle 会跟踪依赖缓存中哪些 artifact 被访问。基于这些信息,缓存会定期扫描(每 24 小时不超过一次),以识别超过 30 天未使用的 artifact。然后删除这些过时的 artifact,以防止缓存无限增长。

您可以在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 缓存可变模块的元数据和 artifact 的时间长度,请使用

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 忽略已解析模块和 artifact 的所有缓存条目。将对所有配置的仓库执行全新解析,重新计算动态版本,刷新模块,并下载 artifact。但是,在可能的情况下,Gradle 会在再次下载之前检查先前下载的 artifact 是否有效。这是通过比较仓库中发布的校验和值与现有已下载 artifact 的校验和值来完成的。

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

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

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

换句话说,刷新依赖仅当您实际使用动态依赖项您有未察觉的可变依赖项(在这种情况下,您有责任将其正确地声明为 Gradle 的可变依赖项)时才会有影响。

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

处理短暂构建环境

在短暂的容器中运行构建是常见的做法。容器通常被生成只执行一个构建,然后就被销毁。当构建依赖于大量依赖项而每个容器都必须重新下载这些依赖项时,这可能成为一个实际问题。为了帮助解决这种情况,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 用户主目录中的可写缓存和共享只读缓存中查找依赖项(artifact 或元数据)。如果只读缓存中存在依赖项,则不会下载。如果只读缓存中缺少依赖项,则会下载并添加到可写缓存中。实际上,这意味着可写缓存将只包含在只读缓存中不可用的依赖项。

只读缓存应来源于已包含部分所需依赖项的 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 安装缓存作为只读缓存,因为此目录可能包含锁,并且可能被种子构建修改。