Maven依赖Jar冲突排查及解决(Jar隔离)

前言

什么是依赖冲突?

依赖冲突是指项目依赖的某一个jar包,有多个不同的版本,因而造成了包版本冲突。

冲突会报如下错误:

  • Caused by:java.lang.NoSuchMethodError
  • Caused by: java.lang.ClassNotFoundException

依赖生效原则

网上有不同的说法,经个人测试下面的是正确的:

  • 最短路径原则:

    面对多级(两级及以上)的不同依赖,会优先选择路径最短的依赖;

  • 声明优先原则:

    面对多级(两级及以上)的同级依赖,先声明的依赖会覆盖后声明的依赖;

  • 一级依赖中,后声明的依赖会覆盖先声明的依赖,并且如果是前面的版本低后面的版本高会显示冲突,反之却不会显示冲突;

解决冲突的方式

  1. 根据优先原则,把需要的版本放在路径最短的位置或最先声明
  2. 排除其他版本的依赖
  3. 使用包名替换(Shade)

冲突检测插件

IDEA中安装Maven Helper插件。

image-20230113165725885

安装重启后,点击pom.xml可以看到两个选项卡,可以查看依赖的关系。

image-20230221093630986

其中三个选项分别表示如下:

  1. Conflicts(查看所有冲突的依赖,所有的冲突依赖都会在下面显示,不冲突的不显示)
  2. All Dependencies as List(列表形式查看所有依赖,冲突的依赖会红字显示)
  3. All Dependencies as Tree(树形式查看所有依赖,冲突的依赖会红字显示)

    注意

排查冲突的时候推荐使用第二种方式找到冲突项,搜索冲突项用第三种方式排除冲突。

从图中可以看出有哪些jar存在冲突,存在冲突的情况下最终采用了哪个依赖的版本。

标红的就是冲突版本,白色的是当前的解析版本

在解决冲突的时候直接把红色的排除是不对的,因为红色的本身就是冲突时被忽略的版本。

当我们排除依赖后直接点击Reimport重新引用依赖,但是这时候页面可能不刷新,我们可以再次点击Reimport,或者点击Refresh UI

对于部署环境中包含的jar可以使用provided标识

如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
<version>${hive.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner-blink_2.12</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>

Jar包冲突的解决方法

发生依赖冲突主要表现为系统启动或运行中会发生异常,99%表现为三种NoClassDefFoundErrorClassNotFoundExceptionNoSuchMethodError

Java 在装载一个目录下所有jar包时, 它加载的顺序完全取决于操作系统!其中Linux的顺序完全取决于INode的顺序。

定位方式

  1. 在IDEA中(快捷键Ctrl+N)查找异常栈中提示缺失的类在哪些版本的jar包中有。
  2. Maven Helper插件

解决冲突有两种方式

  • 检测冲突的插件升降版本解决
  • Jar包隔离
  • 包名替换

归纳了解了几种业内的解决方案如下,各有优劣

  1. spring boot方式,统一管理各个组件版本,简洁高效,但遇到必须使用不同版本jar包时,就不行了
  2. sofa-ark 用FatJar技术去实现OSGI的功能,jar包隔离原理上跟osgi一致,不过基于fat jar技术,通过maven 插件来简化复杂度,比较轻量,也支持服务热部署热更新等功能。
  3. shade 也有maven插件,通过更改jar包的字节码来避免jai包冲突,jar包冲突的本质是类的全限定名(包名+类名)冲突了,通过全限定名不能定位到你想用的那个类,maven-shade插件可以更改jar包里的包名,来达到解决冲突的目的。
  4. 自己定义classload,反射调用冲突方法,代码量太大,不通用,但是会帮助理解上面组件的原理。

升降依赖版本解决

查看上面的冲突检测进行升降版本

Jar隔离

当然不是所有情况都可以通过升降级jar解决冲突,举个例子:

img

如上图假设应用系统同时依赖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
2
3
4
5
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.19.4</version>
</dependency>

其它依赖的版本

1
2
3
4
5
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.5.0</version>
</dependency>

两者互相冲突但是都需要。

新建项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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>cn.psvmc</groupId>
<artifactId>mysql8</artifactId>
<version>1.1</version>
<modelVersion>4.0.0</modelVersion>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.google.protobuf</pattern>
<shadedPattern>cn.psvmc.protobufv3</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/maven/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

打包

1
mvn clean deploy

这里发布到本地Maven中,方便引用。

发布失败可以先卸载之前的

1
mvn dependency:purge-local-repository -DmanualInclude="cn.psvmc:mysql8"

原来的Jar

1
2
3
4
5
6
<!--操作Mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>

可以替换为

1
2
3
4
5
6
<!--操作Mysql-->
<dependency>
<groupId>cn.psvmc</groupId>
<artifactId>mysql8</artifactId>
<version>1.1</version>
</dependency>

原项目导出一下验证一下

1
mvn dependency:copy-dependencies -DincludeScope=compile

总结

一般我们在解决依赖冲突的时候,都会选择保留jar高的版本,因为大部分jar在升级的时候都会做到向下兼容,所以只要保留高的版本就不会有什么问题。

但是有些包,版本变化大没法去做向下兼容,高版本删了低版本的某些类或者某些方法,那么这个时候就不能选择高版本,但也不能选择低版本。这种只能使用Shade处理。