Gradle 如何处理冲突?
在进行依赖项解析时,Gradle 会处理两种类型的冲突:
- 版本冲突
-
即两个或多个依赖项需要给定依赖项但具有不同版本的情况。
- 实施冲突
-
也就是说,依赖关系图包含多个提供相同实现或 Gradle 术语中的功能的模块。
以下部分将详细解释 Gradle 如何尝试解决这些冲突。
依赖性解析过程是高度可定制的,可以满足企业需求。有关更多信息,请参阅控制传递依赖性一章。
版本冲突解决
当两个组件出现以下情况时,就会发生版本冲突:
-
依赖同一个模块,比方说
com.google.guava:guava
-
但在不同的版本上,我们可以
20.0
说25.1-android
-
我们的项目本身取决于
com.google.guava:guava:20.0
-
我们的项目还取决于
com.google.inject:guice:4.2.2
哪个本身取决于com.google.guava:guava:25.1-android
-
解决策略
鉴于上述冲突,有多种方法可以处理它,要么选择版本,要么失败解决。处理依赖关系管理的不同工具有不同的方法来处理这些类型的冲突。
Apache Maven使用最近优先策略。
Maven 将采用到依赖项的最短路径并使用该版本。如果有多条长度相同的路径,则第一个路径获胜。
这意味着在上面的示例中,guava
will 的版本是20.0
因为直接依赖关系比依赖关系更接近guice
。
该方法的主要缺点是它依赖于顺序。在非常大的图中保持顺序可能是一个挑战。例如,如果依赖项的新版本最终具有其自己的依赖项声明,其顺序与先前版本不同怎么办?
对于 Maven,这可能会对已解析的版本产生不必要的影响。
Apache Ivy是一个非常灵活的依赖管理工具。它提供了自定义依赖性解决方案的可能性,包括冲突解决方案。 这种灵活性的代价是难以推理。 |
Gradle 将考虑所有请求的版本,无论它们出现在依赖关系图中的任何位置。在这些版本中,它将选择最高的版本。有关版本订购的更多信息, 请参见此处。
正如您所看到的,Gradle 支持丰富版本声明的概念,因此最高版本取决于版本声明的方式:
-
如果不涉及范围,则选择未被拒绝的最高版本。
-
如果声明的版本
strictly
低于该版本,选择将失败。
-
-
如果涉及范围:
-
如果存在落在指定范围内或高于其上限的非范围版本,则会选择该版本。
-
如果只有范围,则选择将取决于范围的交集:
-
如果所有范围相交,则将选择交集的现有最高版本。
-
如果所有范围之间没有明确的交集,将从最高范围中选择现有的最高版本。如果没有适用于最高范围的版本,分辨率将会失败。
-
-
如果声明的版本
strictly
低于该版本,选择将失败。
-
请注意,在范围发挥作用的情况下,Gradle 需要元数据来确定所考虑的范围确实存在哪些版本。这会导致元数据的中间查找,如Gradle 如何检索依赖项元数据?中所述。 。
预选赛
在选择最高版本时,需要比较版本。所有版本排序规则仍然适用,但冲突解决程序偏向于没有限定符的版本。
版本的“限定符”(如果存在)是版本字符串的尾部,从其中找到的第一个非点分隔符开始。版本字符串的另一(第一)部分称为版本的“基本形式”。下面举一些例子来说明:
原版 | 基础版本 | 预选赛 |
---|---|---|
1.2.3 |
1.2.3 |
<无> |
1.2-3 |
1.2 |
3 |
1_阿尔法 |
1 |
α |
ABC |
ABC |
<无> |
1.2b3 |
1.2 |
b3 |
abc.1+3 |
abc.1 |
3 |
b1-2-3.3 |
乙 |
1-2-3.3 |
正如您所看到的,分隔符是任何.
, -
, _
,+
字符,加上版本的数字和非数字部分彼此相邻时的空字符串。
解决竞争版本之间的冲突时,适用以下逻辑:
-
首先选择具有最高基础版本的版本,其余的被丢弃
-
如果仍然存在多个竞争版本,则优先选择没有资格赛或具有发布状态的版本。
实施冲突解决
Gradle 使用变体和功能来识别模块提供的内容。
这是一个独特的功能,值得单独用一章来理解它的含义和功能。
当两个模块出现以下任一情况时就会发生冲突:
-
尝试选择不兼容的变体,
-
声明相同的能力
在“在候选人之间进行选择”中了解有关处理此类冲突的更多信息。
Gradle 如何检索依赖元数据?
Gradle 需要有关依赖关系图中包含的模块的元数据。需要这些信息的主要有两点:
-
当声明的版本是动态的时,确定模块的现有版本。
-
确定给定版本的模块的依赖关系。
发现版本
面对动态版本,Gradle需要识别具体的匹配版本:
-
检查每个存储库,Gradle 不会在第一个返回一些元数据时停止。当定义多个时,将按照添加的顺序检查它们。
-
对于 Maven 存储库,Gradle 将使用它
maven-metadata.xml
提供有关可用版本的信息。 -
对于 Ivy 存储库,Gradle 将诉诸目录列表。
此过程会产生候选版本列表,然后将其与所表达的动态版本进行匹配。至此,版本冲突解决重新开始。
请注意,Gradle 会缓存版本信息,更多信息可以在控制动态版本缓存部分中找到。
获取模块元数据
给定所需的依赖项和版本,Gradle 尝试通过搜索依赖项指向的模块来解决依赖项。
-
每个存储库都按顺序进行检查。
-
根据存储库的类型,Gradle 查找描述模块的元数据文件(或
.module
文件)或直接查找工件文件。.pom
ivy.xml
-
具有模块元数据文件(或文件)的模块
.module
优先于仅具有工件文件的模块.pom
。ivy.xml
-
一旦存储库返回元数据结果,以下存储库将被忽略。
-
-
检索并解析依赖项的元数据(如果找到)
-
如果模块元数据是声明了父 POM 的 POM 文件,Gradle 将递归尝试解析 POM 的每个父模块。
-
-
然后,从在上述过程中选择的同一存储库请求该模块的所有工件。
-
然后,所有这些数据(包括存储库源和潜在的缺失)都存储在依赖项缓存中。
上面的倒数第二点可能会导致与Maven Local的集成出现问题。由于它是 Maven 的缓存,因此有时会丢失给定模块的一些工件。如果 Gradle 从 Maven Local 采购这样的模块,它将认为丢失的工件完全丢失。 |
依赖缓存
Gradle 包含高度复杂的依赖项缓存机制,该机制旨在最大限度地减少依赖项解析中发出的远程请求的数量,同时努力保证依赖项解析的结果是正确且可重现的。
Gradle 依赖项缓存由两种存储类型组成,位于:$GRADLE_USER_HOME/caches
-
基于文件的下载工件存储,包括 jar 等二进制文件以及 POM 文件和 Ivy 文件等原始下载元数据。下载工件的存储路径包含 SHA1 校验和,这意味着可以轻松缓存 2 个具有相同名称但内容不同的工件。
-
已解析模块元数据的二进制存储,包括解析动态版本、模块描述符和工件的结果。
Gradle 缓存不允许本地缓存隐藏问题并创建其他神秘且难以调试的行为。 Gradle 支持可靠且可重复的企业构建,重点关注带宽和存储效率。
独立的元数据缓存
Gradle 在元数据缓存中以二进制格式保存依赖关系解析的各个方面的记录。元数据缓存中存储的信息包括:
-
1.+
将动态版本(例如)解析为具体版本(例如)的结果1.2
。 -
特定模块的已解析模块元数据,包括模块工件和模块依赖项。
-
特定工件的解析工件元数据,包括指向下载的工件文件的指针。
-
特定存储库中缺少特定模块或工件,从而消除了重复尝试访问不存在的资源的情况。
元数据缓存中的每个条目都包含提供信息的存储库的记录以及可用于缓存到期的时间戳。
存储库缓存是独立的
如上所述,每个存储库都有一个单独的元数据缓存。存储库由其 URL、类型和布局来标识。如果之前未从该存储库解析某个模块或工件,Gradle 将尝试根据该存储库解析该模块。这始终涉及对存储库的远程查找,但在许多情况下不需要下载。
如果所需的工件在构建指定的任何存储库中不可用,即使本地缓存具有从不同存储库检索的该工件的副本,依赖关系解析也会失败。存储库独立性允许构建以以前没有构建工具做过的高级方式彼此隔离。这是创建在任何环境中可靠且可重现的构建的关键功能。
工件复用
在下载工件之前,Gradle 尝试通过下载与该工件关联的 sha 文件来确定所需工件的校验和。如果可以检索校验和,并且已存在具有相同 ID 和校验和的工件,则不会下载工件。如果无法从远程服务器检索校验和,则将下载工件(如果与现有工件匹配,则忽略该工件)。
除了考虑从不同存储库下载的工件之外,Gradle 还将尝试重用本地 Maven 存储库中找到的工件。如果 Maven 下载了候选工件,并且可以验证该工件是否与远程服务器声明的校验和相匹配,则 Gradle 将使用该工件。
基于校验和的存储
不同的存储库可以响应相同的工件标识符来提供不同的二进制工件。 Maven SNAPSHOT 工件经常出现这种情况,但对于任何在不更改其标识符的情况下重新发布的工件也可能如此。通过根据 SHA1 校验和缓存工件,Gradle 能够维护同一工件的多个版本。这意味着当针对一个存储库进行解析时,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 |
|
|
|
摇篮7.6.2 |
|
|
|
摇篮8.0 |
|
|
|
摇篮8.1 |
|
|
|
等级 8.2 及以上 |
与其他 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 依赖项缓存,然后将其复制到另一个目录。然后,该目录可以用作其他构建的只读缓存。您不应该使用现有的 Gradle 安装缓存作为只读缓存,因为该目录可能包含锁并且可能会被种子构建修改。