前言
什么是依赖冲突?
依赖冲突是指项目依赖的某一个jar包,有多个不同的版本,因而造成了包版本冲突。
冲突会报如下错误:
- Caused by:java.lang.NoSuchMethodError
- Caused by: java.lang.ClassNotFoundException
依赖生效原则
网上有不同的说法,经个人测试下面的是正确的:
最短路径原则:
面对多级(两级及以上)的不同依赖,会优先选择路径最短的依赖;
声明优先原则:
面对多级(两级及以上)的同级依赖,先声明的依赖会覆盖后声明的依赖;
一级依赖中,后声明的依赖会覆盖先声明的依赖,并且如果是前面的版本低后面的版本高会显示冲突,反之却不会显示冲突;
解决冲突的方式
- 根据优先原则,把需要的版本放在路径最短的位置或最先声明
- 排除其他版本的依赖
- 使用包名替换(Shade)
冲突检测插件
IDEA中安装Maven Helper
插件。
安装重启后,点击pom.xml可以看到两个选项卡,可以查看依赖的关系。
其中三个选项分别表示如下:
Conflicts
(查看所有冲突的依赖,所有的冲突依赖都会在下面显示,不冲突的不显示)All Dependencies as List
(列表形式查看所有依赖,冲突的依赖会红字显示)All Dependencies as Tree
(树形式查看所有依赖,冲突的依赖会红字显示)注意
排查冲突的时候推荐使用第二种方式找到冲突项,搜索冲突项用第三种方式排除冲突。
从图中可以看出有哪些jar存在冲突,存在冲突的情况下最终采用了哪个依赖的版本。
标红的就是冲突版本,白色的是当前的解析版本
。在解决冲突的时候直接把红色的排除是不对的,因为红色的本身就是冲突时被忽略的版本。
当我们排除依赖后直接点击
Reimport
重新引用依赖,但是这时候页面可能不刷新,我们可以再次点击Reimport
,或者点击Refresh UI
。
对于部署环境中包含的jar可以使用provided
标识
如下所示
1 | <dependency> |
Jar包冲突的解决方法
发生依赖冲突主要表现为系统启动或运行中会发生异常,99%表现为三种NoClassDefFoundError
、ClassNotFoundException
、NoSuchMethodError
。
Java 在装载一个目录下所有jar包时, 它加载的顺序完全取决于操作系统!其中Linux的顺序完全取决于INode的顺序。
定位方式
- 在IDEA中(快捷键Ctrl+N)查找异常栈中提示缺失的类在哪些版本的jar包中有。
Maven Helper
插件
解决冲突有两种方式
- 检测冲突的插件升降版本解决
- Jar包隔离
- 包名替换
归纳了解了几种业内的解决方案如下,各有优劣
- spring boot方式,统一管理各个组件版本,简洁高效,但遇到必须使用不同版本jar包时,就不行了
- sofa-ark 用FatJar技术去实现OSGI的功能,jar包隔离原理上跟osgi一致,不过基于fat jar技术,通过maven 插件来简化复杂度,比较轻量,也支持服务热部署热更新等功能。
- shade 也有maven插件,通过更改jar包的字节码来避免jai包冲突,jar包冲突的本质是类的全限定名(包名+类名)冲突了,通过全限定名不能定位到你想用的那个类,maven-shade插件可以更改jar包里的包名,来达到解决冲突的目的。
- 自己定义classload,反射调用冲突方法,代码量太大,不通用,但是会帮助理解上面组件的原理。
升降依赖版本解决
查看上面的冲突检测进行升降版本
Jar隔离
当然不是所有情况都可以通过升降级jar解决冲突,举个例子:
如上图假设应用系统同时依赖A.jar,B.jar,而A.jar,B.jar都依赖protobuf-java,系统运行时都会分别用到A.jar,B.jar中protobuf部分的功能,而且A.jar,B.jar依赖的protobuf版本无法通过升高降低版本调整到一致。由于protobuf-java3.0版本序列化协议,类内容各方面都不兼容protobuf-java2.0版本。
这种情况无论如何调整依赖都无法解决冲突的问题
sofa-ark
sofa-ark 框架支持单独application 和 sofaboot 两种方式,满足单独使用和web框架下的jar包隔离,还能基于zk 完成服务热部署等高大上的功能,但是配置方式略复杂。
很不幸我的应用是跑在flink里的,做不到将容器启动函数放在main的第一句,因为本来就在flink的容器里了,所以此种方案pass。
包名替换(shade)
比如我这Mysql中依赖的版本
1 | <dependency> |
其它依赖的版本
1 | <dependency> |
两者互相冲突但是都需要。
新建项目
1 |
|
打包
1 | mvn clean deploy |
这里发布到本地Maven中,方便引用。
发布失败可以先卸载之前的
1 | mvn dependency:purge-local-repository -DmanualInclude="cn.psvmc:mysql8" |
原来的Jar
1 | <!--操作Mysql--> |
可以替换为
1 | <!--操作Mysql--> |
原项目导出一下验证一下
1 | mvn dependency:copy-dependencies -DincludeScope=compile |
总结
一般我们在解决依赖冲突的时候,都会选择保留jar高的版本,因为大部分jar在升级的时候都会做到向下兼容,所以只要保留高的版本就不会有什么问题。
但是有些包,版本变化大没法去做向下兼容,高版本删了低版本的某些类或者某些方法,那么这个时候就不能选择高版本,但也不能选择低版本。这种只能使用Shade处理。