深入了解maven多版本依赖冲突处理机制

问题引出

我们在平时的工作以及开发中经常会碰到多版本依赖冲突问题。
比如我们要使用低版本spring-boot-web:2.0.4.RELEASE,于是在POM中添加如下依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-webartifactId>
    <version>2.0.4.RELEASEversion>
dependency>

而由于依赖传递的特性,我们也会自动依赖其他的dependency,观察External Libraries会发现依赖了spring的相关jar
深入了解maven多版本依赖冲突处理机制_第1张图片
spring-boot-web:2.0.4.RELEASE会依赖spring:5.0.8.RELEASE的相关依赖。

当我们要使用高版本spring-jdbc:5.3.8时,POM定义以及External Libraries的情况如下

<dependency>
  <groupId>org.springframeworkgroupId>
  <artifactId>spring-jdbcartifactId>
  <version>5.3.8version>
dependency>

在这里插入图片描述
而当我们将两个dependency一起引用时,观察结果

<dependency>
 <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-webartifactId>
  <version>2.0.4.RELEASEversion>
dependency>
<dependency>
  <groupId>org.springframeworkgroupId>
  <artifactId>spring-jdbcartifactId>
  <version>5.3.8version>
dependency>

深入了解maven多版本依赖冲突处理机制_第2张图片

可以看到,spring-jdbc:5.3.8所依赖的spring全部变成5.3.8版本了,而一些没被spring-jdbc引用的如spring-aop还保留在5.0.8.RELEASE版本。

那么根据上述实验我们可以提出一个疑问,maven是会保留最新版本的依赖吗,还是说保留最后声明的dependency的版本?
接下来我们降低spring-jdbc的版本,然后将POMspring-jdbc的声明位置与spring-boot-web的位置进行交换,再看看结果

<dependency>
 <groupId>org.springframeworkgroupId>
   <artifactId>spring-jdbcartifactId>
   <version>4.3.23.RELEASEversion>
 dependency>
 <dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-webartifactId>
   <version>2.0.4.RELEASEversion>
 dependency>

深入了解maven多版本依赖冲突处理机制_第3张图片
观察结果可以发现,maven并没有保留最新的版本,也并不是保留最后声明的spring-boot-starter-web的版本,现象似乎是按照某种策略或方式,导致结果以spring-jdbc为标准。那么就引出了本文需要研究的内容,即 maven解决依赖版本冲突的策略是什么?

maven的处理方式

1. 最短路径原则

maven是以nearest definition的方式来处理多版本依赖冲突的,翻译过来就是最近定义,也可以解释为最短路径。要理解这个问题,我们首先需要分析一下POMdependeny的树型结构。

可以通过点击IDEA右侧的maven按钮,在maven窗口中展开自己的项目,最后展开Dependencies进行查看。
深入了解maven多版本依赖冲突处理机制_第4张图片
也可以通过打开POM文件,右键选择Maven->Show Dependencies...Show Dependencies Popup...来查看更复杂的依赖关系。比较全面,但是看着比较乱。
深入了解maven多版本依赖冲突处理机制_第5张图片
深入了解maven多版本依赖冲突处理机制_第6张图片
自己整理的树形结构逻辑如下:
深入了解maven多版本依赖冲突处理机制_第7张图片
我们所依赖的dependency中,大多数都是这样重复依赖同一个jar的,由上图可知,我们的例子中至少依赖了5遍spring-core,而这只是部分展示图,实际上会更多。maven依照最短路径来决定使用哪个版本,也就是处于第二层级的spring-core:4.3.23.RELEASE,而该依赖的引入者是第一层级的spring-jdbc:4.3.23.RELEASE,这也就是为什么问题引出章节中,无论版本的新旧以及dependency声明的位置如何,最终都会按照spring-jdbc的版本决定了。

2. 直接依赖优先原则

如果spring-boot-starter-web以及spring-jdbc间接依赖的spring-core版本我都不想使用,我就想使用指定的版本,例如spring-core:5.3.0,那么怎么办呢?
我们已经知道了maven是采用了最短路径原则方式决定该使用什么版本的,而上述两个依赖中都是间接依赖spring-core,最多就是在第二层级,那么如果我们采用直接依赖的方式,直接在第一层级定义spring-core不就好了吗?接下来动手做实验。

<dependency>
  <groupId>org.springframeworkgroupId>
  <artifactId>spring-jdbcartifactId>
  <version>4.3.23.RELEASEversion>
dependency>
<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-webartifactId>
  <version>2.0.4.RELEASEversion>
dependency>
<dependency>
  <groupId>org.springframeworkgroupId>
  <artifactId>spring-coreartifactId>
  <version>5.3.0version>
dependency>

深入了解maven多版本依赖冲突处理机制_第8张图片
可以看到确实是依赖了spring-core:5.3.0版本,也间接证实了maven确实是采用了最短路径原则的方式来解决多版本依赖冲突的。

3. 最先定义优先原则

我们刚才的例子中都是依赖层级不同,但是有些情况下这些依赖的层级是相同的,那么会按照什么规则决定版本呢?

接下来做一个实验,引入spring-beans:5.3.8以及spring-tx:5.2.15.RELEASE依赖,注意这里版本并不相同,而他们都会在第二层级间接依赖spring-core

<dependencies>
  <dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-beansartifactId>
    <version>5.3.8version>
  dependency>
  <dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-txartifactId>
    <version>5.2.15.RELEASEversion>
  dependency>
dependencies>

