浅谈在Jar中同名类冲突问题及覆写第三方Jar中的类

正文

一、同名类产生的原因

当同名类(完全限定名)在自己编写的工程中肯定不会出现,但是这无法保证在服务所依赖的JAR中不会出现。在多人协作时,如果每个人在自己负责的服务中都需要引用一段公共代码,同时又把它抽象到一个类的一个方法中,并且大家还采用了代码拷贝的方式进行维护。此时如果有1个人对这段代码做了定制化的修改,但没有修改方法或类的名称。这个时候如果有场景需要将多个服务合并打包发布的时候,这个定制化的类就变成了一颗定时炸弹。由于同一个类加载器对于同名类只会加载一次,那么一旦JVM的选取策略发生了变化,那么就会爆发不可控的风险。

二、JAR包加载的顺序

问题产生的原因其实是简单的,但是又是什么原因造成同名类在不同的环境下被选择了不同的版本?

由于同名类是在不同JAR出现的,所以这个问题就转化成了JAR的加载顺序是由什么因素导致的

类加载器级别

JVM的类加载其实是一个树形结构,而JVM在加载类的时候采用的是双亲代理模式,层级越高的类加载器会越早加载其路径下的类。下面是JVM类加载器的简单说明。

  +------------------------+
  |Bootstrap ClassLoader   | : 引导类加载器,负责加载JAVA核心类库,
  +------------------------+   	JAVA_HOME/jre/lib/rt.jar,或者
              |                	sun.boot.class.path路径下的内容
              v
  +------------------------+
  |Extension ClassLoader   | : 扩展类加载器,继承于ClassLoader,加载
  +------------------------+  	JAVA_HOME/jre/ext/lib/*.jar,或者
              |                	java.ext,dirs目录下的内容,它的作用是
              v               	加载jvm提供的扩展库
  +------------------------+
  |System ClassLoader      | : 系统类加载器,继承于ClassLoader,加载
  +------------------------+   	应用程序classpath下的类或者java.class.path
                               	下的类,一般应用程序使用的加载器都是该加载器

由于上面出问题的类都属于系统类加载器管理,所以我们可以排除类加载器级别导致的问题。

系统的文件加载顺序

当JAR包都属于同一个类加载器是,它们的加载顺序就是由系统的文件加载顺序来决定的。这往往就是因为环境的不同导致诡异的类冲突问题的元凶。由于大多数的容器的ClassLoader在获取对应录下的文件列表时是不会自己排序的,所以加载的顺序就依赖于底层文件系统返回的顺序。当系统的文件排序规则不一致时,就会发生上面的现象。在Linux系统中文件的顺序是由iNode的顺序来决定的。这时让我们来看一下两个不同环境下对相同JAR的排序是什么样的。

a. 公司环境

[root@home lib]# ls -i | sort
2359300 A.jar -- 该JAR包中的类是希望调用的
2359301 B.jar

b. 客户环境

[root@partner lib]# ls -i | sort
1235909 B.jar
1236002 A.jar -- 该JAR包中的类是希望调用的

此时很明显可以看到上面的现象是由文件系统的排序不同导致的。

三、如何检测同名类冲突

在构建工具中使用插件

当使用maven时可以采用maven-enforcer-plugin,这个强大的maven插件,配合extra-enforcer-rules工具,能自动扫描Jar包将冲突检测并打印出来。

遗憾的是gradle中没有相应的插件,若日常使用的构建工具是gradle,就要💭别的方式

使用Linux命令

为了弥补gralde中没有现成插件的遗憾,当我们知道冲突的类名以后,我们可以在Linux中执行以下命令来检索冲突类出现在那些jar包中

#checkEnforce.sh

#/bin/sh

jarlist=`ls ./|grep jar`
for jarname in $jarlist;do
echo "检索 $jarname"
jar -tvf $jarname|grep class|grep $1
done

扩展:如何覆盖第三方jar的类

有时,出于一些原因,需要覆写第三方jar中的类,比如这次SpringBoot 2.6.0版本,长期不维护的springfox与其不兼容,对此除了直接修改其源码,另一种方式就是对其中的类进行覆写。

相关知识:

  • 自己写的类和jar的类都是通过AppClassLoader来加载
  • jdk下的内容先加载,然后是我们自己的classpath,最后再去加载jar包。 加载class的时候加载成功的不会再次加载,所以可以用自己的类去覆盖第三方jar的类(只要保证包名和类名等一致)

Leave a Reply

Your email address will not be published. Required fields are marked *

lWoHvYe 无悔,专一