浅谈在JAR中同名类冲突问题

楔子

这段时间再给客户部署服务的时候,发现有一个服务在公司通过了所有测试,但是却在客户现场的生产环境上报出了一个诡异的问题。由于对出问题的那段代码不熟悉,所有前后折腾了2个小时才定位出来,原来是服务依赖的两个JAR包中都同时出现了一个同名类(该类的完全限定名一样),并且这两个类中都包含了同一个方法。在公司环境中执行的是正确的那个方法,而在客户现场执行的是错误的方法。这就导致了在公司没有问题的服务部署在现场就出现了问题。

正文

一、同名类产生的原因

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

二、JAR包加载的顺序

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

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

  1. 类加载器级别

    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
                                   下的类,一般应用程序使用的加载器都是该加载器

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

  2. 系统的文件加载顺序

    当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包中的类是希望调用的

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

三、如何检测同名类冲突

  1. 在构建工具中使用插件

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

    遗憾的是gradle中没有相应的插件,而我们日常使用的构建工具就是gradle

  2. 使用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
    
    [root@XXXXX script]# sh checkEnforce.sh className

至此整个分析过程已经全部结束。现在做一下下期预告,我们来讲讲如何使用Shell来实现服务的守护进程。

欢迎关注微信公众号,在这里可以提前看到下一期文章哦~

你可能感兴趣的:(坑)