写点什么

版本不兼容 Jar 包冲突该如何是好?

  • 2021 年 12 月 28 日
  • 本文字数:4808 字

    阅读完需:约 16 分钟

一、引言


“老婆”和“妈妈”同时掉进水里,先救谁?


常言道:编码五分钟,解冲突两小时。作为 Java 开发来说,第一眼见到 ClassNotFoundException、NoSuchMethodException 这些异常来说,第一反应就是排包。经过一通常规和非常规操作以后,往往会找到同一个 Jar 包引入了多个不同的版本,这时候一般排除掉低版本、保留高版本就可以了,这是因为一般 Jar 包都是向下兼容的。但是,如果出现版本不兼容的情况的时候,就会陷入“老婆和妈同时掉进水里,先救谁”的两难境地,如果恰恰这种不兼容发生在中间件依赖和业务自身依赖之间,那就更难了。


如下图所示,Project 表示我们的项目,Dependency A 表示我们的业务依赖,Dependency B 表示中间件依赖,如果业务依赖和中间件依赖都依赖同一个 Jar 包 C,但是版本却不一样,分别为 0.1 版本和 0.2 版本,而且最不巧的是这两个版本还存在冲突,有些老的功能只在 0.1 低版本中存在,有些新功能只在 0.2 高版本中存在,真是“老婆和妈同时掉进水里,先救谁都不行”。



(图片摘自:SOFAArk 官网)


俗话说:没有遇到过 Jar 包冲突的开发,一定是个假 Java 开发;没有解决过 Jar 包冲突的开发,不是一个合格的 Java 开发。在最近的项目里,我们需要使用 Guava 的高版本 Jar 包,但是发现中间件依赖的是低版本且与高版本不兼容的 Jar 包,面对这种两难,我们肯定是“老婆”和“妈妈”都要救,于是我们开始寻求解决方案。


二、不兼容依赖冲突解决方案


“老婆”和“妈妈”都要救,怎么救?


首先,我们想到的是,能不能把需要用到的 Guava 高版本的代码拷出来直接放到我们的工程中去,但是这样做会带来几个问题:


  • Guava 作为一个功能丰富的基础库,某一部分的代码往往与其他很多代码都存在依赖关系,这会造成牵一发而动全身,工作量会比预想的要大很多;


  • 拷贝出来的代码只能自己手动维护,如果官方修复了问题或者重构了代码或者增加了功能,我们想要升级的话,那么只能重头再来一遍。于是,我们只能另外想其他的方案,这个只能作为最后的兜底方案。


然后,我们在想,一个 Java 类被加载到 JVM 虚拟机里区别于另一个 Class,其一是它们俩全路径不一样,是风马牛不相及的两个不同的类,但却是被不同的类加载器加载的,在 JVM 虚拟机里它们仍然被认为是两个不同的 Class。所以,我们就在想从类加载器上来寻求解决方案。在阿里巴巴内部,有一个 Pandora 的组件,正如其名就像一个魔盒,它会把中间件的依赖都装到 Pandora 里(内部叫做 Sar 包),这样的话,就能避免在中间件和业务代码直接出现“老婆和妈同时掉进水里,先救谁”的两难境地。


同样,在类似的场景比如应用合并部署也能发挥威力。但是 Pandora 只在阿里内部使用并未开源。在蚂蚁金服,也有一个这样的组件,并且开源了,叫做 SOFAArk(官方网址,感兴趣的可以去官网了解 SOFAArk 的原理和使用),我们感觉已经找到了那个 Mr.Right,于是我们开始研究 SOFAArk 如何使用。和 Pandora 一样,SOFAArk 也是通过使用不同的 ClassLoader 加载不同版本的三方依赖,进而隔离类,彻底解决包冲突的问题,这就要求我们需要将相关的依赖打包成 Ark Plugin(参见 SOFAArk 官方文档)。


对于公司来说,这样的方案收益是比较大的,打包成 Ark Plugin 后整个公司都能够共享,业务方都能受益,但是对于我们一个项目来说,采用这样的方案无疑过重了。于是,我们与中间件同学联系,询问是否有计划引入类似的隔离组件解决中间件和业务代码之间的依赖冲突问题,得到的答复是公司目前包冲突并不是一个强烈的痛点,暂时没有计划引入。于是,我们只能暂且搁置 SOFAArk,继续寻找新的解决方案。



