首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Maven 依赖调解源码解析(三):传递依赖,路径最近者优先

Maven 依赖调解源码解析(三):传递依赖,路径最近者优先

作者头像
xiaoxi666
发布于 2021-11-24 01:55:54
发布于 2021-11-24 01:55:54
69900
代码可运行
举报
文章被收录于专栏:xiaoxi666的专栏xiaoxi666的专栏
运行总次数:0
代码可运行

场景

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 内容如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<?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 内容如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<?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内容如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<?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 内容如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<?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 中,执行一下这个命令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 mvn dependency:tree -Dverbose

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

可以发现输出为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[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)被忽略了。这句关键信息是

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(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。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 这个版本最新者优先的调解器,本文不再探索,有兴趣的读者可自行研究。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-11-21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Maven 依赖调解源码解析(六):dependencyManagement 版本锁定
我们在根模块 mavenDependencyDemo 中,用 dependencyManagement 的形式直接指定 X 的版本为 2.0。同时,A 依赖 C,而 C 依赖了 X(1.0)。我们观察下,最终 A 会依赖 X(1.0)还是 X(2.0)。
xiaoxi666
2021/11/24
9630
Maven 依赖调解源码解析(六):dependencyManagement 版本锁定
Maven 依赖调解源码解析(五):同一个文件内声明,后者覆盖前者
这次我们让 A 直接依赖 X,且在 A 的 pom.xml 中声明两次 X,分别为 1.0 和 2.0 版本。内容如下:
xiaoxi666
2021/11/24
4920
Maven 依赖调解源码解析(五):同一个文件内声明,后者覆盖前者
Maven 自动化构建(上)
自动化构建定义了这样一种场景: 在一个项目成功构建完成后,其相关的依赖工程即开始构建,这样可以保证其依赖项目的稳定。
陈不成i
2021/07/15
4810
Maven 依赖调解源码解析(四):传递依赖,第一声明者优先
路径最近者优先原则不能解决所有问题,比如这样的依赖关系:A-> C->X(1.0)、A->D->X(2.0),X(1.0)和 X(2.0)的依赖路径长度是一样的,都为 2。那么到底谁会被解析使用呢?在 Maven 2.0.8 及之前的版本中,这是不确定的,但是从 Maven 2.0.9 开始,为了尽可能避免构建的不确定性,Maven 定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在 POM 中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。该例中,C 的依赖声明在 D 之前,那么 X(1.0)就会被解析使用。
xiaoxi666
2021/11/24
6520
Maven 依赖调解源码解析(四):传递依赖,第一声明者优先
微服务下 Spring Boot Maven 工程依赖关系管理
最基本的 pom.xml 包含工程信息、Spring Boot 父工程、属性配置、依赖包、构建插件
轻量级云原生架构实验室
2018/08/02
8220
微服务下 Spring Boot Maven 工程依赖关系管理
最详细的 Spring Boot 多模块开发与排坑指南
创建一个 SpringBoot 项目非常的简单,简单到这里根本不用再提。你可以在使用 IDEA 新建项目时直接选择 Spring Initlalize 创建一个 Spring Boot 项目,也可以使用 Spring 官方提供的 Spring Boot 项目生成页面得到一个项目。
未读代码
2020/03/25
9.1K0
大厂Java项目如何进行Maven多模块管理
多模块管理简单地理解就是一个 Java 工程项目中不止有一个 pom.xml 文件,会在不同的目录中有多个这样的文件,进而实现 Maven 的多模块管理
灵魂画师牧码
2020/07/23
5.8K0
大厂Java项目如何进行Maven多模块管理
项目管理构建工具——Maven(高阶篇)
Maven作为我们项目管理构建的常用工具,具备许多功能,在这篇文章中我们来仔细介绍
秋落雨微凉
2022/10/25
1.6K0
项目管理构建工具——Maven(高阶篇)
结合实例看 maven 传递依赖与优先级,难顶也得上丫
此时你们是不是有疑问了:不就依赖 spring-boot-starter-web,怎么会有各种 log 的依赖?
青石路
2024/08/08
2600
结合实例看 maven 传递依赖与优先级,难顶也得上丫
Maven 依赖管理(下)
现在当我们构建 App-UI-WAR 项目时, Maven 将通过遍历依赖关系图找到所有的依赖关系,并且构建该应用程序。
陈不成i
2021/07/15
4290
Maven学习笔记之Maven入门
本文涉及Maven知识点有Maven安装与配置,Maven项目结构,Maven依赖管理,Maven项目生命周期管理,基于IDE的Maven使用和Maven私服搭建。
Jetpropelledsnake21
2022/03/07
8310
Maven学习笔记之Maven入门
Maven 核心原理解析(2)
聚合与继承 Maven的聚合特性(aggregation)能够使项目的多个模块聚合在一起构建, 而继承特性(inheritance)能够帮助抽取各模块相同的依赖、插件等配置,在简化模块配置的同时, 保持各模块一致. 模块聚合 随着项目越来越复杂(需要解决的问题越来越多、功能越来越重), 我们更倾向于将一个项目划分几个模块并行开发, 如: 将feedcenter-push项目划分为client、core和web三个模块, 而我们又想一次构建所有模块, 而不是针对各模块分别执行$ mvn命令. 于是就有了Mav
Java帮帮
2018/03/16
1.2K0
maven的安装与使用
Maven 是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。 Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。
云深i不知处
2020/09/16
2K0
【Maven】009-Maven 简单父子工程搭建
訾博ZiBo
2025/01/06
2130
【Maven】009-Maven 简单父子工程搭建
使用maven构建java9 service实例
本文主要研究下如何在maven里头构建java9 multi module及service实例
code4it
2018/09/17
6540
Idea操作Maven超级详细使用 高级篇:
聚合: 可以在 dao service web 模块之上在建立一个 父模块: 通过父模块来聚合工程, 运行只需要编译 打包父工程即可!
Java_慈祥
2024/08/06
6950
Idea操作Maven超级详细使用 高级篇:
【Maven】006-Maven 依赖传递和依赖冲突
当直接引用或者间接引用出现了相同的 jar 包! 这时呢,一个项目就会出现相同的重复 jar 包,这就算作冲突!依赖冲突避免出现重复依赖,并且终止依赖传递!
訾博ZiBo
2025/01/06
4800
【Maven】006-Maven 依赖传递和依赖冲突
Maven 引入外部依赖
pom.xml 的 dependencies 列表列出了我们的项目需要构建的所有外部依赖项。
陈不成i
2021/07/15
1.1K0
Maven项目管理
Maven 翻译为"专家"、“内行”,是 Apache 下的一个纯 Java 开发的开源项目。
用户9645905
2023/11/01
5850
Maven项目管理
【maven学习】划分模块
所有用Maven管理的真实的项目都应该是分模块的,每个模块都对应着一个pom.xml。它们之间通过继承和聚合(也称作多模块,multi-module)相互关联。 一个简单的Maven模块结构是这样的: ---- app-parent |-- pom.xml (pom) | |-- app-util | |-- pom.xml (jar) |
冷冷
2018/02/08
8760
相关推荐
Maven 依赖调解源码解析(六):dependencyManagement 版本锁定
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验