共计 5418 个字符,预计需要花费 14 分钟才能阅读完成。
一、引言
“老婆”和“妈妈”同时掉进水里,先救谁?
常言道:编码五分钟,解抵触两小时。作为 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