如果你的MAVEN项目运行中报如下错误:
Caused by:java.lang.NoSuchMethodError
Caused by:java.lang.ClassNotFoundException
那么恭喜你,多半是Maven jar包冲突了,那么jar包冲突是如何产生的?首先我们需要了解jar包依赖的传递性和原理。
一、jar包冲突的原理
1、依赖传递
当我们需要A的依赖的时候,就会在pom.xml中引入A的jar包;而引入的A的jar包中可能又依赖B的jar包,这样Maven在解析pom.xml的时候,会依次将A、B 的jar包全部都引入进来。
2、jar包冲突原理
那么jar包是如何产生冲突的?
假设有如下依赖关系:
F<-E<-A(log 13.0):F中包含对E的依赖,E包含对A的依赖,假设A是同一个日志jar包(version为13.0)。
B<-C<-D<-A(log 13.8):D中包含对C的依赖,C中包含对B的依赖,B中包含对A的依赖,假设是A是日志jar包(version为13.8)。
如下图,箭头的指向(A->B)表示对B中包含A的依赖:
当pom.xml文件中引入F、D两个依赖后,根据Maven传递依赖的原则,同一个依赖的两个版本A(13.0)、A(13.8)都会被引入。但Maven 解析 pom.xml 文件时,同一个 jar 包只会保留一个,那么Maven在执行方法的时候会选择哪个版本的依赖jar包呢,这里简单讲一下Maven选择依赖版本的默认原则:
1. 路径最短原则:Maven 面对A(13.0)和A(13.8)时,会默认选择最短路径的那个 jar 包,即A(13.0)。E<-F<-D2 比 A<-B<-C<-D1 路径短 1。
2.声明优先原则:如果路径一样的话,如: A->B->C1, E->F->C2 ,两个依赖路径长度都是 2,那么就选择xml文件中最先声明的那一个依赖。
当我们在调用eat()方法时,按照路径最短原则首先去找A(13.0)中的eat()方法,然后发现怎么没有这个方法呢。原因是:eat()方法是13.8版本新增的方法,13.8之前的版本没有此方法,这样JVM在加载A(13.0)依赖的时候,找不到eat方法,就会报NoSuchMethodError的错误,此时就产生了jar包冲突。
有人可能会疑问,在项目中有时候存在很多这种情况为什么没有报这个错误呢?顺着上边的逻辑,个人分析会有两个原因:
1.在使用依赖的时候扫描的依赖是最新版本的依赖,也就是虽然不是使用的当前的依赖关系中的依赖,但是使用的依赖比当前扫描的依赖版本还要新,这个方法还是存在的,所以就不会报出依赖冲突的错误。
2.使用的方法是两个版本中都有的方法,依赖版本的更迭没有影响方法的使用,也就是新增的那些方法你的项目中可能都没有用到,那肯定就不会报依赖冲突了。比如上边的drink()方法,调用drink()方法的时候,A(13.0)、A(13.8)都含有这个方法(且升级的版本A(13.8)没有改动这个方法,这样即使A有多个版本,也不会产生版本冲突的问题。)
二、解决方法
上边简单了解了Maven选择依赖jar包的原则,那么下面就顺着冲突产生的原因分析解决方案,整体的思路就是让JVM选择确确实实存在这个方法的依赖jar包。
一、声明优先原则
将依赖中传递的依赖版本比较新的放在上面,JVM就会优先选择读取最先声明的依赖,示例:
<dependencies>
<!-- spring-beans-4.2.4 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<!-- spring-beans-3.0.5 -->
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>2.3.24</version>
</dependency>
二、路径最短原则
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
三、排除原则
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>2.3.24</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
</exclusions>
</dependency>
四、版本锁定原则
一般用在继承项目的父项目中
正常项目都是多模块的项目,如moduleA和moduleB共同依赖X这个依赖的话,那么可以将X抽取出来,同时设置其版本号,这样X依赖在升级的时候,不需要分别对moduleA和moduleB模块中的依赖X进行升级,避免太多地方(moduleC、moduleD…)引用X依赖的时候忘记升级造成jar包冲突,这也是实际项目开发中比较常见的方法。
首先定义一个父pom.xml,将公共依赖放在该pom.xml中进行声明:
<properties>
<spring.version>4.2.4.RELEASE</spring.version>
<hibernate.version>5.0.7.Final</hibernate.version>
<struts.version>2.3.24</struts.version>
</properties>
<!-- 锁定版本,struts2-2.3.24、spring4.2.4、hibernate5.0.7 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
本文是个人查阅资料思考之后的总结,难免有不当及错误之处,还请大家指正,欢迎大家一起讨论!