本文是系列文章《Maven 源码解析:依赖调解是如何实现的?》第三篇,主要介绍依赖调解的第一条原则:传递依赖,路径最近者优先。本篇内容较多,也是开始源码分析的第一篇,请务必仔细阅读,否则后面的文章可能就看不懂了。系列文章总目录参见:https://www.cnblogs.com/xiaoxi666/p/15583241.html。

场景

A有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被Maven解析使用呢?两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。根据路径最近者优先原则,该例中X(1.0)的路径长度为3,而X(2.0)的路径长度为2,因此X(2.0)会被解析使用。

A 的 pom.xml 内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mavenDependencyDemo</artifactId>
<groupId>org.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>A</artifactId>
<version>1.0</version> <dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>B</artifactId>
<version>1.0</version>
</dependency> <dependency>
<groupId>org.example</groupId>
<artifactId>D</artifactId>
<version>1.0</version>
</dependency> </dependencies> </project>

B 的 pom.xml 内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mavenDependencyDemo</artifactId>
<groupId>org.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>B</artifactId>
<version>1.0</version> <dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>C</artifactId>
<version>1.0</version>
</dependency>
</dependencies> </project>

C 的 pom.xml内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mavenDependencyDemo</artifactId>
<groupId>org.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>C</artifactId>
<version>1.0</version> <dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>X</artifactId>
<version>1.0</version>
</dependency>
</dependencies> </project>

D 的 pom.xml 内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mavenDependencyDemo</artifactId>
<groupId>org.example</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion> <artifactId>D</artifactId>
<version>1.0</version> <dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>X</artifactId>
<version>2.0</version>
</dependency>
</dependencies> </project>

源码

刚拿到源码不知道从哪里打断点,我们可以先切换到模块 A 中,执行一下这个命令:

 mvn dependency:tree -Dverbose

其中的verbose是为了输出详细信息,方便我们找到源码中的参照点。

可以发现输出为:

