Maven依赖冲突解决总结

时间:2023-02-17 07:13:35

转载请注明出处:

1.Jar包冲突的通常表现

Jar包冲突往往是很诡异的事情,也很难排查,但也会有一些共性的表现。

  • 抛出java.lang.ClassNotFoundException:典型异常,主要是依赖中没有该类。导致原因有两方面:第一,的确没有引入该类;第二,由于Jar包冲突,Maven仲裁机制选择了错误的版本,导致加载的Jar包中没有该类。

  • 抛出java.lang.NoSuchMethodError:找不到特定的方法。Jar包冲突,导致选择了错误的依赖版本,该依赖版本中的类对不存在该方法,或该方法已经被升级。

  • 抛出java.lang.NoClassDefFoundError,java.lang.LinkageError等,原因同上。

  • 没有异常但预期结果不同:加载了错误的版本,不同的版本底层实现不同,导致预期结果不一致。

2.Jar包冲突的本质

Jar包冲突的本质:Java应用程序因某种因素,加载不到正确的类而导致其行为跟预期不一致

具体分两种情况:

  • 情况一:项目依赖了同一Jar包的多个版本,并且选错了版本;

  • 情况二:同样的类在不同的Jar包中出现,导致JVM加载了错误的类;

  情况一,同一个依赖引入了多个Jar包版本,不同的Jar包版本有不同的类和方法。由于Maven依赖树的仲裁机制导致Maven加载了错误的Jar包,从而导致Jar包冲突;

  情况二,同一类在不同的Jar包中出现。这种情况是由于JVM的同一个类加载器对于同一个类只会加载一次,现在加载一个类之后,同全限定名的类便不会进行加载,从而出现Jar包冲突的问题。

  针对第二种情况,如果不是类冲突抛出了异常,你可能根本意识不到,所以就显得更为棘手。这种情况就可以采用前文所述的通过分析不同类加载器的优先级及加载路径、文件系统的文件加载顺序等进行调整来解决。

3.解决Jar包冲突的方法

  几种场景下解决Jar冲突的方法:

    • Maven默认处理:路径最近者优先和第一声明优先;

    • 排除法:使用 Maven Helper,可以将冲突的Jar包在pom.xml中通过exclude来进行排除;

    • 版本锁定法:如果项目中依赖同一Jar包的很多版本,一个个排除非常麻烦,此时可用版本锁定法,即直接明确引入指定版本的依赖。此种方式的优先级最高。

    • 通过设置classpath指定jar包加载的先后顺序 

3.1Mavenhelper 解决冲突

  Maven依赖冲突解决总结

  具体步骤:  

    1.使用mavenHelper 选择 Confilicts;

    2.如果列表存在冲突的依赖,则点击查看冲突的详情

    3.对冲突的依赖可进行右键;使用 exclude 进行排除即可解决冲突

3.2 通过设置 classpath 指定jar包加载的先后顺序

  第一种方式:
java -jar -classpath C:\dependency\framework.jar:C:\location\otherFramework.jar  test.jar

  第二种方式(-cp 等于 -classpath):

java -jar -cp C:\dependency\framework.jar:C:\location\otherFramework.jar  test.jar

  第三种方式:

-Djava.class.path=a.jar

  第四种方式:

-Xbootclasspath/a: 

  classpath 理解和设置可以查看这篇文章Tomcat 与 JVM 中classpath的理解和设置总结 

4.Maven管理机制

依赖传递原则

  当在Maven项目中引入A的依赖,A的依赖通常又会引入B的jar包,B可能还会引入C的jar包。这样,当你在pom.xml文件中添加了A的依赖,Maven会自动的帮你把所有相关的依赖都添加进来。

  如下图所示,项目 A 依赖于项目 B,B 又依赖于项目 C,此时 B 是 A 的直接依赖,C 是 A 的间接依赖。

 Maven依赖冲突解决总结

  Maven 的依赖传递机制是指:不管 Maven 项目存在多少间接依赖,POM 中都只需要定义其直接依赖,不必定义任何间接依赖,Maven 会动读取当前项目各个直接依赖的 POM,将那些必要的间接依赖以传递性依赖的形式引入到当前项目中。Maven 的依赖传递机制能够帮助用户一定程度上简化 POM 的配置。

  基于 A、B、C 三者的依赖关系,根据 Maven 的依赖传递机制,我们只需要在项目 A 的 POM 中定义其直接依赖 B,在项目 B 的 POM 中定义其直接依赖 C,Maven 会解析 A 的直接依赖 B的 POM ,将间接依赖 C 以传递性依赖的形式引入到项目 A 中。

   即当一个依赖需要另外一个依赖支撑时,Maven会帮我们把相应的依赖依次添加到项目当中。这样的好处是,使用起来就非常方便,不用自己挨个去找依赖Jar包了。坏处是会引起Jar包冲突,

最短路径优先原则

  但当一个间接依赖存在多条引入路径时,为了避免出现依赖重复的问题,Maven 通过依赖调节来确定间接依赖的引入路径。

  依赖调节遵循以下两条原则:

    1. 引入路径短者优先

    2. 先声明者优先

  以上两条原则,优先使用第一条原则解决,第一条原则无法解决,再使用第二条原则解决。

引入路径短者优先

  引入路径短者优先,顾名思义,当一个间接依赖存在多条引入路径时,引入路径短的会被解析使用。

  例如,A 存在这样的依赖关系: A->B->C->D(1.0) A->X->D(2.0)

  D 是 A 的间接依赖,但两条引入路径上有两个不同的版本,很显然不能同时引入,否则造成重复依赖的问题。根据 Maven 依赖调节的第一个原则:引入路径短者优先,D(1.0)的路径长度为 3,D(2.0)的路径长度为 2,因此间接依赖 D(2.0)将从 A->X->D(2.0) 路径引入到 A 中。

先声明者优先

  先声明者优先,顾名思义,在引入路径长度相同的前提下,POM 文件中依赖声明的顺序决定了间接依赖会不会被解析使用,顺序靠前的优先使用。

  例如,A 存在以下依赖关系: A->B->D(1.0) A->X->D(2.0)

  D 是 A 的间接依赖,其两条引入路径的长度都是 2,此时 Maven 依赖调节的第一原则已经无法解决,需要使用第二原则:先声明者优先。

5.Tomcat启动时Jar包和类的加载顺序

最后,梳理一下Tomcat启动时,对Jar包和类的加载顺序,其中包含上面提到的不同种类的类加载器默认加载的目录:

  • $java_home/lib 目录下的java核心api;
  • $java_home/lib/ext 目录下的java扩展jar包;
  • java -classpath/-Djava.class.path所指的目录下的类与jar包;
  • $CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载;
  • $CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载;
  • $CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载;
  • 项目路径/WEB-INF/classes下的class文件;
  • 项目路径/WEB-INF/lib下的jar文件;

上述目录中,同一文件夹下的Jar包,按照顺序从上到下一次加载。如果一个class文件已经被加载到JVM中,后面相同的class文件就不会被加载了