天天看點

工作半年遇到最奇葩的問題工作半年遇到最奇葩的問題

工作半年遇到最奇葩的問題

背景

公司最近買了一套項目,在啟動的時候出現了一系列奇怪的問題,對方的技術棧要求是Tomcat7啟動,但是由于我們公司出于安全的考慮是以是要求用Tomcat9進行啟動的。

問題描述

下面情況都是相同war包相同Tomcat情況下

系統 Tomcat版本 能否啟動
Windows Tomcat7
Tomcat9
macOS
不能
Linux

由于對于項目的不熟悉,導緻找了很久才找出來原因。查找過程就是用了阿裡開源的

Arthas

編譯出正在運作時出問題的那個類,發現兩個類來源于不同的Jar包,是以問題就轉向了Jar的加載順序是由什麼因素導緻了。

問題深究

兩個同路徑名同類名的類在類加載器隻會加載一次

出現這個問題的時候查了些資料知道,JVM的類加載是一個樹形的結構,JVM在加載的過程采用的雙親委派的模式,層級越高,那麼類加載器會越早的加載其路徑下的類。下面是Tomcat的類加載器所在的級别。

Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 .
           

我們可以知道出問題的兩個Jar是在相同的類加載器中,是以排除了不同級别類加載器導緻的問題。

Tomcat7加載Jar包原理

Tomcat自己實作了自己的類加載器,用于加載自己本地項目中jar包中的所有class檔案,是以在相同的類加載器下,如果有相同路徑名和類名那麼加載順序就是根據jar包的順序來決定的。誰的jar包先進來,那麼就先加載哪個類。

但是為什麼在Tomcat7所有環境都能運作正常,而在Tomcat9中就不行了呢?于是就檢視了Tomcat7的源碼在Context附加元件目中的jar包時

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文章