在使用Maven构建的应用中,因为传递依赖的原因,最终选择的jar包可能因为依赖的新版本新增了方法或者不向下兼容,导致与项目不适配。所以在新引入或者更新了包之后,应用运行期可能会报 ClassNotFoundException
、NoClassDefFoundError
、NoSuchMethodError
等异常或者错误,又或者不该同时存在的依赖出现(例如 log4j-to-slf4j
和 log4j-slf4j-impl
),这时候就要考虑jar包冲突的问题,通过人工干预的方式来选择适配的依赖版本。
什么是传递传递
最短路径优先原则:
假如引入了A和B两个不同的依赖,他们都依赖了D这个依赖,那么最终被引入使用的D依赖版本是1.0,因为这条路径最短。
1
2A -> D(1.0)
B -> C -> D(2.0)最先声明优先原则:
假如引入了A和B两个不同的依赖,他们都依赖了D这个依赖,而且依赖路径的长度一样,但A在B之前在pom文件中被声明,那么最终被引入使用的D依赖版本是1.0,因为A先被声明。
1
2A -> D(1.0)
B -> D(2.0)
排除传递传递带来的冲突
声明排除
Maven提供了依赖树来展示引入依赖之间的关联,可以借助依赖树来判断jar包冲突并排除冲突。使用
mvn dependency:tree
命令即可打印依赖树,找到发生冲突的jar包就可以查看多种不同版本的依赖路径。了解依赖路径之后,可根据实际场景在对应传递引入的依赖下声明排除。在 pom.xml 中,
<dependency>
标签用于声明依赖引入,如果要排除依赖传递引入的其他依赖,可以使用<exclusion>
标签进行声明排除传递引入的依赖。例如:排除
hadoop-client
引入的slf4j-reload4j
,只保留其他依赖传递引入的版本。1
2
3
4
5
6
7
8
9
10
11<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
</exclusion>
</exclusions>
</dependency>版本锁定
如果项目中引入的多个依赖都传递依赖了相同包不同的版本,又或者项目存在多个模块,此时管理依赖是一件复杂的事情。可以通过在 pom.xml 中锁定依赖的版本,避免不同模块的进行多次的声明版本、排除依赖。大型项目一般会有顶级的pom,在最高级的pom中定义即可管理子模块的版本。
1
2
3
4
5
6
7<dependencyManagement>
<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.13.0</version>
</dependency>
</dependencyManagement>此时项目中的
libthrift
的版本就会被锁定在指定的0.13.0
。但需要注意的是<dependencyManagement>
标签只用于锁定依赖版本,并不会引入对应的依赖,仍然需要使用<dependency>
标签显式声明引入依赖。
错误/异常说明
ClassNotFoundException 是一个需要被捕获的异常,在类加载阶段尝试加载类的过程中,在类路径中找不到指定的类时触发抛出该异常。使用 Class.forName() 或者类加载器 loadClass/findSystemClass。导致此异常的原因就两种类路径拼写错误或者缺少依赖。
例如使用JDBC时加载MySQL驱动,如果忘记在Maven中导入相关包,或者将MySQL驱动类的全路径写错,又或者发生了依赖的版本冲突,都有可能会抛出此异常。
1 | try { |
NoSuchMethodError 是一个错误,在编译器时找得到类和方法,在运行时只能找到类但找不到要使用的方法,通常是因为依赖版本冲突导致的。
NoClassDefFoundError 是一个错误,运行时尝试加载定义的类时,虽然找到了类文件,但在加载/解析/链接类的过程中发生了问题,一般是因为 .class
文件损坏/找不到导致的。例如:定义 A.java
和 B.java
两个类,编译后生成了 A.class
和 B.class
,通过解压删除 B.class
再重新打包,就会导致出现 NoClassDefFoundError。