Maven依赖冲突问题

1、简介

1.1、什么是依赖冲突

依赖冲突是指:在 Maven 项目中,当多个依赖包,引入了同一份类库的不同版本时,可能会导致编译错误或运行时异常。

1.2、依赖冲突的原因

我们在 Maven 项目的 Pom 中 一般会引用许许多多的 Dependency。例如,项目A有这样的依赖关系:

rust 复制代码
A -> C -> X(1.0)
B -> D -> X(2.0)

X是A的 传递性依赖 ,但是两条依赖路径上有两个版本的X,那么哪个X会被 Maven 解析使用呢? 两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。

在绝对大多数情况下,依赖冲突问题并不需要我们考虑,Maven 工具会自动根绝依赖原则选择,这里我们先假设最终引用的 X(1.0) 版本,

1、你想如果B引用 X(2.0) 的新创建的类,但因为最终被解析的是 X(1.0),所以就会出现很典型的 NoClassDefFoundErrorClassNotFoundException 依赖冲突报错。

2、如果B引用 X(2.0) 的新创建的方法,但因为最终被解析的是 X(1.0),所以就会抛出 NoSuchMethodError系统异常。

但换种角度,如果最终解析的是 X(2.0),就没问题了吗?

1、如果 X(2.0) 删掉了 X(1.0) 的一些类,但A已经引用了,同样也会报 NoClassDefFoundError 或者 ClassNotFoundException 错误。

2、如果 X(2.0) 删掉了 X(1.0) 的一些方法,但A已经引用了,同样也会报 NoSuchMethodError 错误。

所以说具体问题还需具体分析,到底采用哪个版本还需要看实际项目。也可能我们需要升级对应的A或者B的版本才能解决问题。

2、Maven 依赖原则

2.1、层级优先原则(路径最近者优先)

在同一个 Pom 内,相同 Jar 不同版本,根据依赖的路径长短来决定引入哪个依赖。

举例

rust 复制代码
依赖链路一:A -> B -> C -> X(1.0)
依赖链路二:F -> D -> X(2.0)

该例中 X(1.0) 的路径长度为3,而 X(2.0) 的路径长度为2,因此 X(2.0) 会被解析使用。依赖调解第一原则不能解决所有问题,比如这样的依赖关系:

rust 复制代码
A -> B -> Y(1.0)
c -> D -> Y(2.0)

Y(1.0) 和 Y(2.0) 的依赖路径长度是一样的,都为2。Maven 定义了依赖调解的第二原则:

2.2、声明优先原则(第一声明者优先)

在依赖路径长度相等的前提下,在同一个 Pom 中,间接依赖声明的顺序决定了谁会被解析使用,顺序最前的那个依赖优胜。该例中,如果A的依赖声明在C之前,那么 Y (1.0) 就会被解析使用.

比如 我在 demo01 中引入了 demo02 和 demo03,demo02 和 demo03 都引入了 Lombok 的依赖

demo02 和 demo03 换个顺序

2.3、特殊情况

  • Pom内声明的优先于父Pom 中的依赖。
  • Pom内出现不同版本的相同类库时,后声明的会覆盖先声明的。也就是在同一个Pom里配置了相同资源的不同版本的直接依赖 ,后配置的覆盖先配置的。比如下边这个例子 调换下顺序就是引用的4.12的依赖。

3、如何排除依赖

我们先来解释下什么是传递性依赖

3.1、什么是传递性依赖

比如当我们项目中,引用了A的依赖,A的依赖通常又会引入B的 Jar 包,B可能还会引入C的 Jar 包。

这样,当你在 pom.xml 文件中添加了A的依赖,Maven 会自动的帮你把所有相关的依赖都添加进来。

就这样一层层的,Maven 会自动的帮你把所有相关的依赖都添加进来。传递性依赖会给项目引入很多依赖,简化项目依赖管理,但是也会带来问题。

最明显的就是容易发生依赖冲突。

3.2、如何排除依赖

这种情况下,想要解决依赖冲突,可以靠升级/降级某些依赖项的版本,从而让不同依赖引入的同一类库,保持一致的版本号。另外,还可以通过隐藏依赖、或者排除特定的依赖项来解决问题。

3.2.1、<exclusions>标签

Exclusions是主动断开依赖的资源,被排除的资源无需指定版本---指不需要

也就是说可以包含一个或者多 Exclusion 子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明 Exclusion 的时候只需要 GroupldArtifactld ,而不需要要 Version 元素。

用法示例:

xml 复制代码
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>5.1.8.RELEASE</version>
  <exclusions>
    <!-- 排除web包依赖的beans包 -->
    <exclusion>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
    </exclusion>
  </exclusions>
</dependency>

3.2.2、<optional>标签

