本章介绍了Gradle 内部依赖项解析的工作方式. 在介绍了如何声明存储库依赖项之后 ,有必要解释在依赖项解析期间这些声明如何组合在一起.

依赖性解析是一个由两个阶段组成的过程,将重复执行两个阶段,直到完成依赖性图为止:

  • 将新的依存关系添加到图后,请执行冲突解决方案以确定应将哪个版本添加到图.

  • 当特定的依赖关系(即具有版本的模块)被标识为图形的一部分时,请检索其元数据,以便可以依次添加其依赖关系.

以下部分将描述Gradle识别为冲突的内容以及如何自动解决冲突. 之后,将介绍元数据的检索,解释Gradle如何遵循依赖关系链接 .

How Gradle handles conflicts?

执行依赖关系解析时,Gradle处理两种类型的冲突:

Version conflicts

那是当两个或多个依赖项需要给定的依赖项但版本不同时.

Implementation conflicts

那就是当依赖图包含在Gradle术语中提供相同实现或功能的模块时.

以下各节将详细说明Gradle如何尝试解决这些冲突.

依赖性解析过程可以高度自定义以满足企业需求. 有关更多信息,请参见控制传递依赖关系一章.

Version conflict resolution

当两个组件发生版本冲突:

  • 取决于同一模块,假设com.google.guava:guava

  • 但是在不同的版本上,假设20.025.1-android

    • 我们的项目本身取决于com.google.guava:guava:20.0

    • 我们的项目还依赖com.google.inject:guice:4.2.2 ,而com.google.inject:guice:4.2.2本身依赖com.google.guava:guava:25.1-android

Resolution strategy

鉴于上述冲突,可以通过选择版本或通过失败的解决方法来解决. 处理依赖性管理的不同工具具有处理此类冲突的不同方法.

Apache Maven使用最接近的优先策略.

Maven将采用最短路径获得依赖关系并使用该版本. 如果有多个相同长度的路径,则第一个获胜.

这意味着在上面的示例中, guava的版本将为20.0因为直接依赖关系guice依赖关系更紧密 .

这种方法的主要缺点是依赖于顺序. 在很大的图中保持顺序可能是一个挑战. 例如,如果新版本的依赖项最终以不同于先前版本的顺序拥有其自己的依赖项声明,该怎么办?

使用Maven,这可能会对已解决的版本产生不良影响.

Apache Ivy是一种非常灵活的依赖项管理工具. 它提供了自定义依赖关系解决方案(包括冲突解决方案)的可能性.

这种灵活性伴随着难以推理的代价.

Gradle将考虑所有请求的版本,无论它们出现在依赖关系图中的何处. 在这些版本中,它将选择最高的版本.

如您所见,Gradle支持丰富版本声明的概念,因此最高版本取决于版本声明的方式:

  • 如果不涉及范围,则将选择不被拒绝的最高版本.

    • 如果声明为strictly低于该版本的版本,则选择将失败.

  • If ranges are involved:

    • 如果有非范围版本落入指定范围内或高于其上限,则将选择该版本.

    • 如果仅存在范围,则将选择具有最高上限的范围的现有最高版本.

    • 如果声明为strictly低于该版本的版本,则选择将失败.

请注意,在范围起作用的情况下,Gradle需要元数据来确定对于所考虑范围确实存在哪些版本. 这将导致对元数据的中间查找,如Gradle如何检索依​​赖元数据? .

Implementation conflict resolution

Gradle使用变体和功能来识别模块提供的功能 .

这是一个独特的功能,值得一章以了解其含义和功能.

两个模块之一发生冲突时:

  • 尝试选择不兼容的变体,

  • 声明相同的功能

在" 选择候选人"中了解有关处理此类冲突的更多信息.

How Gradle retrieves dependency metadata?

Gradle需要有关依赖关系图中包含的模块的元数据. 该信息是两点所必需的:

  • 当声明的版本是动态的时,确定模块的现有版本.

  • 确定给定版本的模块依赖性.

