歷史
log4j可以當之無愧地說是Java日志框架的元老,1999年發布首個版本,2012年發布最后一個版本,2015年正式宣布終止,至今還有無數的系統在使用log4j,甚至很多新系統的日志框架選型仍在選擇log4j。
然而老的不等于好的,在IT技術層面更是如此。盡管log4j有著出色的歷史戰績,但早已不是Java日志框架的最優選擇。
在log4j被Apache Foundation收入門下之后,由于理念不合,log4j的作者Ceki離開并開發了slf4j和logback。
slf4j因其優秀的性能和理念很快受到了廣泛歡迎,2016年的統計顯示,github上的熱門Java項目中,slf4j是使用率第二名的類庫(第一名是junit)。
logback則吸取了log4j的經驗,實現了很多強大的新功能,再加上它和slf4j能夠無縫集成,也受到了歡迎。
在這期間,Apache Logging則一直在關門憋大招,log4j2在beta版鼓搗了幾年,終于在2014年發布了GA版,不僅吸收了logback的先進功能,更通過優秀的鎖機制、LMAX Disruptor、"無垃圾"機制等先進特性,在性能上全面超越了log4j和logback。
slf4j
slf4j是一個“日志門面”(Logging Facade),而不是一個完整的日志框架。它提供了一套記錄日志的api,但不提供輸出日志的功能,而是通過對接如log4j、java.util.logging等日志框架,來實現日志的輸出。
所以說slf4j的對標選手實際上是Jakarta Commons Logging(簡稱JCL),二者的最大區別在于與日志服務的綁定機制
JCL采用的動態綁定機制:
- 在進程啟動時嘗試獲取名為"org.apache.commons.logging.Log"的配置屬性(可與在commons-logging.properties文件中配置,或使用Java代碼進行配置),按配置選取對應的日志輸出服務
- 如果沒有獲取到對應配置屬性,會嘗試在系統參數中尋找名為"org.apache.commons.logging.Log"的參數項
- 如果1,2均沒有獲取到,會在classpath下尋找log4j的相關class,如果找到,則使用log4j作為日志輸出服務
- 如果沒有找到log4j,則嘗試使用java.util.logging包作為日志輸出服務
- 如果上述都失敗,則使用SimpleLog作為日志輸出服務,即將所有日志輸出至控制臺標準輸出System.err
JCL的動態綁定機制基于ClassLoader實現,缺點一是效率較低,二是容易引發混亂,在一個復雜甚至混亂的依賴環境下,確定當前正在生效的日志服務是很費力的,特別是在程序開發和設計人員并不理解JCL的機制時,三是最致命的問題:在使用了自定義ClassLoader的程序中,使用JCL會引發各類問題,例如內存泄露、與OSGI沖突等。
而slf4j則簡單得多,采用靜態綁定機制:
- slf4j為各類日志輸出服務提供了適配庫,如slf4j-log4j12,slf4j-simple,slf4j-jdk14等。一個Java工程下只能引入一個slf4j適配庫
- slf4j會加載org.slf4j.impl.StaticLoggerBinder作為輸出日志的實現類。這個類在每個適配庫中都存在,所以slf4j不需要像JCL一樣主動去尋找日志輸出實現,自然而然地就能與具體的日志輸出實現綁定起來
- 當需要更換日志輸出服務時(比如從logback切換回log4j),只需要替換掉適配庫即可
所以slf4j不僅對比JCL有性能上的優勢,使用slf4j的程序員也不需要去翻找配置文件或追蹤啟動過程就能夠清除明白地了解當前使用的是什么日志輸出服務。
slf4j的優勢還不止此:
強制輸出String,避免不規范代碼
傳統的日志api都接收Object類型的參數,在程序員不遵守規范時容易引發一些錯誤,比如:
SomeObject obj;
//...
logger.info(obj); //如果SomeObject并未覆蓋toString()方法,這里就只記下來了hashcode
又如:
try {
//...
} catch(Exception e) {
logger.error(e); //未記錄異常stacktrace
}
slf4j的api強制要求傳入String類型的參數,能夠在一定程度上避免此類不規范的代碼出現。
日志模板功能
在使用傳統的日志api時,可能會有這樣的代碼:
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
這引發了兩個問題:
- 需要編寫拼接字符串的代碼,使開發效率降低
- 即使不需要輸出這條日志(比如當前日志級別是ERROR時),也會執行拼接字符串的操作,消耗額外性能,占用額外內存
而slf4j使用日志模板功能解決了這兩個問題:
logger.debug("Entry number: {} is {}", i, String.valueOf(entry[i]));
不僅開發變得簡單了,而且slf4j只會在此條日志確實需要輸出時才會去拼裝字符串。
并且在輸出異常信息時也可以使用模板,不會妨礙stacktrace的輸出:
String s = "Hello world";
try {
Integer i = Integer.valueOf(s);
} catch (NumberFormatException e) {
logger.error("Failed to format {}", s, e);
}
slf4j的橋接功能
雖然slf4j如此優秀,但一些類庫因為歷史原因仍然在使用JCL作為日志api(如Spring等),為此slf4j還推出了jcl-over-slf4j橋接庫,能夠把使用JCL的API輸出的日志橋接到slf4j上,方便那些想要使用slf4j作為日志門面但同時又要使用Spring等需要依賴JCL的類庫的系統。
對于自動依賴JCL的類庫,如要橋接至slf4j的話,除了引入jcl-over-slf4j適配庫之外,還需要把JCL庫從classpath中移除。可以在maven配置中將JCL庫標記為provided:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
<scope>provided</scope>
</dependency>
同時slf4j還提供了log4j-over-slf4j等橋接庫,能夠在不改動代碼的前提下把使用各種日志框架輸出的日志都橋接到slf4j,如下圖:
slf4j提供的適配庫和橋接庫
適配庫:
- slf4j-log4j12:使用log4j-1.2作為日志輸出服務
- slf4j-jdk14:使用java.util.logging作為日志輸出服務
- slf4j-jcl:使用JCL作為日志輸出服務
- slf4j-simple:日志輸出至System.err
- slf4j-nop:不輸出日志
- log4j-slf4j-impl:使用log4j2作為日志輸出服務
logback天然與slf4j適配,不需要額外引入適配庫(畢竟是一個作者寫的)
橋接庫:
- log4j-over-slf4j:將使用log4j api輸出的日志橋接至slf4j
- jcl-over-slf4j:將使用JCL api輸出的日志橋接至slf4j
- jul-to-slf4j:將使用java.util.logging輸出的日志橋接至slf4j
- log4j-to-slf4j:將使用log4j2輸出的日志橋接至slf4j
題外話:
slf4j唯獨沒有提供log4j2的適配庫和橋接庫,log4j-slf4j-impl和log4j-to-slf4j都是Apache Logging自己開發的,看樣子Ceki和Apache Logging的梁子真的很深啊……倒是Apache沒有端架子,可能也是因為slf4j太火了吧
logback與log4j2
logback和log4j2都宣稱自己是log4j的后代,一個是出于同一個作者,另一個則是在名字上根正苗紅。
撇開血統不談,比較一下log4j2和logback:
- log4j2比logback更新:log4j2的GA版在2014年底才推出,比logback晚了好幾年,這期間log4j2確實吸收了slf4j和logback的一些優點(比如日志模板),同時應用了不少的新技術
- 由于采用了更先進的鎖機制和LMAX Disruptor庫,log4j2的性能優于logback,特別是在多線程環境下和使用異步日志的環境下
- 二者都支持Filter(應該說是log4j2借鑒了logback的Filter),能夠實現靈活的日志記錄規則(例如僅對一部分用戶記錄debug級別的日志)
- 二者都支持對配置文件的動態更新
- 二者都能夠適配slf4j,logback與slf4j的適配應該會更好一些,畢竟省掉了一層適配庫
- logback能夠自動壓縮/刪除舊日志
- logback提供了對日志的HTTP訪問功能
- log4j2實現了“無垃圾”和“低垃圾”模式。簡單地說,log4j2在記錄日志時,能夠重用對象(如String等),盡可能避免實例化新的臨時對象,減少因日志記錄產生的垃圾對象,減少垃圾回收帶來的性能下降
log4j2和logback各有長處,總體來說,如果對性能要求比較高的話,log4j2相對還是較優的選擇。
附上log4j2與logback性能對比的benchmark,這份benchmark是Apache Logging出的,有多大水分不知道,僅供參考
同步寫文件日志的benchmark:
異步寫日志的benchmark:
當然,這些benchmark都是在日志Pattern中不包含Location信息(如日志代碼行號 ,調用者信息,Class名/源碼文件名等)時測定的,如果輸出Location信息的話,性能誰也拯救不了:
對Java日志組件選型的建議
- slf4j已經成為了Java日志組件的明星選手,可以完美替代JCL,使用JCL橋接庫也能完美兼容一切使用JCL作為日志門面的類庫,現在的新系統已經沒有不使用slf4j作為日志API的理由了
- 日志記錄服務方面,log4j在功能上輸于logback和log4j2,在性能方面log4j2則全面超越log4j和logback。所以新系統應該在logback和log4j2中做出選擇,對于性能有很高要求的系統,應優先考慮log4j2
對現有系統日志架構的改造建議
-
如果現有系統使用JCL作為日志門面,又確實面臨著JCL的ClassLoader機制帶來的問題,完全可以引入slf4j并通過橋接庫將JCL api輸出的日志橋接至slf4j,再通過適配庫適配至現有的日志輸出服務(如log4j),如下圖:
圖片.png
這樣做不需要任何代碼級的改造,就可以解決JCL的ClassLoader帶來的問題,但沒有辦法享受日志模板等slf4j的api帶來的優點。不過之后在現系統上開發的新功能就可以使用slf4j的api了,老代碼也可以分批進行改造。
如果現有系統使用JCL作為日志門面,又頭疼JCL不支持logback和log4j2等新的日志服務,也可以通過橋接庫以slf4j替代JCL,但同樣無法直接享受slf4j api的優點。
如果想要使用slf4j的api,那么就不得不進行代碼改造了,當然改造也可以參考1中提到的方式逐步進行。
如果現系統面臨著log4j的性能問題,可以使用Apache Logging提供的log4j到log4j2的橋接庫log4j-1.2-api,把通過log4j api輸出的日志橋接至log4j2。這樣可以最快地使用上log4j2的先進性能,但組件中缺失了slf4j,對后續進行日志架構改造的靈活性有影響。另一種辦法是先把log4j橋接至slf4j,再使用slf4j到log4j2的適配庫。這樣做稍微麻煩了一點,但可以逐步將系統中的日志輸出標準化為使用slf4j的api,為后面的工作打好基礎。
最后附上一些鏈接
slf4j官網: https://www.slf4j.org/
logback官網: https://logback.qos.ch/
log4j2官網: http://logging.apache.org/log4j/2.x/