Maven依赖冲突引起的一些问题

在使用Maven构建的应用中,因为传递依赖的原因,最终选择的jar包可能因为依赖的新版本新增了方法或者不向下兼容,导致与项目不适配。所以在新引入或者更新了包之后,应用运行期可能会报 ClassNotFoundExceptionNoClassDefFoundErrorNoSuchMethodError 等异常或者错误,又或者不该同时存在的依赖出现(例如 log4j-to-slf4jlog4j-slf4j-impl),这时候就要考虑jar包冲突的问题,通过人工干预的方式来选择适配的依赖版本。

什么是传递传递

  • 最短路径优先原则

    假如引入了A和B两个不同的依赖,他们都依赖了D这个依赖,那么最终被引入使用的D依赖版本是1.0,因为这条路径最短。

    1
    2
    A -> D(1.0)
    B -> C -> D(2.0)
  • 最先声明优先原则

    假如引入了A和B两个不同的依赖,他们都依赖了D这个依赖,而且依赖路径的长度一样,但A在B之前在pom文件中被声明,那么最终被引入使用的D依赖版本是1.0,因为A先被声明。

    1
    2
    A -> 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
2
3
4
5
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

NoSuchMethodError 是一个错误,在编译器时找得到类和方法,在运行时只能找到类但找不到要使用的方法,通常是因为依赖版本冲突导致的。

NoClassDefFoundError 是一个错误,运行时尝试加载定义的类时,虽然找到了类文件,但在加载/解析/链接类的过程中发生了问题,一般是因为 .class 文件损坏/找不到导致的。例如:定义 A.javaB.java 两个类,编译后生成了 A.classB.class ,通过解压删除 B.class 再重新打包,就会导致出现 NoClassDefFoundError。