接着,我们在想既然 Pandora/SOFAArk 采用类加载隔离了同一路径的类,那么如果我们把冲突的两个版本库的 groupId 变得不一样,那么即使同名的类全路径也是不一样的,这样在 JVM 里面必然是不同的 Class。如果把 Pandora/SOFAArk 的隔离方式称之为逻辑隔离的话,这种就相当于物理隔离了。要实现这一点,借助 IDE 的重构功能或者全局替换的功能就能比较容易的实现这一点。


正在我们准备撸起袖子动手干的时候,我们不禁在想,这样的痛点应该早就有人遇到,尤其像 Guava、Commons 这类的基础类库,冲突在所难免,前人应该已经找到了优雅的挠痒姿势。于是,我们就去搜索相关的文章,果不其然,maven-shade-plugin 正是那优雅的挠痒姿势,这个 Maven 插件的原理正是将类的包路径进行重新映射,达到隔离不兼容 Jar 包的目的。

三、maven-shade-plugin 解决依赖冲突


最后如何来配置和使用 maven-shade-plugin 将 Guava 映射成我们自己定制的 Jar 包,实现与中间件 Guava 的隔离。整个的过程还是比较清晰明了的,主要是创建一个 Maven 工程,引入依赖,配置我们要发布的仓库地址,引入编译打包插件和 maven-shade-plugin 插件,配置映射规则(标签之间部分),然后编译打包发布到 Maven 仓库。pom.xml 的配置如下:


<?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"> <modelVersion>4.0.0</modelVersion>
<groupId>com.shaded.example</groupId> <artifactId>guava-wrapper</artifactId> <version>${guava.wrapper.version}</version>
<name>guava-wrapper</name> <url>https://example.com/guava-wrapper</url>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!- 版本与 guava 版本基本保持一致 -> <guava.wrapper.version>27.1-jre</guava.wrapper.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
<dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.1-jre</version> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>${maven.compiler.source}</source> <target>${maven.compiler.target}</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.3.2</version> <executions> <execution> <id>default-jar</id> <goals> <goal>jar</goal> </goals> <phase>package</phase> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>2.4</version> <executions> <execution> <id>default-sources</id> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.1</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <!-- 重命名规则配置 --> <relocations> <relocation> <!-- 源包路径 --> <pattern>com.google.guava</pattern> <!-- 目标包路径 --> <shadedPattern>com.google.guava.wrapper</shadedPattern> </relocation> <relocation> <pattern>com.google.common</pattern> <shadedPattern>com.google.common.wrapper</shadedPattern> </relocation> <relocation> <pattern>com.google.thirdparty</pattern> <shadedPattern>com.google.wrapper.thirdparty</shadedPattern> </relocation> </relocations> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"/> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build>
<distributionManagement> <!- Maven仓库配置,略 -> </distributionManagement></project>
复制代码


项目引入这个新打包的 guava-wrapper 后,import 选择从这个包导入我们需要的相关类即可。如下:


<dependency>  <groupId>com.vivo.internet</groupId>  <artifactId>guava-wrapper</artifactId>  <version>27.1-jre</version></dependency>
复制代码


四、结语


为了在同一个项目中使用多个版本不兼容的 Jar 包,我们首先想到手动自行维护代码,但是工作量和维护成本很高,接着我们想到通过类加载器隔离(开源方案 SOFAArk),但是需要将相关依赖都打包成 Ark Plugin,解决方案无疑有点过重了,最后通过 maven-shade-plugin 插件重命名并打包,优雅地解决了项目中不兼容多个版本 Jar 包的冲突问题。从问题出来,我们一步一步探寻问题的解决方案,最终的 maven-shade-plugin 插件方案虽然看似与手动自行维护代码本质一致,看似回到了原点,但其实最终的方案优雅性远比最开始高得多,正如人生的道路那样,螺旋式上升,曲线式前进。


如果遇到类似需要支持版本不兼容 Jar 包共存的场景,可以考虑使用 maven-shade-plugin 插件,这种方法比较轻量级,可用于项目中存在个别不兼容 Jar 包冲突的场景,简单有效,成本也很低。但是,如果 Jar 包冲突现象比较普遍,已成为明显或者普遍的痛点,还是建议考虑文中提到的类似 Pandora、SOFAArk 等类加载器隔离的方案。


作者:vivo 互联网服务器团队-Zhang Wei

发布于: 10 分钟前
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
版本不兼容Jar包冲突该如何是好?