[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------------< org.example:A >----------------------------
[INFO] Building A 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ A ---
[INFO] org.example:A:jar:1.0
[INFO] +- org.example:B:jar:1.0:compile
[INFO] | \- org.example:C:jar:1.0:compile
[INFO] | \- (org.example:X:jar:1.0:compile - omitted for conflict with 2.0)
[INFO] \- org.example:D:jar:1.0:compile
[INFO] \- org.example:X:jar:2.0:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.002 s
[INFO] Finished at: 2021-11-20T12:17:29+08:00
[INFO] ------------------------------------------------------------------------

可以看到,A 依赖了 X(2.0),而 X(1.0)被忽略了。这句关键信息是

(org.example:X:jar:1.0:compile - omitted for conflict with 2.0)

那我们分别到apache-maven-3.6.3、maven-dependency-plugin 和 maven-dependency-tree这三个项目中找一下,看看是哪里输出的这句话。

我们最终在maven-dependency-tree这个插件项目中发现了这段输出的源头:

此时我们可以用前面讲述的调试方法(注意是调试插件maven-dependency-tree),在这里打断点,从而找出调用链:

很明显,这个 TreeMojo 就在 maven-dependency-plugin 项目中了:

结合前面在 maven-dependency-tree 中的调用链,可知是在TreeMojo#serializeVerboseDependencyTree 这个方法中,以访问者方式序列化依赖关系,这里面用到的visitor 是 BuildingDependencyNodeVisitor ,可以回到 maven-dependency-tree 项目查看调用链加以印证:

这一步明确之后,我们继续分析 maven-dependency-tree 刚才打断点的地方,看看为什么能进到这里,也即有哪些上下文。

我们发现,重点是 state 这个字段,因此看看哪里给它赋了值。

根据上图可以看出,只有一处赋值了 OMITTED_FOR_CONFLICT,点进去看看:

顺便可以看到:

1、如果重复声明的依赖版本号相同,那么 state 是 OMMITTED_FOR_DUPLICATE,意味着重复。

2、如果重复声明的依赖版本号不同,那么 state 是 OMMITTED _FOR_CONFLICT,意味着冲突,最终必然只会选其中的一个。

继续往上找调用链:

我们发现 omitForNearer 这个方法的定义在 apache-maven-3.6.3 这个核心项目中的 ResolutionListener 类中,而插件 maven-dependency-tree 中的 DependencyTreeResolutionListener负责了具体的实现。

那么我们中断此次调试,并在 omittedNode.omitForConflict( kept ) 这里打个断点,重新调试,看看上下文都有什么:

可以看到,要被忽略的依赖 和 要被保留的依赖,是由上层传入的。也就是说,apache-maven-3.6.3 这个核心项目已经做出了「应该保留哪个依赖」的判断

因此我们再次中断调试,回到 apache-maven-3.6.3 核心项目重新调试。

按照刚才的分析,我们找到 omitForNearer 被调用的地方,打上断点:

断点进来后,我们顺着调用链网上找,看看是在哪里决定的:

可以看到,是在 org.apache.maven.repository.legacy.resolver.DefaultLegacyArtifactCollector#recurse这个方法中决定的。看起来,似乎关键方法是 checkScopeUpdate( farthest, nearest, listeners )。我们需要点进去看看,它直接决定了哪个依赖被忽略,哪个依赖被保留。

我们再次重新调试。为方便,可以设置条件断点 "X".equals(((DefaultArtifact) nearest.artifact).artifactId),只关心 X 依赖。

可以看出,这个方法只是根据 scope 优先级进行处理,总而言之就是保留优先级更高的依赖。然而这并不是我们的场景。

因此,我们应该顺着调用链继续往上找。

重新调试(或者先回退调用栈,再前进),会发现进入到了这里:

可以看到,nearest 来源于 node,farthest 来源于 previous。而且这个赋值关系受到 resolved 和 previous 的相等关系控制。那我们分别看看 previous、 resolved 以及 node 的来源。

往上翻,可以看到 previous 也即X(1.0)和 node 也即 X(2.0)均是在上一步解析得到的:

而 resolved 是在这里解析得到的:

结合前面的分析,我们就可以知道:

如果 resolved 和 previous 相同,那么保留 previous,忽略 node;反之,保留 node,忽略 previous。

那我们需要看看这行代码内部究竟干了啥:

resolved = conflictResolver.resolveConflict( previous, node );

调试进入:

可以看出我们进入了 NearestConflictResolver 这个冲突调解器,具体地,它会选择路径最近的依赖。从实现层面看,非常简单:它直接比较两个依赖的路径深度,发现 X(1.0)的深度为3,X(2.0)的深度为2,按照规则,需要保留路径深度更小的 X(2.0)。

那么问题来了,什么时候会调用 conflictResolver.resolveConflict( previous, node ) 呢?看下图:

处理完这一步之后,会把X(2.0)也加入previousNodes中:

还有个小尾巴,上面我们提到: previous 也即X(1.0)和 node 也即 X(2.0)均是在上一步解析得到,让我们看看:

其实很容易发现,依赖的解析过程就是一种深度遍历,这里的 recurse 方法会被不断递归。用我们的例子来理解,先遍历了A->B->C->X(1.0),然后遍历了 A->D->X(2.0) ,我们刚刚调试的过程正处于 D->X(2.0)刚刚完成的时刻。

好了,当这些递归遍历结束后,返回到org.apache.maven.repository.legacy.resolver.DefaultLegacyArtifactCollector#collect 方法,准备生成结果:

可以看到,只有 isActive 的依赖才会被收集到结果中,也就是最终起作用的依赖版本。其实 active 的设置就是在之前的这个步骤实现的:

可以看到,如果一个依赖被忽略,它本身的所有依赖也会被忽略。

小结

至此,我们已经知道了路径最近者优先原则的运行原理:依赖的解析是一种深度遍历的过程,每当解析一个依赖时,均会放到 resolvedArtifacts 这个Map中,后续再看到同名的依赖时,进行冲突调解。对于路径最近者优先原则来说,具体的冲突调解器是NearestConflictResolver。

扩展一下:上述分析过程中,我们看到了 ConflictResolver 这个接口,发现它是专门进行依赖调解的,不同的调解方式应该就是由具体的实现类来处理。对于路径最近者优先原则来说,就是由 NearestConflictResolver 处理。那其他的原则会由其他的依赖调解器处理吗?

就让我们看看都有哪些具体的依赖调解器:

上图结合源码可以看到,总共有4种调解器,分别是:

  • 版本最老者优先
  • 版本最新者优先
  • 路径最近者优先(还有一个默认调解器继承了它,但实现是空的,已经被打了 @Deprecated 标记,可以不考虑)
  • 路径最远者优先

回到刚才的调解过程可以看到,默认调解器是「路径最近者优先」:

所以可以猜测,本文中其余的原则应该没有使用其他的调解器,它们应该是在某些插件中起作用的。比如Maven 有插件可以将版本更新到最新,应该就是用了 NewestConflictResolver 这个版本最新者优先的调解器,本文不再探索。

Maven 依赖调解源码解析(三):传递依赖,路径最近者优先的更多相关文章

  1. Maven 依赖调解源码解析(四):传递依赖,第一声明者优先

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第四篇,主要介绍依赖调解的第二条原则:传递依赖,第一声明者优先.请按顺序阅读其他系列文章,系列文章总目录参见:https:// ...

  2. Maven 依赖调解源码解析(一):开篇

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第一篇,主要做个开头介绍.并为后续的实验做一些准备.系列文章总目录参见:https://www.cnblogs.com/xia ...

  3. Maven 依赖调解源码解析(六):dependencyManagement 版本锁定

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第六篇,主要介绍 dependencyManagement 版本锁定原则.请按顺序阅读其他系列文章,系列文章总目录参见:htt ...

  4. Maven 依赖调解源码解析(七):总结

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第七篇,也是最后一篇,主要做个总结.请按顺序阅读其他系列文章,系列文章总目录参见:hhttps://www.cnblogs.c ...

  5. Maven 依赖调解源码解析(二):如何调试 Maven 源码和插件源码

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第二篇,主要介绍如何调试 Maven 源码和插件源码.系列文章总目录参见:https://www.cnblogs.com/xi ...

  6. Maven 依赖调解源码解析(五):同一个文件内声明,后者覆盖前者

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第五篇,主要介绍同一个文件内声明,后者覆盖前者的原则.请按顺序阅读其他系列文章,系列文章总目录参见:https://www.c ...

  7. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  8. Celery 源码解析三: Task 对象的实现

    Task 的实现在 Celery 中你会发现有两处,一处位于 celery/app/task.py,这是第一个:第二个位于 celery/task/base.py 中,这是第二个.他们之间是有关系的, ...

  9. Mybatis源码解析(三) —— Mapper代理类的生成

    Mybatis源码解析(三) -- Mapper代理类的生成   在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...

随机推荐

  1. 【nvidia jetson xavier】 Deepstream Yolov3示例模型运行

    作者声明 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 原文链接:https://www.cnblogs.com/phoenixash/p/15 ...

  2. VS 调试 提示 Lc.exe已退出 代码为-1问题解决方法

    找到程序项目下Properties文件夹licenses.licx文件,然后右键选择删除就可以了,调试运行正常了 https://jingyan.baidu.com/article/b24f6c822 ...

  3. 题解 [SHOI2012]随机树

    题目传送门 Description \(n\le 100\) Solution Problem 1 不难看出,答案就是: \[1+\sum_{i=1}^{n-1} 2/(i+1) \] Problem ...

  4. FastAPI 学习之路(八)路径参数和数值的校验

    系列文章: FastAPI 学习之路(一)fastapi--高性能web开发框架 FastAPI 学习之路(二) FastAPI 学习之路(三) FastAPI 学习之路(四) FastAPI 学习之 ...

  5. programmercarl——数组——二分查找

    二分查找,在经过: 34--https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-arr ...

  6. 机器学习:KNN

    KNN:K-nearst neighbors 简介: k-近邻算法采用测量不同特征值之间的距离来进行分类,简而言之为:人以类聚,物以群分 KNN既可以应用于分类中,也可用于回归中:在分类的预测是,一般 ...

  7. [Beta]the Agiles Scrum Meeting 12

    会议时间:2020.5.27 21:00 1.每个人的工作 今天已完成的工作 成员 已完成的工作 issue yjy 帮助解决技术问题 tq 撰写技术博客 wjx 博客评分界面美化 dzx 博客评分界 ...

  8. [对对子队]Alpha阶段项目展示博客

    Alpha阶段项目展示博客 1 团队成员的简介和个人博客地址 成员 头像 岗位 博客 个人介绍 黄贤昊 PM 17373253 喜欢玩游戏和做游戏,项目经验基本都和游戏相关,擅长摸鱼,偶尔敬业. 刘子 ...

  9. BUAA2020软工作业(二)——对软件工程的初步理解

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标是 进一步提高自己的编码能力,工程能力 这个作业在哪个具体方面帮助 ...

  10. 2021.9.9考试总结[NOIP模拟50]

    T1 第零题 神秘结论:从一个点满体力到另一个点的复活次数与倒过来相同. 于是预处理出每个点向上走第$2^i$个死亡点的位置,具体实现可以倍增或二分. 每次询问先从两个点同时向上倍增,都转到离$LCA ...