【Maven】jar包冲突原因与最优解决方案
文章目录
前言
你是否经常遇到这样的报错:
java.lang.NoSuchMethodErrorjava.lang.ClassNotFoundExceptionjava.lang.NoClassDefFoundError
以上报错就有可能是jar包冲突造成的,Maven中jar包冲突是开发过程中比较常见而又令人头疼的问题,我们需要知道 jar包冲突的原理,才能更好的去解决jar包冲突的问题。本文将从jar包冲突的原理和解决jar包冲突两个方面阐述Maven中jar包问题。
jar包冲突原因
当我们在maven项目中引入第三方组件时,三方组件中的依赖可能会与项目已有组件发生冲突。
比如三方组件中依赖httpclient的版本是4.5.x,而项目中已有的httpclient版本是3.1.x,那么此时就会产生一下两种情况:
如果用三方组件的高版本httpclient覆盖原有的低版本httpclient,有可能会导致原来项目启动运行失败。即使高版本兼容低版本,这样高风险的操作也是很危险的;
如果在三方maven依赖中对其对依赖的httpclient在引入时使用进行排除,使三方组件使用项目中的低版本httpclient,此时可能会因为版本不一致导致三方组件无法使用
在这样的情况下我们应当如何保证不影响项目原有依赖版本的情况下正常使用三方组件呢?此时可以考虑使用maven-shade-plugin插件,jar包冲突解决方案最后介绍。
依赖传递
首先我们需要了解jar包依赖的传递性。
关于依赖作用范围详解见:【Maven】属性scope依赖作用范围详解。
当我们需要A的依赖的时候,就会在pom.xml中引入A的jar包;而引入的A的jar包中可能又依赖B的jar包,这样Maven在解析pom.xml的时候,会依次将A、B 的jar包全部都引入进来。
举个例子:
在Spring Boot应用中导入Hystrix和原生Guava的jar包:
com.google.guava guava 20.0
org.springframework.cloud spring-cloud-starter-netflix-hystrix 1.4.4.RELEASE
利用Maven Helper插件得到项目导入的jar包依赖树:
从图中可以看出Hystrix包含对Guava jar包依赖的引用: Hystrix -> Guava,所以在引入Hystrix的依赖的时候,会将Guava的依赖也引入进来。
冲突原因
假设有如下依赖关系:
A->B->C->D1(log 15.0):A中包含对B的依赖,B中包含对C的依赖,C中包含对D1的依赖,假设是D1是日志jar包,version为15.0
E->F->D2(log 16.0):E中包含对F的依赖,F包含对D2的依赖,假设是D2是同一个日志jar包,version为16.0
当pom.xml文件中引入A、E两个依赖后,根据Maven传递依赖的原则,D1、D2都会被引入,而D1、D2是同一个依赖D的不同版本。
当我们在调用D2中的method1()方法,而D1中是15.0版本(method1可能是D升级后增加的方法),可能没有这个方法,这样JVM在加载A中D1依赖的时候,找不到method1方法,就会报NoSuchMethodError的错误,此时就产生了jar包冲突。
注:
如果在调用method2()方法的时候,D1、D2都含有这个方法(且升级的版本D2没有改动这个方法,这样即使D有多个版本,也不会产生版本冲突的问题。)
举个例子:
利用Maven Helper插件分析得出:Guava这个依赖包产生冲突。
我们之前导入了Guava的原生jar包,版本号是20.0;而现在提示Guava产生冲突,且冲突发生位置是Hystrix所在的jar包,所以可以猜测Hystrix中包含了对Guava不同版本的jar包的引用。
为了验证我们的猜想,使用Maven Helper插件打印出Hystrix依赖的jar tree:
可以看到:Hystrix jar中所依赖的Guava jar包是15.0版本的,而我们之前在pom.xml中引入的原生Guava jar包是20.0版本的,这样Guava就有15.0 与20.0这两个版本,因此发生了jar包冲突。
jar包冲突解决方案
Maven 解析 pom.xml 文件时,同一个 jar 包只会保留一个,那么面对多个版本的jar包,需要怎么解决呢?
Maven默认处理策略
最短路径优先
Maven 面对 D1 和 D2 时,会默认选择最短路径的那个 jar 包,
即 D2。E->F->D2 比 A->B->C->D1 路径短1。
最先声明优先
如果路径一样的话,如: A->B->C1, E->F->C2 ,两个依赖路径长度都是 2,那么就选择最先声明。
排除依赖
移除依赖:用于排除某项依赖的依赖jar包
1.我们可以借助Maven Helper插件中的Dependency Analyzer分析冲突的jar包,然后在对应标红版本的jar包上面点击execlude,就可以将该jar包排除出去。
再刷新以后冲突就会消失。
2.手动排除
手动在pom.xml中使用
<exclusion>
标签去排除冲突的jar包(上面利用插件Maven Helper中的execlude方法其实等同于方法1):
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId><version>1.4.4.RELEASE</version><exclusions><exclusion><groupId>com.google.guava</groupId><artifactId>guava</artifactId></exclusion></exclusions></dependency>
mvn分析包冲突命令:
mvn dependency:tree
版本锁定
版本锁定原则:一般用在继承项目的父项目中
正常项目都是多模块的项目,如moduleA和moduleB共同依赖X这个依赖的话,那么可以将X抽取出来,同时设置其版本号,这样X依赖在升级的时候,不需要分别对moduleA和moduleB模块中的依赖X进行升级,避免太多地方(moduleC、moduleD…)引用X依赖的时候忘记升级造成jar包冲突,这也是实际项目开发中比较常见的方法。
首先定义一个父pom.xml,将公共依赖放在该pom.xml中进行声明:
<properties><spring.version>spring4.2.4</spring.version><properties><dependencyManagement><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId><version>${spring.versio}</version></dependency></dependencies></dependencyManagement>
这样如moduleA和moduleB在引用Spring-beans jar包的时候,直接使用父pom.xml中定义的公共依赖就可以:
moduleA在其pom.xml使用spring-bean的jar包(不用再定义版本):
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId></dependency></dependencies>
moduleB在其pom.xml使用spring-bean的jar包如上类似:
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId></dependency></dependencies>
maven-shade-plugin插件
作用是将依赖的包在package阶段一起打入jar包中,以及对依赖的jar包进行重命名从而达到隔离的作用。这里为了解决上面的问题我们主要使用第二个功能特性,使得相同依赖不同版本达到共存的目的。
1.环境准备
这里用fastjson来模拟使用maven-shade-plugin解决项目中不同版本共存问题。原项目此时使用的是1.1.15版本的fastjson
<!-- 原项目 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.1.15</version></dependency>
假引入一个三方依赖,该依赖使用1.2.75版本的fastjson
```xml
<!-- 将引入依赖 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.75</version></dependency>
2.解决方案
搭建一个新的模块rename-dependencies,专门用于存放1.2.75依赖。在pom文件中添加1.2.75的依赖,然后添加maven-shade-plugin插件。rename-dependencies的pom如下
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><groupId>com.sk</groupId><artifactId>rename-dependencies</artifactId><version>1.0-SNAPSHOT</version><modelVersion>4.0.0</modelVersion><dependencies><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.75</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.2.4</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><filters><filter><artifact>*:*</artifact><excludes><exclude>META-INF/*.SF</exclude><exclude>META-INF/*.DSA</exclude><exclude>META-INF/*.RSA</exclude></excludes></filter></filters><relocations><relocation><pattern>com.alibaba</pattern><shadedPattern>shade.com.alibaba</shadedPattern></relocation></relocations></configuration></execution></executions></plugin></plugins></build></project>
从配置文件中可以看到,由于maven-shade-plugin插件在解决这个问题上其实是通过对依赖进行重命名而达到隔离的目的,所以配置主要是集中在relocations中。这里将以com.alibaba开头的包全部重命名为以shade.com.alibaba开头。
3.引入依赖
将rename-dependencies进行打包,打包好之后在原项目中引入rename-dependencies的依赖。此时在引入rename-dependencies之后,可以在项目下看到该依赖中的fastjson包名发生了变化
此时在代码中调用fastjson相关方法,会提示选择所需要包,如下图,此时问题解决,两个版本的fastjson可同时使用已经兼容。
一些需要注意的坑
描述: 引入依赖找不到重命名的shade包
原因:重命名的模块和需要引入依赖的模块在一个项目中,idea优先找本项目,所以没有走仓库
解决方案:
将模块从项目maven中移除,右键项目-maven-unlink maven projects
新建一个项目专门来做依赖
总结
本文从jar包冲突的原理和解决jar包冲突两个方面阐述Maven引入jar包依赖的问题;
其中在解决方案选择方面:
如果Maven不能根据默认处理策略解决掉,就需要从移除依赖或者升级现有依赖处理;
但是升级现有依赖风险比较大,有时会对原项目不兼容的代码进行大量修改,就比如有次项目引入封装的工作流组件时,其中组件内使用的mybatis-plus为3.5.1版本,原项目使用的3.3.1,先是对组件排除掉mybatis-plus,但是后面发现mybatis-plus中有一个反射工具类无法使用,只有高版本才能使用,于是就对原项目做依赖升级,但是需要对原项目不兼容的代码进行大量修改,比如mybatisconfig类和大量接口返回类型修改;
最优选择:maven-shade-plugin 保证不影响项目原有依赖版本的情况下正常使用三方组件。
参考:
版权归原作者 沙糖橘 所有, 如有侵权,请联系我们删除。