问题引出
我们在平时的工作以及开发中经常会碰到多版本依赖冲突问题。
比如我们要使用低版本spring-boot-web:2.0.4.RELEASE
,于是在POM
中添加如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-web</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
而由于依赖传递的特性,我们也会自动依赖其他的dependency
,观察External Libraries
会发现依赖了spring
的相关jar
即spring-boot-web:2.0.4.RELEASE
会依赖spring:5.0.8.RELEASE
的相关依赖。
当我们要使用高版本spring-jdbc:5.3.8
时,POM
定义以及External Libraries
的情况如下
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.8</version>
</dependency>
而当我们将两个dependency
一起引用时,观察结果
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.8</version>
</dependency>
可以看到,spring-jdbc:5.3.8
所依赖的spring
全部变成5.3.8
版本了,而一些没被spring-jdbc
引用的如spring-aop
还保留在5.0.8.RELEASE
版本。
那么根据上述实验我们可以提出一个疑问,maven
是会保留最新版本的依赖吗,还是说保留最后声明的dependency
的版本?
接下来我们降低spring-jdbc
的版本,然后将POM
中spring-jdbc
的声明位置与spring-boot-web
的位置进行交换,再看看结果
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
观察结果可以发现,maven
并没有保留最新的版本,也并不是保留最后声明的spring-boot-starter-web
的版本,现象似乎是按照某种策略或方式,导致结果以spring-jdbc
为标准。那么就引出了本文需要研究的内容,即 maven
解决依赖版本冲突的策略是什么?
maven
的处理方式
1. 最短路径原则
maven
是以nearest definition
的方式来处理多版本依赖冲突的,翻译过来就是最近定义
,也可以解释为最短路径
。要理解这个问题,我们首先需要分析一下POM
的dependeny
的树型结构。
可以通过点击IDEA
右侧的maven
按钮,在maven
窗口中展开自己的项目,最后展开Dependencies
进行查看。
也可以通过打开POM
文件,右键选择Maven
->Show Dependencies...
或Show Dependencies Popup...
来查看更复杂的依赖关系。比较全面,但是看着比较乱。
自己整理的树形结构逻辑如下:
我们所依赖的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.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.0</version>
</dependency>
可以看到确实是依赖了spring-core:5.3.0
版本,也间接证实了maven
确实是采用了最短路径原则
的方式来解决多版本依赖冲突的。
3. 最先定义优先原则
我们刚才的例子中都是依赖层级不同,但是有些情况下这些依赖的层级是相同的,那么会按照什么规则决定版本呢?
接下来做一个实验,引入spring-beans:5.3.8
以及spring-tx:5.2.15.RELEASE
依赖,注意这里版本并不相同,而他们都会在第二层级间接依赖spring-core
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.15.RELEASE</version>
</dependency>
</dependencies>
观察External Libraries
,可以看出是以spring-beans
的版本为主进行的依赖
接下来再将这两个依赖在POM
中的位置进行交换,再观察结果
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.15.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.8</version>
</dependency>
</dependencies>
可以看出这次是以spring-tx
为主进行了依赖。
那么我们就可以得出一个结论:如果最短路径原则无法决定一个依赖的版本,即若干个引入同一依赖的路径长度相同,那么由最先定义的依赖决定其版本。
最短路径带来的问题
java
的世界中变动是非常快的,理念也好,框架也好,就连jar
包也是。spring
的相关jar
几天就能冒出一个新版本来,我们不可能去了解每一个jar
的每一个版本都进行了哪些变更,有哪些新特性,我们的项目也不可能总去升级依赖的版本,也符合那句话
如果你的代码可以跑起来了,就不要动他。
我们在开发新功能时经常会引用新的依赖,而这些依赖所带来的依赖传递问题,就有可能在不知情的情况下影响我们的项目。
情景模拟
假如我现在有一个项目叫做parent
,parent
会依赖a.jar
,a.jar
会依赖b.jar
,b.jar
会依赖c:1.0.jar
,并且会使用它的一个方法SomeOperation#operate
,程序运行的很流畅,也没有什么bug。
这时候有了一个新需求,为了实现它我们需要我引入d.jar
,而这个d.jar
引入了高版本的c:2.0.jar
。
依赖的树状图如下:
根据最短路径原则
可知,在引入d.jar
之后,c.jar
就会被maven
引入为2.0
的版本而非1.0
的版本。而且有一个非常不幸的事情,c.jar
由于某些原因,在2.0
版本之后移除了SomeOperation
类,将优化后的代码放入了NewOperation
类中。
我们来实战模拟一下会发生什么
首先我们以此创建project-a
、project-b
、project-c
三个工程,版本都指定为1.0
,每一个工程中都创建相关类SomeOperation
,其中都有一个方法operate
,之后我们将工程通过maven
的install
插件本地打包为jar
供其他工程引用,最后实现我们上述的依赖关系,即a
->b
, b
->c
。再创建一个project-parent
工程,引入project-a:1.0.jar
可以看到程序执行成功,从parent
最终一步一步执行project-c
中指定的方法。
接下来升级project-c
至2.0
版本,注意2.0
版本中我们需要删除SomeOperation
,并创建NewOperation
类。然后创建project-d
,引入project-c:2.0
并install
至本地。最后在parent
项目中引入d.jar
,调用方法进行测试
projec-parent
的POM
<dependencies>
<dependency>
<groupId>com.beemo</groupId>
<artifactId>project-a</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.beemo</groupId>
<artifactId>project-d</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
可以看出新依赖的引进让我们可以实现我们的新功能。一切似乎都很顺利,按时完成了新功能,也没有BUG,这时候我们再回去执行我们之前的功能,调用operate()
方法,看看会发生什么。
可以看到,我们经常碰到的两个异常在这里碰到了。ClassNotFoundException
以及NoClassDefFoundError
。
情况我们之前已经分析过,那就是d.jar
的引入带来的c.jar
版本升级而找不到指定的类了
而有时候新的版本路径较长,导致新功能找不到类或,也是同样的问题。
解决办法
很遗憾的是,目前没有什么好的解决办法,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
提供了<dependency>
的自标签<exclusitons>
,可以指定需要排除的当前依赖的间接依赖。
如上述例子,假设我们不想让系统引入c:2.0.jar
而是使用c:1.0.jar
,我们可以这么定义denpendency
<dependencies>
<dependency>
<groupId>com.beemo</groupId>
<artifactId>project-a</artifactId>
<version>1.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.beemo</groupId>
<artifactId>project-d</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>com.beemo</groupId>
<artifactId>project-c</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
观察External Libraries
可以看到已经退回了原来的c:1.0.jar
版本了。