工作半年遇到最奇葩的問題
工作半年遇到最奇葩的問題
背景
公司最近買了一套專案,在啟動的時候出現了一系列奇怪的問題,對方的技術棧要求是Tomcat7啟動,但是由於我們公司出於安全的考慮所以是要求用Tomcat9進行啟動的。
問題描述
下面情況都是相同war包相同Tomcat情況下
系統 | Tomcat版本 | 能否啟動 |
---|---|---|
Windows | Tomcat7 | 能 |
Windows | Tomcat9 | 能 |
macOS | Tomcat7 | 能 |
macOS | Tomcat9 | 不能 |
Linux | Tomcat7 | 能 |
Linux | Tomcat9 | 不能 |
由於對於專案的不熟悉,導致找了很久才找出來原因。查詢過程就是用了阿里開源的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
就可以輸出自然順序,你會發現沒有什麼規律可言。
解決
到這裡上面描述的所有問題我們都能解釋通了,接下來就該如何解決了。
- 修改Tomcat9的原始碼,在獲取所有Jar包的時候,也對它進行排序
- 解決掉有衝突的檔案
第一種解決辦法只能解決一時問題,即專案能正常啟動起來,但是一旦隨後涉及到了相關類的修改,那麼衝突類的哪個類呢?那麼這個問題肯定是一個定時炸彈。
第二種方案是找到有衝突的檔案,然後找出不用的那個給刪除掉,但是發現刪除一個又會蹦出其他的,刪除了好幾個以後發現由於買的專案程式碼不規範,所以這種現象特別多,如果單純靠手工篩選的話極其麻煩。於是就寫了一個指令碼跑出專案中所有同名類的檔案。
指令碼思路
- 找出所有Java檔案
- 找到Java檔案上
package
那一行,然後讀取此行 package
後面的包名與類名拼接存入List集合中- 篩選出集合中相同的內容
具體的指令碼程式碼可以去GitHub中檢視。使用簡單說明,將想要掃描的專案程式碼全放在一個資料夾中,例如我要掃描A、B、C、D四個專案。
--/
--scanDir
--A
--B
--C
--D
那麼我只要引入了Jar包以後如下呼叫即可
List<String> list = FindDuplicate.findDuplicatePath("/scanDir/");
返回的是一個集合,一條記錄表示有一個組衝突檔案,兩個衝突檔案路徑被
||||||||
隔開