深入了解maven多版本依赖冲突处理机制_第9张图片
观察External Libraries,可以看出是以spring-beans的版本为主进行的依赖
深入了解maven多版本依赖冲突处理机制_第10张图片
接下来再将这两个依赖在POM中的位置进行交换,再观察结果

 <dependencies>
  <dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-txartifactId>
    <version>5.2.15.RELEASEversion>
  dependency>
  <dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-beansartifactId>
    <version>5.3.8version>
  dependency>
dependencies>

深入了解maven多版本依赖冲突处理机制_第11张图片
可以看出这次是以spring-tx为主进行了依赖。

那么我们就可以得出一个结论:如果最短路径原则无法决定一个依赖的版本,即若干个引入同一依赖的路径长度相同,那么由最先定义的依赖决定其版本。

最短路径带来的问题

java的世界中变动是非常快的,理念也好,框架也好,就连jar包也是。spring的相关jar几天就能冒出一个新版本来,我们不可能去了解每一个jar的每一个版本都进行了哪些变更,有哪些新特性,我们的项目也不可能总去升级依赖的版本,也符合那句话

如果你的代码可以跑起来了,就不要动他。

我们在开发新功能时经常会引用新的依赖,而这些依赖所带来的依赖传递问题,就有可能在不知情的情况下影响我们的项目。

情景模拟

假如我现在有一个项目叫做parentparent会依赖a.jara.jar会依赖b.jarb.jar会依赖c:1.0.jar,并且会使用它的一个方法SomeOperation#operate,程序运行的很流畅,也没有什么bug。

这时候有了一个新需求,为了实现它我们需要我引入d.jar,而这个d.jar引入了高版本的c:2.0.jar

依赖的树状图如下:
深入了解maven多版本依赖冲突处理机制_第12张图片
根据最短路径原则可知,在引入d.jar之后,c.jar就会被maven引入为2.0的版本而非1.0的版本。而且有一个非常不幸的事情,c.jar由于某些原因,在2.0版本之后移除了SomeOperation类,将优化后的代码放入了NewOperation类中。

我们来实战模拟一下会发生什么

首先我们以此创建project-aproject-bproject-c三个工程,版本都指定为1.0,每一个工程中都创建相关类SomeOperation,其中都有一个方法operate,之后我们将工程通过maveninstall插件本地打包为jar供其他工程引用,最后实现我们上述的依赖关系,即a->b, b->c。再创建一个project-parent工程,引入project-a:1.0.jar
深入了解maven多版本依赖冲突处理机制_第13张图片

深入了解maven多版本依赖冲突处理机制_第14张图片
深入了解maven多版本依赖冲突处理机制_第15张图片深入了解maven多版本依赖冲突处理机制_第16张图片

可以看到程序执行成功,从parent最终一步一步执行project-c中指定的方法。

接下来升级project-c2.0版本,注意2.0版本中我们需要删除SomeOperation,并创建NewOperation类。然后创建project-d,引入project-c:2.0install至本地。最后在parent项目中引入d.jar,调用方法进行测试
深入了解maven多版本依赖冲突处理机制_第17张图片

深入了解maven多版本依赖冲突处理机制_第18张图片
projec-parentPOM

<dependencies>
   <dependency>
        <groupId>com.beemogroupId>
        <artifactId>project-aartifactId>
        <version>1.0version>
    dependency>
    <dependency>
        <groupId>com.beemogroupId>
        <artifactId>project-dartifactId>
        <version>1.0version>
    dependency>
dependencies>

深入了解maven多版本依赖冲突处理机制_第19张图片

可以看出新依赖的引进让我们可以实现我们的新功能。一切似乎都很顺利,按时完成了新功能,也没有BUG,这时候我们再回去执行我们之前的功能,调用operate()方法,看看会发生什么。
深入了解maven多版本依赖冲突处理机制_第20张图片
可以看到,我们经常碰到的两个异常在这里碰到了。ClassNotFoundException以及NoClassDefFoundError
情况我们之前已经分析过,那就是d.jar的引入带来的c.jar版本升级而找不到指定的类了
深入了解maven多版本依赖冲突处理机制_第21张图片
而有时候新的版本路径较长,导致新功能找不到类或,也是同样的问题。

解决办法

很遗憾的是,目前没有什么好的解决办法,maven不允许一个依赖同时出现两个版本。

也有可能是笔者没找到解决方法,如果小伙伴有解决方案请留言告知,一起探讨。

手动排除间接依赖

有一种情况就是我们新引入的依赖间接引入一个高版本的.jar,例如刚才例子中的d.jar引入了c:2.0.jar,我们原来的项目中引入的是一个低版本的jar,例如c:1.0.jar,而高版本的路径更近,即maven依照最短路径原则选择了c:2.0.jar,并且这个高版本的jar会影响低版本jar的某些功能。

但是假设d.jar引用原来版本的c:1.0.jar就可以运行,结果现在导致原来的功能不好用了,这时候该怎么办呢?

maven提供了的自标签,可以指定需要排除的当前依赖的间接依赖。
如上述例子,假设我们不想让系统引入c:2.0.jar而是使用c:1.0.jar,我们可以这么定义denpendency

<dependencies>
  <dependency>
        <groupId>com.beemogroupId>
        <artifactId>project-aartifactId>
        <version>1.0version>
        <optional>trueoptional>
    dependency>
    <dependency>
        <groupId>com.beemogroupId>
        <artifactId>project-dartifactId>
        <version>1.0version>
        <exclusions>
            <exclusion>
                <groupId>com.beemogroupId>
                <artifactId>project-cartifactId>
            exclusion>
        exclusions>
    dependency>
dependencies>

观察External Libraries可以看到已经退回了原来的c:1.0.jar版本了。
深入了解maven多版本依赖冲突处理机制_第22张图片

参考文献

Maven官网:依赖机制

你可能感兴趣的:(maven)