Discovering versions

面对动态版本,Gradle需要确定具体的匹配版本:

  • 检查每个存储库,Gradle不会在第一个返回某些元数据的站点上停止. 定义多个时,将按照添加顺序对其进行检查.

  • 对于Maven存储库,Gradle将使用maven-metadata.xml提供有关可用版本的信息.

  • 对于常春藤存储库,Gradle将诉诸目录列表.

此过程将生成候选版本列表,这些候选版本然后与表示的动态版本匹配. 此时,将恢复版本冲突解决 .

请注意,Gradle会缓存版本信息,有关更多信息,请参见控制动态版本缓存一节.

Obtaining module metadata

给定所需的依赖关系(带有版本),Gradle尝试通过搜索依赖关系指向的模块来解决依赖关系.

  • 依次检查每个存储库.

    • 根据存储库的类型,Gradle会查找描述模块的元数据文件( .module.pomivy.xml文件)或直接查找工件文件.

    • 具有模块元数据文件( .module.pomivy.xml文件)的模块优于仅具有工件文件的模块.

    • 存储库返回元数据结果后,以下存储库将被忽略.

  • 如果找到依赖项的元数据,则将对其进行检索和解析

    • 如果模块元数据是声明了父POM的POM文件,则Gradle将递归地尝试为POM解析每个父模块.

  • 然后,从上述过程中选择的同一存储库中请求模块的所有工件.

  • 然后,所有这些数据(包括存储库源和潜在的丢失)都存储在Dependency Cache中 .

上面的倒数第二点是使与Maven Local集成的问题. 由于它是Maven的缓存,因此有时会丢失给定模块的某些工件. 如果Gradle从Maven Local采购了这样的模块,它将认为丢失的工件完全丢失.

Repository disabling

当Gradle无法从存储库中检索信息时,它将在构建期间禁用它,并使所有依赖项解析失败.

最后一点对于可重复性很重要. 如果允许继续构建而忽略有问题的存储库,则一旦存储库重新联机,后续的构建可能会有不同的结果.

HTTP Retries

在禁用某个存储库之前,Gradle将进行几次尝试. 如果连接失败,Gradle将重试某些可能会被瞬态发生的错误,从而增加每次重试之间的等待时间.

当由于永久错误或达到最大重试次数而无法联系存储库时,就会将其列入黑名单.

The Dependency Cache

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

The Gradle dependency cache consists of two storage types located under GRADLE_USER_HOME/caches:

  • 基于文件的下载工件的存储,包括二进制文件(如jars)以及原始下载的元数据(如POM文件和Ivy文件). 下载的工件的存储路径包括SHA1校验和,这意味着可以轻松地缓存2个名称相同但内容不同的工件.

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

Gradle缓存不允许本地缓存隐藏问题并创建其他神秘且难以调试的行为. Gradle专注于带宽和存储效率,可实现可靠且可复制的企业构建.

Separate metadata cache

Gradle在元数据缓存中以二进制格式记录了依赖关系解决方案各个方面的记录. 存储在元数据缓存中的信息包括:

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

  • 特定模块的已解析模块元数据,包括模块构件和模块依赖性.

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

  • 由于没有在一个特定的存储库中的特定模块或工件,省去重复尝试访问不存在的资源.

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

Repository caches are independent

如上所述,对于每个存储库,都有一个单独的元数据缓存. 存储库由其URL,类型和布局标识. 如果以前没有从此存储库解析模块或工件,则Gradle将尝试根据存储库解析模块. 这将始终涉及对存储库的远程查找,但是在许多情况下, 不需要下载 .

如果所需的工件在构建指定的任何存储库中均不可用,则依赖关系解析将失败,即使本地缓存具有从其他存储库检索到的该工件的副本,也是如此. 存储库独立性允许构建以以前没有构建工具完成的高级方式彼此隔离. 这是创建可在任何环境中可靠且可复制的内部版本的关键功能.

Artifact reuse

