深入理解jar包衝突的本質
- 2019 年 10 月 7 日
- 筆記
前言
上篇文章 記一次log4j不列印日誌的踩坑記 介紹了遇到的log4j踩坑經歷和解決方法,這篇文章我們重點來學習和了解下有關Java中日誌組件的內容,在這之前,其實在我的頭腦里,並沒有形成系統的日誌框架知識,原因其實是一直沒有重視過這塊,之前都是各種拷貝改改能跑就行,並不理解相關的架構和原理,這次趁著這個機會正好來系統了解一下,除了要系統的理解日誌框架大多數知識外,我們還要學習一個非常關鍵的知識,就是關於Java默認的類載入器載入jar包的順序問題,不誇張的說,只有理解了這個,才能搞明白jar衝突問題發生的本質。
Java日誌框架一覽
- java.util.logging (JUL)
JDK自帶日誌組件,使用方式簡單,不需要依賴第三方日誌組件。支援將日誌列印到控制台,文件,甚至可以將日誌通過網路列印到指定主機。相對於第三方獨立日誌框架來說,支援的日誌級別比較少,功能也比較單一。
2,apache commons logging
Apache Commons Logging也稱(JCL),為啥不叫ACL而是JCL呢,其實這裡面有個小典故,曾經Apache 基金會用於管理各個 Java 子項目,諸如 Ant、Commons、JAMES 等都使用Jakarta,2011 年 12 月,在所有子項目都被遷移為獨立項目後, Jakarta 名稱就不再使用了。其實2011年12月之前,apache commons logging的別名是Jakarta commons logging 故稱為JCL,因為歷史問題,所以一直沒有更正。
JCL提供了一個輕量級的日誌抽象,為應用程式提供統一的日誌API。允許用戶使用具體的日誌實現,如:log4j,Avalon LogKit,java.util.logging。當然,JCL同時也提供了一個簡單的日誌實現org.apache.commons.logging.impl.SimpleLog,將日誌輸出到System.err。目前JCL已經停止更新了,最新發布版本為1.2 Release – July 2014。
3,log4j
Apache的一個開放源程式碼項目,log4j可以當之無愧地說是Java日誌框架的元老,1999年發布首個版本,2012年發布最後一個版本,2015年正式宣布終止,至今還有無數的系統在使用log4j,甚至很多新系統的日誌框架選型仍在選擇log4j。log4j是通過一個.properties的文件作為主配置文件的log系統,由於自身存在太多弊端,比如高並發情況下死鎖問題,不支援佔位符等
所以已經在2015年8月份停止更新,最後一個版本為log4j 1.2.17。
4,logback
Logback是由log4j創始人設計的另一個開源日誌組件,可以認為是log4j的改進版,非常簡單靈活,官方網站:http://logback.qos.ch。它當前分為下面幾個模組:
logback-core:核心程式碼模組 logback-classic:log4j的一個改良版本,同時實現了slf4j的介面,這樣你如果之後要切換其他日誌組件也是一件很容易的事 logback-access:訪問模組與Servlet容器集成提供通過Http來訪問日誌的功能
5,log4j2
logback和log4j2其實都是log4j的後代,最初都是出自同一個作者,但其後來放棄了log4j,又開發了新的logback,而log4j則由社區接管,在2014年底才推出log4j2,比logback晚了好幾年,這期間log4j2大量吸收了slf4j和logback的一些優點(比如日誌模板),同時應用了不少的新技術,由於採用了更先進的鎖機制和LMAX Disruptor庫,log4j2的性能優於logback,特別是在多執行緒環境下和使用非同步日誌的環境下,此外也支援佔位符,插件化,gc優化,支援java8等功能。
6,slf4j
slf4j也稱The Simple Logging Facade ,即簡單日誌門面(Simple Logging Facade for Java),有點類似於我們的USB介面,制定了統一的介面訪問標準,並不是具體的日誌實現,也就是說slf4j並不是為了替代前面的幾個日誌框架,而是合作互惠關係,當然你也可以不合作,拒絕使用USB介面,那就會導致你這個產品兼容性太差,綁定太緊,肯定不是用戶所期待的。這也是slf4j與前面介紹過的5個日誌框架是最大的區別之處。簡單點說sfl4j是一層api介面,其他的5個日誌框架都是實現類。
slf4j的出現是為了解決,一個項目中出現了多個日誌依賴,從而導致項目難以管理和維護。舉個例子假如你的項目依賴log4j,某天你引入了redis框架,而redis又依賴logback,這個時候你得同時管理兩套日誌框架,最為致命的是多年後log4j已經廢棄不維護了,這個時候如果你想換新的log4j2框架,那就意味著你需要改原來到處散落的log4j程式碼,這導致了耦合太近,維護複雜。為了解決這個問題,slf4j就誕生了,其制定了統一的api介面,從設計模式的角度考慮,它是用來在log和程式碼層之間起到門面的作用。對用戶來說只要使用slf4j提供的介面,即可隱藏日誌的具體實現。這與jdbc相似。使用jdbc也就避免了不同的具體資料庫。使用了slf4j可以對客戶端應用解耦。因為當我們在程式碼實現中引入log日誌的時候,用的是介面,所以可以實時的根據情況來調換具體的日誌實現類。這就是slf4j的作用。當然前面提到的JCL也有門面的功能,但後來被slf4j全面超越,故已經衰退,所以slf4j已經成了事實上日誌門面介面標準。
官網給出的一張圖,非常形象的表達了這種關係:

