1. 問題回顧
問題背景是在進行中臺應用中間件遷移過程中,發現存在項目啟動失敗或者項目正常啟動(jsf正常掛載并正常運行,mq正常發送和消費)但是無任何日志打印現象。更奇怪的是不打印日志竟然是偶發的,在測試環境中多次部署都未出現項目啟動但無日志打印情況,而且玄學的是生產環境兩臺機器,其中一臺正常日志打印,另一臺無任何日志打印(應用運行正常)。
通過多次重啟無日志打印機器仍未恢復日志打印,最終通過排查發現問題在于項目中引入的多個日志jar包沖突,進而導致無日志打印現象。
圖1 場景1項目啟動失敗和場景2項目目正常啟動但是無日志打印
圖2 運行項目所包含的日志jar包
2. 日志框架
日志框架通常分為兩大類:
?
日志門面
(Logging Facade):如SLF4J(Simple Logging Facade for Java)和JCL(Apache Commons Logging),它們提供了一層抽象接口,使得開發者可以編寫與具體日志實現無關的代碼。這樣在不修改代碼的情況下,可以靈活地切換底層的日志實現框架。?
日志實現
(Logging Implementation):如Logback、Log4j、java.util.logging (JUL)等,它們是具體的日志庫,負責實際的日志生成、處理和存儲工作。這些實現直接響應門面層的請求,執行日志操作。
圖3 日志門面和日志實現
日志門面使用到了一種設計模式:門面模式,接下來簡單介紹下門面模式。下面是門面模式的一個典型調用過程,其核心為外部與一個子系統的通信必須通過一個統一的外觀對象進行,使得子系統更易于使用。 下圖中客戶端不需要直接調用幾個子系統,只需要與統一的門面進行通信即可。
圖4 門面模式的一個典型調用過程
門面模式的核心為Facade即門面對象,核心為幾個點:
?知道所有子角色的功能和責任。?將客戶端發來的請求委派到子系統中,沒有實際業務邏輯。?不參與子系統內業務邏輯的實現。
舉個栗子
當你通過電話給商店下達訂單時, 接線員就是該商店的所有服務和部門的外觀。 接線員為你提供了一個同購物系統、 支付網關和各種送貨服務進行互動的簡單語音接口。
注:具體想要了解門面模式的可以參看這篇文章。?
2.1 為什么要引入日志門面?
回答這個問題之前,我們先看看如果需要用上面幾個日志框架來打印日志,一般怎么做,具體代碼如下:
// 使用log4j,需要log4j.jar import org.apache.log4j.Logger; Logger logger_log4j = Logger.getLogger(Test.class); logger_log4j.info("Hello World!"); // 使用log4j2,需要log4j-api.jar、log4j-core.jar import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; Logger logger_log4j2 = LogManager.getLogger(Test.class); logger_log4j2.info("Hello World!"); // logback,需要logback-classic.jar、logback-core.jar import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; Logger logger_logback = new LoggerContext().getLogger(Test.class); logger_logback.info("Hello World!");
從上面不難看出,使用不同的日志框架需要要引入不同的jar包,使用不同的代碼獲取Logger。如果項目升級需要更換不同的框架,那么就需要修改所有的地方來獲取新的Logger,這將會產生巨大的工作量。
基于此,我們需要一種接口來將不同的日志框架的使用統一起來,這也是為什么要使用SLF4J的原因。
日志門面——SLF4J
即簡單日志門面(Simple Logging Facade for Java),不是具體的日志解決方案,它只服務于各種各樣的日志系統。按照官方的說法,SLF4J是一個用于日志系統的簡單Facade,允許最終用戶在部署其應用時使用其所希望的日志系統。
另一個常用的日志門面——JCL
常見的日志門面還有一個叫JCL(Jakarta Common logging),這個是在2001年左右旨在解決日志實現多樣性的問題,允許開發者編寫與具體日志實現無關的代碼,并作為第一個廣泛使用的日志門面被提出了。其中SLF4J日志門面是在2006年,由Ceki Gülcü,同時也是Log4j的創始人,推出了SLF4J,這是一個更為先進、設計更優的日志門面,旨在克服JCL存在的問題,如類加載沖突和運行時綁定的不確定性。
2.2 SLF4J和JCL的主要區別?
1. 動態與靜態綁定
?JCL:采用
動態綁定
機制,意味著它在
運行時
通過類加載器查找并決定使用哪個日志實現(如Log4j、JUL等)。這種方式
可能導致類加載順序
問題,尤其是在類路徑復雜的應用中,可能會引起
不確定性和潛在的類加載沖突
。?SLF4J:提倡
靜態綁
定,
即在編譯時就確定日志實現
。SLF4J要求
在類路徑中明確包含一個到具體日志實現的橋接器
(如slf4j-log4j12.jar),這樣在編譯時就能確切知道日志將如何被處理。這減少了運行時的不確定性,提高了性能,并且在日志實現未正確配置時能給出更明確的錯誤提示。
2. 錯誤處理與診斷
?JCL:如果日志實現沒有正確配置,可能會導致難以診斷的錯誤,比如
NoClassDefFoundError
或
ClassNotFoundException
,因為JCL在
運行時
才會發現日志實現不可用。?SLF4J:在
初始化
時,如果發現
不兼容的或缺失
的日志實現,SLF4J會立即拋出一個明確的
警告或錯誤信息
,幫助開發者快速定位問題。
3. 性能
?SLF4J:通常被認為比JCL有更高的性能,尤其是當使用靜態綁定時,因為減少了解析和查找日志實現的開銷。
4. API設計
?SLF4J:提供了更簡潔、更易用的API,支持更靈活的日志級別控制和參數化日志消息,有助于減少字符串拼接的開銷。
5. 社區與支持、更新與活躍度
?SLF4J:隨著時間的推移,SLF4J因其設計優勢獲得了更廣泛的社區支持和采納,許多現代的Java庫和框架直接支持或推薦使用SLF4J。?SLF4J:相比JCL,SLF4J持續得到維護和更新,提供了對新特性和日志實現更好的支持。
綜上所述,SLF4J在設計上克服了JCL的一些缺陷,提供了更穩定、高效和易于使用的日志接口,因此在新項目中更受推崇。而JCL盡管仍在一些遺留系統中使用,但已逐漸被SLF4J取代。
2.3 常用的日志實現
一些趣聞
使用過Log4J和LogBack的同學肯定能發現,這兩個框架的設計理念極為相似,使用方法也如出一轍。其實這個兩個框架的作者都是一個人,Ceki Gülcü,土耳其軟件工程師。 Log4J 最初是基于Java開發的日志框架,發展一段時間后,作者Ceki Gülcü將Log4j捐獻給了Apache軟件基金會,使之成為了Apache日志服務的一個子項目。 又由于Log4J出色的表現,后續又被孵化出了支持C, C++, C#, Perl, Python, Ruby等語言的子框架。 然而,偉大的程序員好像都比較有個性。Ceki Gülcü由于不滿Apache對Log4J的管理,決定不再參加Log4J的開發維護。“出走”后的Ceki Gülcü另起爐灶,開發出了LogBack這個框架(SLF4J是和LogBack一起開發出來的)。LogBack改進了很多Log4J的缺點,在性能上有了很大的提升,同時使用方式幾乎和Log4J一樣,許多用戶開始慢慢開始使用LogBack。 由于受到LogBack的沖擊,Log4J開始式微。終于,2015年9月,Apache軟件基金業宣布,Log4j不再維護,建議所有相關項目升級到Log4j2。Log4J2是Apache開發的一個新的日志框架,改進了很多Log4J的缺點,同時也借鑒了LogBack,號稱在性能上也是完勝LogBack。性能這塊后續我會仔細分析。
根據這些日志實現的出現順序及特點整理出了一條時間線如下:
1999年: Log4j 1.x:由Ceki Gülcü(土耳其裔美國軟件工程師)創建,成為Java社區廣泛采用的第一個流行日志框架。它的出現使得開發者能夠更方便地控制日志記錄,包括日志級別、輸出格式和目的地。
2001年: JUL (Java Util Logging):隨著Java 1.4的發布,Oracle(當時是Sun Microsystems)引入了JUL作為標準的日志庫。雖然它是一個內置的解決方案,但由于API相對復雜,開發者普遍認為它不如Log4j好用。
2003年: JCL (Apache Commons Logging):Apache軟件基金會推出JCL,作為日志門面,旨在提供一個統一的API,使得開發者可以編寫與具體日志實現無關的代碼。然而,JCL在運行時動態加載日志實現的方式導致了類加載問題和性能問題。
2006年: SLF4J (Simple Logging Facade for Java):Ceki Gülcü,也是Log4j的創建者,推出了SLF4J,作為對JCL的改進。SLF4J強調靜態綁定,提高了性能和穩定性,并且支持更多的日志實現,如Logback、Log4j 1.x等。
2007年: Logback:Ceki Gülcü同時推出了Logback,作為Log4j的替代,設計為SLF4J的首選實現。Logback提供了更高效、更靈活的日志記錄功能,包括異步日志記錄和豐富的配置選項。
2010年: Log4j 2.x:Apache Log4j項目在2010年代進行了重大升級,推出了Log4j 2,它修復了Log4j 1.x的一些問題,提供了更好的性能和更多特性,如異步日志記錄和更強大的配置能力。
2010年至今: 微服務和云原生日志:隨著微服務和云原生應用的興起,日志收集和分析的需求變得更加復雜。工具如Loggly、Logstash、Fluentd、Elasticsearch、Kibana等開始流行,它們與各種日志實現配合,提供了日志的集中處理、搜索、分析和可視化。 現代輕量級日志框架:
TinyLog:針對簡單應用和資源受限環境,輕量級的日志框架如TinyLog應運而生,提供簡單易用的API,注重效率和小巧。
注:對TinyLog感興趣可參考這篇文章。?
圖5 日志演變路線
3. 日志門面和日志實現結合
3.1 日志門面如何和日志實現結合使用呢?
以比較常用的SLF4J為例,并結合現有比較常用的日志實現可歸納出以下幾種組合依賴結構(如圖6),即SLF4J綁定到具體日志實現時需要引入的jar包依賴。圖6最下方給出的不同顏色的含義,分別是抽象接口、原生支持SLF4J的實現、適配層、非原生支持SLF4J的實現。
1.抽象接口層都是slf4j-api,很好理解,因為slf4j主要就是做日志門面。2.原生支持SLF4J的實現:有logback、slf4j-simple.jar、slf4j-nop.jar。3.非原生支持SLF4J的實現,有log4j和jul,因為這兩個在SLF4J之前就出現了,后面SLF4j出現后,大家覺得這個日志門面很優秀,所以出現了適配SLF4J和log4j、jul的橋接包,也就是下圖中的slf4j-reload4j.jar和slf4j-jdk14.jar4.log4j2是最后出現的,可以說吸取了前面一些日志框架的優點,自成一體,所以未在下面的圖中出現。當然SLF4J和log4j2也可以搭配,使用log4j-slf4j-impl的橋接包。
注:logback、slf4j-simple.jar、slf4j-nop.jar之所以能天然支持SLF4J的接口是有原因的,slf4j-simple.jar、slf4j-nop.jar都是slf4j自帶的實現框架,本身就是按slf4j-api的接口開發的。logback之所以也天然適配SLF4J,有兩個原因,一是出現的先后原因,log4j ->JUL->JCL-> SLF4J -> logback -> log4j2,logback在SLF4J后面出現,第二個是因為這兩個都是同一個作者寫的。
圖6 SLF4J與日志實現結構圖
總結來說主要的日志門面和日志實現的依賴搭配如下:
?
slf4j + logback
: slf4j-api.jar + logback-classic.jar + logback-core.jar?
slf4j + log4j 1.
x : slf4j-api.jar +
slf4j-log412.jar
+ log4j.jar?
slf4j + jul
: slf4j-api.jar + slf4j-jdk14.jar?
slf4j無日志實現
:slf4j-api.jar + slf4j-nop.jar
(日志不會被記錄:適合調試和測試環境,避免不必要的輸出)
注意到這里沒有log4j2依賴jar的關系,和log4j2配合需要導入log4j2的log4j-api.jar、log4j-core.jar和橋接包log4j-slf4j-impl.jar。
?
slf4j + log4j 2.x
:slf4j-api.jar + log4j-api.jar + log4j-core.jar + log4j-slf4j-impl.jar?
log4j 2.x
: log4j-core + log4j-api
(log4j 2.x 可單獨使用)
3.2 什么是橋接包?
聊起橋接包,需要回顧下之前提到的SLF4J。SLF4J通過定義一套API,使得應用程序可以在不依賴具體日志實現的情況下進行日志記錄。為了實現這一目標,SLF4J引入了StaticLoggerBinder這個關鍵組件。
StaticLoggerBinder是SLF4J API與底層日志實現之間的一個接口,它是一個單例類,負責在運行時返回日志實現的LoggerFactory實例。這個類的存在使得SLF4J能夠在不直接引用具體日志庫的情況下,依然能夠找到并使用正確的日志實現。
橋接包(Bridge Package)的作用是解決已有代碼依賴特定日志框架(如Log4j 1.x)與SLF4J之間的兼容性問題。例如,slf4j-log4j12.jar橋接包包含了SLF4J的StaticLoggerBinder實現,這個實現將SLF4J的調用適配到Log4j 1.x的API上。這意味著即使代碼中使用了SLF4J API,日志記錄仍然可以通過Log4j 1.x來完成。
對于支持SLF4J的日志實現,如Logback和Log4j 2.x,它們自身就提供了StaticLoggerBinder的實現。例如,Logback的logback-classic.jar和Log4j 2.x的log4j-slf4j-impl.jar模塊,都包含了一個符合SLF4J規范的StaticLoggerBinder,使得它們可以直接作為SLF4J的實現。因此不需要額外的橋接包,SLF4J能夠識別并使用這些日志實現進行日志記錄。
總之,橋接包確保了SLF4J與傳統日志框架之間的兼容性,而StaticLoggerBinder則是SLF4J實現其核心功能的關鍵,即在運行時找到并使用正確的日志實現。
注:具體有關StaticLoggerBinder底層實現可參考這篇文章。?
常用的橋接包:如使用SLF4J的API進行編程,底層想使用log4j1來進行實際的日志輸出,這就是slf4j-log4j12干的事。
?
slf4j-jdk14
: 讓SLF4J使用Java內置的日志系統(JUL)。?
slf4j-log4j12
: 將SLF4J與Log4j 1.x綁定。?
log4j-slf4j-impl
: 綁定SLF4J到Log4j 2。?
logback-classi
c: SLF4J的實現,使用Logback作為日志引擎。?
slf4j-jcl
: 橋接SLF4J到Apache Commons Logging。
3.3 如何從其它日志實現/門面到SLF4J呢?
其實大致的實現就是兩步,一是選擇SLF4J和具體實現,二是兼容舊的日志實現/門面到SLF4J。
例如項目之前是用的JCL的API,不可能因為要換一個日志框架,把原先的日志代碼都改掉吧(API的方法不一樣,入參和使用方法也不一樣),這個代價太大。 我們希望的是,原有的日志代碼可以不動,后續的代碼可以用新的SLF4J的API,橋接包就是為了達到這樣的效果。具體操作就三步:1、移除掉舊的日志依賴2、引入SLF4J提供的橋接依賴3、項目中引入SLF4J和新的日志實現。
圖7 SLF4J相關橋接包依賴
場景介紹:如 使用log4j1的API進行編程,但是想最終通過logback來進行輸出,所以就需要先將log4j1的日志輸出轉交給slf4j來輸出,slf4j 再交給logback來輸出。將log4j1的輸出轉給slf4j,這就是log4j-over-slf4j做的事。
?
jul-to-slf4j
:jdk-logging到slf4j的橋梁,將jul的日志輸出切換到slf4j。?
log4j-over-slf4
j:log4j1到slf4j的橋梁,將log4j1的日志輸出切換到slf4j。?
jcl-over-slf4
j:commons-logging到slf4j的橋梁,將commons-logging的底層日志輸出切換到slf4j。
注:更詳細的SLF4J和不同日志實現的搭配以及各個日志系統之間的切換所需引用的具體jar包可參考這篇文章。?
3.4 橋接包導致的沖突
場景1:jcl-over-slf4j 與 slf4j-jcl 沖突
?
jcl-over-slf4j
: 這個橋接器的作用是將Apache Commons Logging(JCL)的日志調用轉換為SLF4J API。
如果你的代碼或依賴項使用了JCL API,但你希望統一日志處理并利用SLF4J的靈活性,可以引入jcl-over-slf4j。這將使得JCL的日志記錄調用被重定向到SLF4J,從而可以選擇和配置任何SLF4J兼容的日志實現。
?
slf4j-jcl
: 這個橋接器則是將SLF4J API的調用橋接到Apache Commons Logging。
如果你的項目中使用了SLF4J,但希望日志輸出通過Commons Logging處理,可以使用slf4j-jcl。這將SLF4J的日志調用映射到JCL,使得你的日志記錄通過Commons Logging的實現進行。
如果這兩者共存的話,必然造成相互委托,造成內存溢出
場景2:log4j-over-slf4j 與 slf4j-log4j12 沖突
?l
og4j-over-slf4j
: 這個庫的目的是將Log4j 1.x的日志API調用重定向到SLF4J API。
如果你的應用程序原本使用Log4j 1.x進行日志記錄,但你想利用SLF4J的靈活性,可以選擇使用log4j-over-slf4j。它會模擬Log4j的API,使得Log4j的配置和調用能夠透明地轉換為SLF4J,這樣你就可以在運行時使用任何SLF4J兼容的日志實現,如Logback或Log4j 2。
?
slf4j-log4j12
: 這個橋接器將SLF4J API的調用綁定到Log4j 1.x實現。
如果你的項目使用了SLF4J API,但希望日志輸出通過Log4j 1.x處理,那么可以引入slf4j-log4j12。這樣,所有的SLF4J調用都會被轉換為Log4j的具體操作。
如果這兩者共存的話,理論上必然造成相互委托,造成內存溢出。但是log4j-over-slf4內部做了一個判斷,可以防止造成內存溢出。
注意:log4j-over-slf4j庫在啟動時會進行內部檢查,以確保它不會與Log4j 1.x直接使用或者其他SLF4J綁定(如slf4j-log4j12)沖突。它會檢查是否存在多個SLF4J綁定,特別是org.slf4j.impl.StaticLoggerBinder的實例,因為這個類是SLF4J用來確定實際日志實現的標志。如果發現多個這樣的綁定,log4j-over-slf4j會拋出一個警告或異常,指出類路徑中存在沖突,并建議用戶清理類路徑以避免循環引用或日志記錄的不正確行為。 這個檢查通常在類加載時執行,即當應用程序啟動并嘗試加載log4j-over-slf4j時。如果檢測到類路徑中有其他SLF4J綁定,它會通過org.slf4j.LoggerFactory的靜態初始化來拋出錯誤信息,而不是在運行時導致內存溢出。這種檢查機制有助于防止潛在的問題,并指導開發者如何解決日志庫的沖突。
場景3:jul-to-slf4j 與 slf4j-jdk14 沖突
?
jul-to-slf4j
: 這個橋接器的作用是將Java內置的日志框架java.util.logging(JUL)的日志記錄調用轉換為SLF4J API。
如果你的Java應用使用了JUL API,但希望將日志記錄委托給SLF4J,以便于選擇和切換不同的日志實現,那么可以引入jul-to-slf4j。這使得JUL的日志記錄能夠被SLF4J的實現如Logback或Log4j處理。
?
slf4j-jdk14
: 這個橋接器則剛好相反,它將SLF4J API的調用重定向到JUL。
這意味著,即使你的代碼使用SLF4J API,日志記錄實際上會通過JDK的java.util.logging框架進行。這通常發生在你有一個使用SLF4J的庫,但希望使用JUL作為日志實現的場景。請注意,使用這個橋接器可能會限制你對日志系統的控制和配置,因為JUL通常不如SLF4J的其他實現那樣功能豐富。
如果這兩者共存的話,必然造成相互委托,造成內存溢出
4. 處理日志包沖突
OK,到現在我們已經清楚地知道日志門面與日志實現的對應關系,以及在多個日志實現jar包存在的情況下如何通過橋接包實現我們期望的最終日志輸出效果,那么有沒有一種方式能夠幫助我們在項目啟動的時候只管的發現是否存在日志jar包沖突呢(比如場景2情況能否提前感知呢)?回答這個問題之前,我們需要先解答下文中最初的問題,為什么同樣的應用部署在不同的機器上面會出現不同的表征呢(一臺正常打印日志,另一臺雖正常啟動,但是無任何日志打印)?
4.1 無日志打印原因
答案:因為每個SLF4J的橋接包都有org.slf4j.impl.StaticLoggerBinder,SLF4J則會隨機選擇一個使用。當選擇的跟系統配置的一樣時就可以打印日志,否則就打印不出。
查看acccheck應用的pom文件定位對應的jar包,發現同時共存多套日志jar包(圖8)。
日志門面: slf4j-api、 commons-logging 日志實現: log4j、logback(logback-classic和logback-core) 橋接包: jcl-over-slf4j、slf4j-log4j12 日志記錄配置文件: logback.xml
很明顯該應用是通過logback的日志實現方式來進行日志記錄的,但是應用中同時引用了日志門面SLF4J和JCL,并且在日志實現中同時引用了log4j和logback,兩個橋接包的作用分別是jcl-over-slf4j(commons-logging到slf4j的橋梁,將commons-logging的底層日志輸出切換到slf4j),slf4j-log4j12(將SLF4J的日志門面與log4j 1.x日志實現綁定)。
但是日志記錄的配置文件是logback.xml,所以當SLF4J綁定到log4j日志實現時,無法正常找到相關配置文件,故而無法輸出日志,只有當SLF4J綁定到logback日志實現時才能夠正常進行日志打印。
圖 8 acccheck應用中應用的相關日志jar包
4.2 解決方案
OK,現在已經明確問題是因為SLF4J綁定到不同的日志實現導致的日志會出現無法記錄的表征,并且可以確定需要輸出的配置文件是logback.xml,那么處理方式已經很清晰啦。
第一步:首先先確立需要使用的一套日志框架(日志門面+日志實現),在該應用中使用SLF4J+Logback。不難發現滿足需求的日志jar包如下。
日志門面: slf4j-api 日志實現: logback(logback-classic和logback-core)
第二步:需要對無用的日志jar包進行去除,在該應用中需要去除掉JCL(該jar包已經通過橋接包 jcl-over-slf4j實現功能替換),去除log4j相關依賴(log4j的日志實現和SLF4J到log4j 1.x的橋接包slf4j-log4j12)。
第三步:需要評估是否存在引用需要共存場景,在該應用中存在JCL日志門面,發現在代碼中未直接引用JCL包中的類和接口,因此橋接包 jcl-over-slf4j也無需保留。如果代碼中對JCL有直接引用的話可以通過引入橋接包jcl-over-slf4j實現功能替換。
4.3 監控日志jar包沖突
回到本節的問題,那么有沒有一種方式能夠幫助我們在項目啟動的時候只管的發現是否存在日志jar包沖突呢(比如場景2情況能否提前感知呢)?答案是可以。
注:由于這塊不是本文的重點,大家感興趣可以參考這篇文章。?
5. 總結
通過以上有關日志框架相關知識的介紹以及實踐,可以將解決日志框架共存/沖突問題概括為需遵循一下幾個原則:
1.
明確
需要使用的一套日志實現2.
刪除
多余的無用日志依賴jar包3.視應用的引用是否必須共存情況
引入橋接包
如果有引用必須共存的話,那么就移除原始包,使用“over”類型的包(over類型的包復制了一份原始接口,重新實現)
4. 使用日志抽象提供的指定方式
不能over的,使用日志抽象提供的指定方式,例如jboss-logging中,可以通過org.jboss.logging.provider環境變量指定一個具體的日志框架實現
項目里統一了日志框架之后,無論用那種日志框架打印,最終還是走向我們中轉/適配后的唯一一個日志框架。解決了共存/沖突之后,項目里就只剩一款日志框架。再也不會出現“日志打不出”,“日志配置不生效”之類的各種惡心問題。
最后補充一張以SLF4J為日志門面的適配方案圖(如圖9),目前SLF4J是適配方案中最核心的那個框架,也是圖9的中心樞紐。只要圍繞slf4j做適配/轉化,理論上就沒有處理不了的沖突。
圖 9 SLF4J的適配轉化流程圖
審核編輯 黃宇
-
接口
+關注
關注
33文章
8596瀏覽量
151145 -
日志
+關注
關注
0文章
138瀏覽量
10642 -
JCL
+關注
關注
0文章
2瀏覽量
6426
發布評論請先 登錄
相關推薦
評論