该标签即是"隐藏依赖"的开关,指对外隐藏当前所依赖的资源---指不透明:

  • true:开启隐藏,当前依赖不会向其他工程传递,只保留给自己用;
  • false:默认值,表示当前依赖会保持传递性,其他引入当前工程的项目会间接依赖。
xml 复制代码
用法示例:
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.1.8.RELEASE</version>
  <optional>true</optional>
</dependency>

3.2.2.1、上边两种<exclusions>标签和``<optional>标签的区别

  • A依赖B,B依赖C , C 通过依赖传递会被 A 使用到,现在要想办法让 A 不去依赖 C
  • 可选依赖是在B上设置 <optional> , A 不知道有 C 的存在,代表这个依赖是否需要被发现。这种适用于可以修改B的配置文件的情况下 * * 先看默认情况,也就是false 改为 true 后
  • 排除依赖是在A上设置 <exclusions> , A 知道有 C 的存在,主动将其排除掉。代表这个依赖已经被发现,但自己是否需要引用。这种适用于不能修改B的配置文件的情况下

3.2.3、Maven 聚合工程 统一管理版本

聚合工程,即是指:一个项目允许创建多个子模块,多个子模块组成一个整体,可以统一进行项目的构建。要弄明白聚合工程,得先清楚"父子工程"的概念:

  • 父工程:不具备任何代码、仅有pom.xml的空项目,用来定义公共依赖、插件和配置;
  • 子工程:编写具体代码的子项目,可以继承父工程的配置、依赖项,还可以独立拓展。

Maven聚合工程,就是基于父子工程结构,来将一个完整项目,划分出不同的层次,这种方式可以很好的管理多模块之间的依赖关系,以及构建顺序,大大提高了开发效率、维护性。

为了防止不同子工程引入不同版本的依赖,在父工程中,统一对依赖的版本进行控制,规定所有子工程都使用同一版本的依赖,可以使用<dependencyManagement>标签来管理。

  • <dependencies>:定义强制性依赖,写在该标签里的依赖项,子工程必须强制继承;
  • <dependencyManagement>:定义可选性依赖,该标签里的依赖项,子工程可选择使用。

子工程在使用<dependencyManagement>中已有的依赖项时,不需要写<version>版本号,版本号在父工程中统一管理,这样做的好处在于:以后为项目的技术栈升级版本时,不需要单独修改每个子工程的POM,只需要修改父POM文件即可,减少了版本冲突的可能性。

4、Maven Helper 插件分析jar包冲突

如果你的项目中依赖许许多多的 Jar ,肉眼排查就没那么方便了,这里推荐一个 Maven 管理插件

Pom 文件中看到 Dependency Analyzer标志,说明 Maven Helper 插件就安装成功了。

点击 Dependency Analyzer 之后就会进入到下面的页面

从图中可以看出有哪些jar存在冲突,存在冲突的情况下最终采用了哪个依赖的版本。标红的就是冲突版本,白色的是当前的解析版本

如果我们想保留标红的版本,那我们可以标白区域右击选择排除(Exclude)即可。

5、总结

一般我们在解决依赖冲突的时候,都会选择保留jar高的版本,因为大部分jar在升级的时候都会做到向下兼容,所以只要保留高的版本就不会有什么问题。

但是有些包,版本变化大没法去做向下兼容,高版本删了低版本的某些类或者某些方法,那么这个时候就不能一股脑的去选择高版本,但也不能选择低版本。

就比如下面这个案例

rust 复制代码
依赖链路一:A -> B -> C -> X(1.0)
依赖链路二:F -> D -> X(2.0)

X(2.0) 没有对 X(1.0) 做向下兼容也就是说可能存在排除哪个都不行,那怎么办我们只能考虑升级A的版本或者降低F的版本。比如A升级到A(2.0),使它依赖的X版本变成X(2.0)这样的话就解决依赖冲突。

但话有说回来 A升级到A(2.0) 可能会影响许许多多的地方,比如自己项目中代码是否需要改变,或者因为 A升级到A(2.0) 导致 B和C的版本有所改变,这些影响点都需要我们去考虑的。所以说为什么说一个大型项目稳定后,Pom文件的升级是件繁琐的事情,那是因为考虑的东西是在太多了,稍有不慎就会因为依赖冲突而导致系统报错。

推荐阅读

Kubernetes Informer基本原理

JDK17 与 JDK11 特性差异浅谈

业务分析师眼中的数据中台

政采云大数据权限系统设计和实现

JDK11 与 JDK8 特性差异浅谈

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

相关推荐
monkey_meng8 分钟前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee23 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书1 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang2 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net
Rverdoser3 小时前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
Tech Synapse4 小时前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴4 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
微信-since811924 小时前
[ruby on rails] 安装docker
后端·docker·ruby on rails
代码吐槽菌6 小时前
基于SSM的毕业论文管理系统【附源码】
java·开发语言·数据库·后端·ssm