日誌組件的構造
大體上來說共有三個部分:
(1)日誌門面介面
(2)橋接器
(3)日誌框架具體實現
如下面的一張圖所示:

從上面的圖中我們可以看到日誌門面介面會通過橋接綁定的方式與下游的日誌框架類進行綁定,需要注意的是slf4j在運行的時候,只會與下游的實現類綁定一次,也就是說slf4j,有且只能在運行時綁定一款日誌實現框架,那麼該有同學該提問了,如果下游同時存在多個日誌實現框架,會發生什麼情況?
這個問題很有意思,首先slf4j在運行時會列印所有在classpath裡面發現的所有日誌實現類,然後會選擇第一個被類載入器載入的實現類作為底層的真正的日誌組件,之後其他的實現類會被忽略,因為Java類載入器在載入多個同包名同類名的class的時候,只有第一個會成功,後面的不會被載入,這也是雙親委派模型的經典之處。
jar包衝突之謎
ok,我們回顧下上篇文章末尾提到的問題:
(1)同樣的部署包,為什麼有的機器會正常輸出log,而有的卻失敗了呢?
(2)同樣的slf4j 綁定的實現類,為什麼也會發生有的機器可以輸出,有的會失敗呢?
回答:
第一個原因:
Java類載入器載入同一個目錄下的jar包的順序是隨機的,會受作業系統的文件系統影響。
第二個原因:
載入的jar包中出現了衝突,包括同jar不同版本和不同jar但存在同包名同類名的class,其實在包衝突的情況下,如果類載入器按照正常的順序載入,是沒有問題的,但如果恰好衝突的jar包,載入的順序發生了顛倒,那麼就極有可能引發莫名其妙的問題,這也是為什麼篇文章中提到的在200多台機器中,僅僅只有50多台發生了問題,其他的缺沒有出現任何問題,這也從側面證實了jar順序的問題。
關於Java類載入器載入jar包的順序是隨機的,我特意找了相關的理論資料,因為僅僅從現象上推斷還不夠嚴謹,必須有權威的資料來說明才行:
(一)來自Oracle JDK官網的一段說明:
https://docs.oracle.com/javase/7/docs/technotes/tools/solaris/classpath.html
The order in which the JAR files in a directory are enumerated in the expanded class path is not specified and may vary from platform to platform and even from moment to moment on the same machine. A well-constructed application should not depend upon any particular order. If a specific order is required then the JAR files can be enumerated explicitly in the class path.
含義:同一個目錄下,jvm載入jar包順序是無法保證的,每個系統的都不一樣,甚至同一個系統不同的時刻載入都不一樣。良好設計的系統不應該依賴任何特定的載入順序。(畫外音:最好不要出現多個衝突的jar包)
(二)其他搜集資料
https://stackoverflow.com/questions/5474765/order-of-loading-jar-files-from-lib-directory/26642798
https://www.maheshsubramaniya.com/article/understanding-how-jars-are-loaded-into-jvm-from-a-directory.html
jar衝突的解決方法
(1)上線前 + 提前檢測
這裡分兩種情況:jar衝突可避免:在部署前檢測是否有衝突的可能,如果有就提前移除衝突的依賴,減少這種問題發生的可能。推薦使用maven的maven-enforcer-plugin插件,可以幫助檢測同包名+同類名的依賴衝突 jar衝突不可避免 如果jar衝突不可避免,這個時候是不能直接移除依賴的,否則會引起另外一個組件報異常,這個時候 可以使用maven-shade-plugin插件,來對同名同包的其中一個版本進行 」rename「,不影響正常功能,相當於是 繞過了衝突。
(2)上線後 + 臨時解決
如果上線前沒有注意到這些,導致在上線後才發現問題,那麼我們可以採用臨時處理方式,只需要移除與衝突的相關的jar包即可。比如在我們的項目里使用的是log4j 2.x 版本,所以只需要臨時移除如下衝突的log4j 1.x的jar包,然後重啟即可,另外在記得下一個版本中去掉無用的依賴。
總結
想必現在,大家應該對jar衝突的問題,應該有了一個深刻的認知了,而不是僅僅停留在問題的表面,這裡面關鍵點在於,要認知到JVM載入jar順序是不確定的,其會受不同的作業系統平台帶來的影響,具體細節可以看我發的資料鏈接。正如Oracle JDK官網文檔所言,良好的系統設計不應該依賴jar包的載入順序,其實也在提示我們最好不要有衝突存在的情況,如果衝突真的不可避免,那麼可以通過maven插件來間接的繞過衝突。
最後在多說一句,遇到問題時,不要有意排斥,而應該抓住機會,迎面而上研究其根本原因,排查的過程也是提升問題解決能力的一個重要方式,每一個優秀的工程師和技術專家都必定經歷過無數個複雜問題的洗禮才得以成長起來。
相關文章: