暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

如何解决让人头疼的Maven依赖冲突

来搞笑的Yuan 2020-03-07
2154

最近开了这个坑。。大概可以称之为“如何解决让人头疼的XXX”系列。

这次来说一说依赖冲突。之前在上家公司的时候,每个项目相对来说比较独立,互相依赖较少,只有上下游项目会有所依赖,而公共的包我们用的是Springboot,只需要引入spring-boot-starter-parent,很少会遇到比较麻烦的依赖冲突问题。

到了新公司,第一次看几百行的pom真的有点眼花缭乱,一个web项目后面可能是几十个子系统的rpc服务,还有公司若干内部组件。犹记得第一次调代码的时候一直运行错误,跟我对接的同事一脸淡定,说这个很正常,excluede一下就好了,看来这对他们来说是经常遇到的问题。

最近通过自己搭建springboot项目脚手架向组内推广,踩了一些坑加上查找了一些资料,总结了一些常用的经验来解决这类问题。

1.为什么会冲突

每个Java的项目,无论是开源的工具类,框架类,还是接口类,都是一系列Java程序的集合,任何项目都不会是单纯的用Java底层api造一个轮子,都或多或少会引用其他第三方依赖,那么就存在下图展示的情况

项目依赖于A、B两个jar,A、B中分别依赖了C的不同版本。

那么问题来了,为什么一个项目里不能引入两个不同版本,但名字相同的jar包呢?我们知道Jvm有类加载机制,有双亲委派模型,它保证了Java类加载的安全性,例如JDK中核心类库通过BootstrapClassloader去加载,我们编写的代码是无法改写Jvm中的核心类,从而保证了安全性。Maven加载类也是通过其自定义的类加载器进行,在同一个类加载器中,相同类名的Class只会被加载一次,因此不可能出现一个相同的类名,两个不同版本的类在一个Jvm中(除非两个版本是不同的类加载器加载的)。

通过Maven引入的Jar包,Maven有 路径最短原则
以及 定义顺序原则
覆写优先原则
保证了由Maven引入的jar包是唯一的,并且程序员可以通过此规则判断出最终加载到Jvm的是哪个版本。

路径最短原则,如下图

右边的version2路径距离为2,左边的路径距离为3,右边距离最短因此会引入右边version2

定义顺序原则,在文章最开始的那张图中,当同一个包的两个版本的路径距离相同时,无法通过最短路径原则来判断,那就看在pom.xml中哪个包先申明,则优先加载哪个版本。

覆写优先原则,当子类和父类都申明了一个包,优先加载子类的版本。

2.为什么会报错

了解了为什么存在依赖冲突,我们知道Maven上述三个原则,仅能保证一点,即自动选择了唯一的版本加载在Jvm中,但是这个类通常未必是我们需要的版本。

例如我们通常需要高版本的基础类,如果自动选择了低版本的jar,会有很多方法或者新版本的类找不到,就会出现一系列报错,例如ClassNotFoundException,NoSuchMethodError。

这种问题就回到了文章开头,同事告诉我,exclude一下就好了,将不需要的版本排除,保证不需要的版本不被加载即可。

IDEA软件也有对应的插件(Maven Helper),能够快速帮开发者找到冲突的jar包,查看Maven的依赖树,方便地exclude掉不需要的版本。


可以从左边看到,fastjson这个包我引入的是1.2.54.sec06这个版本(直接申明,路径为1),在dpsf-net中包含了1.2.39这个版本(路径为2),根据最短路径原则,Maven选择了1.2.54.sec06版本,如果需要的话,可以手动将dpsf-net中的1.2.39exclude掉(实际上在本例子中并不需要)。

如果点击了右键exclude会发现,在pom中

<dependency>
<groupId>com.dianping.dpsf</groupId>
<artifactId>dpsf-net</artifactId>
<exclusions>
<exclusion>
<artifactId>fastjson</artifactId>
<groupId>com.alibaba</groupId>
</exclusion>
</exclusions>
</dependency>
复制

对应的包被排除了,当然这部分其实也可以手动操作。

有了这个插件和上述对Maven加载顺序的了解,当我们在项目中引入的了新的jar而启动或编译报错时,可以看一下Mavan的依赖树,找到需要排除的版本,然后重新编译即可。

除此之外,在多模块项目开发中,通常会通过dependencyManagement指定统一的版本,子类只需要指定包名即可复用父类的版本号,避免不必要的混乱。

//父级项目 pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>api</artifactId>
<version>${api.version}</version>
        </dependency>
<dependency>
<groupId>com.sankuai</groupId>
<artifactId>inf-bom</artifactId>
<version>${inf-bom.version}</version>
<type>pom</type>
<scope>import</scope>
        </dependency>
</dependencies>
</dependencyManagement>
//子项目 pom.xml
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>api</artifactId>
<version>${api.version}</version>
</dependency>
</dependencies>
复制

3.同时需要一个包的两个版本

这类问题其实也很常见,实际上是个软件开发中的大问题,例如spark使用了netty3的版本,redisson使用了netty4的版本,我们知道netty在一个Jvm只能有一个版本。

假如redisson有依赖netty3的版本,我们可以回退redisson的版本,如果没有的话如何处理呢。

对于这个问题我也只是做了简单的了解,蚂蚁金服的SOFAArk就是专门用来解决这种问题的。下面引用一段官网的文字

日常使用 Java 开发,常常会遇到包依赖冲突的问题,尤其当应用变得臃肿庞大,包冲突的问题也会变得更加棘手,导致各种各样的报错,例如 LinkageError, NoSuchMethodError 等;实际开发中,可以采用多种方法来解决包冲突问题,比较常见的是类似 Spring Boot 的做法,统一管理应用所有依赖包的版本,保证这些三方包不存在依赖冲突;这种做法只能有效避免包冲突问题,不能根本上解决包冲突的问题;如果某个应用的确需要在运行时使用两个相互冲突的包,例如 protobuf2 和 protobuf3,那么类似 Spring Boot 的做法依然解决不了问题。

为了彻底解决包冲突的问题,需要借助类隔离机制,使用不同的 ClassLoader 加载不同版本的三方依赖,进而隔离包冲突问题;OSGI 作为业内最出名的类隔离框架,自然是可以被用于解决上述包冲突问题,但是 OSGI 框架太过臃肿,功能繁杂;为了解决包冲突问题,引入 OSGI 框架,有牛刀杀鸡之嫌,且反而使工程变得更加复杂,不利于开发;

SOFAArk 采用轻量级的类隔离方案来解决日常经常遇到的包冲突问题,在蚂蚁金服内部服务于整个 SOFABoot 技术体系,弥补 Spring Boot 没有的类隔离能力。SOFAArk 提出了一种特殊的包结构 – Ark Plugin,在遇到包冲突时,用户可以使用 Maven 插件将若干冲突包打包成 Plugin,运行时由独立的 PluginClassLoader 加载,从而解决包冲突。

可以看到它的思路是通过一个特别的类加载器PluginClassLoader加载冲突的版本,来解决这个问题。如果感兴趣可以看看SOFA的官网介绍。https://www.sofastack.tech/projects/sofa-boot/sofa-ark-readme/



祝各位编码愉快,生活愉快!下篇再见!


上篇文章:如何处理让人头疼的空指针

文章转载自来搞笑的Yuan,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论