在下载工件之前,Gradle会尝试通过下载与该工件关联的sha文件来确定所需工件的校验和. 如果可以检索校验和,那么如果已经存在具有相同ID和校验和的工件,则不会下载工件. 如果无法从远程服务器检索校验和,则将下载工件(如果它与现有工件匹配,则将被忽略).

除了考虑从其他存储库下载的工件外,Gradle还将尝试重用在本地Maven存储库中找到的工件. 如果Maven已下载了候选工件,则Gradle将使用此工件,前提是可以对其进行验证以匹配远程服务器声明的校验和.

Checksum based storage

响应相同的工件标识符,不同的存储库可能会提供不同的二进制工件. Maven SNAPSHOT工件通常是这种情况,但对于在不更改其标识符的情况下重新发布的任何工件也是如此. 通过基于工件的SHA1校验和缓存工件,Gradle能够维护同一工件的多个版本. 这意味着在针对一个存储库进行解析时,Gradle绝不会覆盖来自其他存储库的缓存工件文件. 无需在每个存储库中单独存放工件文件即可完成此操作.

Cache Locking

Gradle依赖项缓存使用基于文件的锁定来确保可以被多个Gradle进程同时安全地使用. 每当读取或写入二进制元数据存储时,都会保留该锁,但是会为缓慢的操作(例如下载远程工件)而释放该锁.

仅当不同的Gradle进程可以一起通信时,才支持此并发访问. 对于容器化版本,通常不是这种情况 .

Cache Cleanup

Gradle跟踪访问依赖项缓存中的哪些工件. 使用此信息,定期(最多每24小时)扫描缓存,以查找未使用超过30天的工件. 然后删除过时的工件,以确保高速缓存不会无限期增长.

Dealing with ephemeral builds

在临时容器中运行构建是一种常见的做法. 通常会产生一个容器,以便在销毁它之前仅执行一个构建. 当构建依赖于每个容器必须重新下载的许多依赖项时,这可能成为一个实际问题. 为了帮助解决这种情况,Gradle提供了两个选项:

Copying and reusing the cache

依赖项缓存(文件和元数据部分)都使用相对路径进行了完全编码. 这意味着完全有可能复制缓存并查看Gradle从中受益.

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

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

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

表1.依赖项高速缓存兼容性
模块缓存版本 文件缓存版本 元数据缓存版本 摇篮版本

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及更高版本

Sharing the dependency cache with other Gradle instances

除了将依赖项缓存复制到每个容器中之外 ,还可以安装一个共享的只读目录,该目录将充当所有容器的依赖项缓存. 与传统的依赖项高速缓存不同,此高速缓存无需锁定即可访问,从而可以从高速缓存中同时读取多个构建. 重要的是,当其他构建可能正在从中读取只读缓存时,不要将其写入.

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

The read-only cache should be sourced from a Gradle dependency cache that already contains some of the required dependencies. The cache can be incomplete; however, an empty shared cache will only add overhead.

共享的只读依赖项缓存是一个孵化功能.

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

然后将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安装缓存用作只读缓存,因为该目录可能包含锁,并且可能由种子版本修改.

Accessing the resolution result programmatically

尽管大多数用户只需要访问文件的"固定列表",但是在某些情况下,可能有必要在图形上进行推理并获得有关分辨率结果的更多信息:

  • 用于工具集成,其中需要依赖图的模型

  • 用于生成依赖关系图的可视表示形式(图像, .dot文件等)的任务

  • 用于提供诊断的任务(类似于dependencyInsight任务)

  • 适用于需要在执行时执行依赖性解析的任务(例如,按需下载文件)

对于这些用例,Gradle提供了惰性的,线程安全的API,可通过调用Configuration.getIncoming()方法进行访问:

  • 无论解析成功与否, ResolutionResult API都可以访问已解析的依赖关系图.

  • 工件API提供了对未转换但未转换的工件的简单访问,但是具有工件的延迟下载(它们只能按需下载).

  • 工件视图API提供了可能经过转换的工件的高级筛选视图.