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
的版本将是 20.0
,因为直接依赖项比 guice
依赖项更近。
此方法的主要缺点是它依赖于顺序。在非常大的图中保持顺序可能具有挑战性。例如,如果依赖项的新版本最终以与前一个版本不同的顺序排列其自己的依赖项声明,会怎样?
使用 Maven,这可能会对已解决的版本产生不良影响。
Apache Ivy是一个非常灵活的依赖项管理工具。它提供了自定义依赖项解析(包括冲突解决)的可能性。 这种灵活性是以难以推理为代价的。 |
Gradle 将考虑所有请求的版本,无论它们出现在依赖项图中的何处。在这些版本中,它将选择最高版本。有关版本排序的更多信息,请在此处查看。
正如你所见,Gradle 支持 丰富版本声明 的概念,那么最高版本取决于版本声明的方式
-
如果未涉及范围,则将选择未拒绝的最高版本。
-
如果声明为
strictly
的版本低于该版本,则选择将失败。
-
-
如果涉及范围
-
如果存在一个非范围版本,它落在指定的范围内或高于其上限,则将选择它。
-
如果只有范围,则选择将取决于范围的交集
-
如果所有范围相交,则将选择交集的最高现有版本。
-
如果所有范围之间没有明确的交集,则将从最高范围选择最高现有版本。如果最高范围没有可用版本,则解析将失败。
-
-
如果声明为
strictly
的版本低于该版本,则选择将失败。
-
请注意,在涉及范围的情况下,Gradle 需要元数据来确定哪些版本存在于所考虑的范围内。这会导致对元数据的中间查找,如 Gradle 如何检索依赖项元数据? 中所述。
限定符
在选择最高版本时,比较版本有一个需要注意的地方。所有 版本排序 规则仍然适用,但冲突解析器偏向于没有限定符的版本。
如果存在,版本的“限定符”是版本字符串的尾端,从其中找到的第一个非点分隔符开始。版本字符串的另一部分(第一部分)称为版本的“基本形式”。以下是一些说明性示例
原始版本 | 基本版本 | 限定符 |
---|---|---|
1.2.3 |
1.2.3 |
<none> |
1.2-3 |
1.2 |
3 |
1_alpha |
1 |
alpha |
abc |
abc |
<none> |
1.2b3 |
1.2 |
b3 |
abc.1+3 |
abc.1 |
3 |
b1-2-3.3 |
b |
1-2-3.3 |
如你所见,分隔符是 .
、-
、_
、+
字符中的任何一个,以及当版本的数字部分和非数字部分彼此相邻时的空字符串。
在解决竞争版本之间的冲突时,应用以下逻辑
-
首先选择具有最高基本版本的版本,其余版本将被丢弃
-
如果仍然有多个竞争版本,则会选择一个,优先选择没有限定符或具有发布状态的版本。
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 |
|
|
|
Gradle 7.6.2 |
|
|
|
Gradle 8.0 |
|
|
|
Gradle 8.1 |
|
|
|
Gradle 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 安装缓存用作只读缓存,因为此目录可能包含锁,并且可能被播种构建修改。
以编程方式访问解析结果
虽然大多数用户只需要访问“平面文件”列表,但在某些情况下,对图表进行推理并获取有关解析结果的更多信息会很有趣
-
对于需要依赖项图表模型的工具集成
-
对于生成依赖项图表的可视表示(图像、
.dot
文件,…)的任务 -
对于提供诊断(类似于
dependencyInsight
任务)的任务 -
对于需要在执行时执行依赖项解析的任务(例如,按需下载文件)
对于这些用例,Gradle 提供了惰性、线程安全的 API,可通过调用 Configuration.getIncoming() 方法访问
-
ResolutionResult API 可以访问已解析的依赖项图表,无论解析是否成功。
-
the artifacts API 提供对已解析工件的简单访问,未转换,但延迟下载工件(仅在需要时下载)。
-
the artifact view API 提供工件的高级过滤视图,可能 已转换。
有关如何在任务中使用结果的更多详细信息,请参阅 使用依赖关系解析结果 的文档。 |