工做半年遇到最奇葩的问题

工做半年遇到最奇葩的问题

背景

公司最近买了一套项目,在启动的时候出现了一系列奇怪的问题,对方的技术栈要求是Tomcat7启动,可是因为咱们公司出于安全的考虑因此是要求用Tomcat9进行启动的。git

问题描述

下面状况都是相同war包相同Tomcat状况下github

系统 Tomcat版本 可否启动
Windows Tomcat7
Windows Tomcat9
macOS Tomcat7
macOS Tomcat9 不能
Linux Tomcat7
Linux Tomcat9 不能

因为对于项目的不熟悉,致使找了好久才找出来缘由。查找过程就是用了阿里开源的Arthas 编译出正在运行时出问题的那个类,发现两个类来源于不一样的Jar包,因此问题就转向了Jar的加载顺序是由什么因素致使了。web

问题深究

两个同路径名同类名的类在类加载器只会加载一次安全

出现这个问题的时候查了些资料知道,JVM的类加载是一个树形的结构,JVM在加载的过程采用的双亲委派的模式,层级越高,那么类加载器会越早的加载其路径下的类。下面是Tomcat的类加载器所在的级别。架构

Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 .

咱们能够知道出问题的两个Jar是在相同的类加载器中,因此排除了不一样级别类加载器致使的问题。app

Tomcat7加载Jar包原理

Tomcat本身实现了本身的类加载器,用于加载本身本地项目中jar包中的全部class文件,因此在相同的类加载器下,若是有相同路径名和类名那么加载顺序就是根据jar包的顺序来决定的。谁的jar包先进来,那么就先加载哪一个类。webapp

可是为何在Tomcat7全部环境都能运行正常,而在Tomcat9中就不行了呢?因而就查看了Tomcat7的源码在Context加载项目中的jar包时post

Tomcat7加载jar部分,在WebappLoader.setRepositories()方法中,粘贴出其中重要代码。操作系统

// Looking up directory /WEB-INF/lib in the context
    NamingEnumeration<NameClassPair> enumeration = null;
    try {
        //这一句是得到jar包的路径
        enumeration = libDir.list("");
    } catch (NamingException e) {
        IOException ioe = new IOException(sm.getString(
                "webappLoader.namingFailure", libPath));
        ioe.initCause(e);
        throw ioe;
    }

list是得到了应用中WEB-INF下lib下全部jar包的路径。咱们跟踪进去发现FileDirContext 的list方法中有下面这一句调试

Arrays.sort(names);             // Sort alphabetically

咱们能够发如今Tomcat7中对得到全部jar包做了一个排序的动做。对jar包进行了首字母a-z进行了排序。而咱们所指望加载的那个jar包首字母正好在错误jar包的前面。

Tomcat9加载Jar包原理

上面咱们知道了为何在全部项目中Tomcat7能启动起来的缘由了,是由于Tomcat7作了排序的动做,那么在Tomcat9加载Jar包时,又是怎么作的呢?

Tomcat9在加载源码的时候是经过StandardRoot.processWebInfLib()方法进行加载的

protected void processWebInfLib() throws LifecycleException {
        WebResource[] possibleJars = listResources("/WEB-INF/lib", false);
        for (WebResource possibleJar : possibleJars) {
            if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
                createWebResourceSet(ResourceSetType.CLASSES_JAR,
                        "/WEB-INF/classes", possibleJar.getURL(), "/");
            }
        }
    }

在这咱们能够看到Tomcat没有对取出来的Jar做任何动做,仅仅是File file = new File()这样遍历出来了。那么为何相同的Tomcat9相同的War包在Windos能启动起来,可是在macOS和Linux中都启动不起来呢?通过试验发现Java的获取文件夹下面的全部文件是跟操做系统的文件系统有关系的,相同的文件夹内容,在Windows中取出来,输出名字你会发现输出是通过a-z排序过的,可是在macOS或者Linux中你能够根据命令ll -fi就能够输出天然顺序,你会发现没有什么规律可言。

解决

到这里上面描述的全部问题咱们都能解释通了,接下来就该如何解决了。

  1. 修改Tomcat9的源码,在获取全部Jar包的时候,也对它进行排序
  2. 解决掉有冲突的文件

第一种解决办法只能解决一时问题,即项目能正常启动起来,可是一旦随后涉及到了相关类的修改,那么冲突类的哪一个类呢?那么这个问题确定是一个定时炸弹。

第二种方案是找到有冲突的文件,而后找出不用的那个给删除掉,可是发现删除一个又会蹦出其余的,删除了好几个之后发现因为买的项目代码不规范,因此这种现象特别多,若是单纯靠手工筛选的话极其麻烦。因而就写了一个脚本跑出项目中全部同名类的文件。

脚本思路

  1. 找出全部Java文件
  2. 找到Java文件上package那一行,而后读取此行
  3. package后面的包名与类名拼接存入List集合中
  4. 筛选出集合中相同的内容

具体的脚本代码能够去GitHub中查看。使用简单说明,将想要扫描的项目代码全放在一个文件夹中,例如我要扫描A、B、C、D四个项目。

--/
  --scanDir
   --A
   --B
   --C
   --D

那么我只要引入了Jar包之后以下调用便可

List<String> list = FindDuplicate.findDuplicatePath("/scanDir/");

返回的是一个集合,一条记录表示有一个组冲突文件,两个冲突文件路径被||||||||隔开

脚本代码

往期关于Tomcat文章

相关文章
相关标签/搜索