Gradle 包含一个非常复杂的依赖缓存机制,该机制旨在最大限度地减少依赖解析中发出的远程请求数量,同时努力保证依赖解析的结果是正确且可重现的。
-
本地缓存:Gradle 在本地缓存依赖项以避免重复下载。缓存位于用户主文件夹下的
.gradle
目录中(例如,~/.gradle/caches/modules-2
)。当请求依赖项时,Gradle 首先检查此本地缓存,然后再尝试从远程仓库获取它。 -
更改依赖项:默认情况下,Gradle 对标记为“changing”(例如,SNAPSHOT 或动态依赖项)的依赖项进行不同的处理,并更频繁地刷新它们。可以以编程方式更改这些依赖项的缓存时间。
-
离线模式:Gradle 可以在离线模式下运行,仅使用缓存的依赖项,而无需尝试从远程仓库下载任何内容。您可以使用
--offline
标志启用离线模式,确保您的构建仅使用缓存的构件。 -
刷新依赖项:要强制 Gradle 更新其依赖项,请使用
--refresh-dependencies
标志。此选项指示 Gradle 绕过缓存并检查远程仓库中更新的构件。Gradle 会下载它们,但仅当检测到更改时才会下载,并使用哈希来避免不必要的下载。
1. 依赖缓存
Gradle 依赖缓存由位于 $GRADLE_USER_HOME/caches
下的两种存储类型组成
-
基于文件的已下载构件存储,包括像 jars 这样的二进制文件以及像 POM 文件和 Ivy 文件这样的原始下载元数据。构件存储在校验和下,因此名称冲突不会引起问题。
-
已解析模块元数据的二进制存储,包括解析动态版本、模块描述符和构件的结果。
独立的元数据缓存
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 跟踪依赖缓存中哪些构件被访问。 基于此信息,定期扫描缓存(不超过每 24 小时一次),以识别超过 30 天未使用过的构件。 然后删除这些过时的构件,以防止缓存无限增长。
您可以在Gradle 管理的目录中了解有关缓存清理的更多信息。
2. 更改依赖项
Gradle 将标记为“changing”(例如 SNAPSHOT 依赖项)的依赖项与常规依赖项区别对待,更频繁地刷新它们,以确保您始终使用最新版本。
要将依赖项声明为 changing,您可以在依赖项声明中设置 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 会将这些依赖项(包括动态版本和 changing 模块)缓存 24 小时,这意味着在此期间它不会联系远程仓库以获取新版本。
要让 Gradle 更频繁地或在每次构建时检查较新版本,您可以相应地调整缓存阈值或生存时间 (TTL) 设置。
对于动态或 changing 版本,使用较短的 TTL 阈值可能会导致构建时间更长,因为远程仓库访问次数增加。 |
您可以使用配置的 ResolutionStrategy 以编程方式微调缓存的某些方面。 如果您想永久更改设置,则编程方法很有用。
要更改 Gradle 缓存动态版本的已解析版本的时间,请使用
configurations.all {
resolutionStrategy.cacheDynamicVersionsFor(10, "minutes")
}
configurations.all {
resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes'
}
要更改 Gradle 缓存 changing 模块的元数据和构件的时间,请使用
configurations.all {
resolutionStrategy.cacheChangingModulesFor(4, "hours")
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor 4, 'hours'
}
3. 使用离线模式
--offline
命令行开关指示 Gradle 从缓存中使用依赖模块,无论它们是否应该再次检查。 当以 offline
模式运行时,Gradle 将不会尝试访问网络进行依赖解析。 如果所需的模块不在依赖缓存中,构建将失败。
4. 强制刷新依赖项
您可以从命令行控制特定构建调用的依赖缓存行为。 命令行选项有助于为单个构建执行做出有选择性的临时选择。
有时,Gradle 依赖缓存可能与配置仓库的实际状态不同步。 也许仓库最初配置错误,或者可能错误地发布了“non-changing”模块。 要刷新依赖缓存中的所有依赖项,请在命令行中使用 --refresh-dependencies
选项。
--refresh-dependencies
选项告诉 Gradle 忽略已解析模块和构件的所有缓存条目。 将针对所有配置的仓库执行新的解析,重新计算动态版本,刷新模块并下载构件。 但是,在可能的情况下,Gradle 将在再次下载之前检查先前下载的构件是否有效。 这是通过比较仓库中发布的校验和值与现有下载构件的校验和值来完成的。
刷新依赖项将导致 Gradle 使其列表缓存无效。 然而
-
它将对元数据文件执行 HTTP HEAD 请求,但如果它们相同,则不会重新下载它们
-
它将对构件文件执行 HTTP HEAD 请求,但如果它们相同,则不会重新下载它们
换句话说,只有在您实际使用动态依赖项或您有未意识到的 changing 依赖项时,刷新依赖项才会产生影响(在这种情况下,您有责任将它们正确地声明为 Gradle 的 changing 依赖项)。
认为使用 --refresh-dependencies
将强制下载依赖项是一种常见的误解。 情况并非如此:Gradle 只会执行刷新动态依赖项严格要求的操作。 这可能涉及下载新列表、元数据文件甚至构件,但如果没有任何更改,则影响最小。
处理临时构建
在临时容器中运行构建是一种常见的做法。 容器通常仅生成以执行单个构建,然后在销毁之前执行。 当构建依赖于大量每个容器都必须重新下载的依赖项时,这可能会成为一个实际问题。 为了帮助解决这种情况,Gradle 提供了几个选项
-
将依赖缓存复制到每个容器中
复制和重用缓存
依赖缓存,包括文件部分和元数据部分,都完全使用相对路径编码。 这意味着完全有可能复制缓存并在周围查看 Gradle 从中受益。
可以复制的路径是 $GRADLE_USER_HOME/caches/modules-<version>
。 唯一的约束是在目标位置使用相同的结构放置它,其中 GRADLE_USER_HOME
的值可能不同。
如果 *.lock
或 gc.properties
文件存在,请勿复制它们。
请注意,创建缓存和使用缓存应使用兼容的 Gradle 版本完成,如下表所示。 否则,构建可能仍然需要与远程仓库进行一些交互才能完成缺少的信息,这些信息可能在不同的版本中可用。 如果使用了多个不兼容的 Gradle 版本,则在播种缓存时应全部使用。
模块缓存版本 | 文件缓存版本 | 元数据缓存版本 | Gradle 版本 |
---|---|---|---|
|
|
|
Gradle 6.1 到 Gradle 6.3 |
|
|
|
Gradle 6.4 到 Gradle 6.7 |
|
|
|
Gradle 6.8 到 Gradle 7.4 |
|
|
|
Gradle 7.5 到 Gradle 7.6.1 |
|
|
|
Gradle 7.6.2 |
|
|
|
Gradle 8.0 |
|
|
|
Gradle 8.1 |
|
|
|
Gradle 8.2 到 Gradle 8.10.2 |
|
|
|
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 安装缓存作为只读缓存,因为此目录可能包含锁并且可能被播种构建修改。