log4j, log4j2, slf4j, logback關系
log4j是由Apache開發的一套元老級日志框架,為無數新老系統提供了日志服務;而后來log4j的作者Ceki因為某些原因離開Apache,并自己開發了性能更優化的日志門面slf4j和新的日志框架logback,logback能夠與slf4j無縫集成;Apache之后也發力對log4j進行了多方面優化,并推出了新的日志框架log4j2,相對于log4j和logback,很大程度上提高了日志的吞吐量并降低了延時。
目前slf4j因為其優秀的性能和“日志門面”的設計思想,受到了廣泛的應用;而log4j2因為其性能已全面超越log4j與logback,也是日志系統中一個很重要的選擇。
除了這幾個糾葛比較深的日志框架外,還包括其他的日志框架,common-logging, java.util.logging等。
日志門面與日志框架
slf4j就是典型的“日志門面(Logging Facade)”,利用了設計模式中的門面模式思想,對外提供一套通用的日志記錄的API,而不提供具體的日志輸出服務,要實現日志輸出,需要集成其他的日志框架,例如log4j2,logback,log4j,jul等。
這種門面模式的好處在于,記錄日志的API和日志輸出的服務分離開,代碼里面只需要關注記錄日志的API,通過slf4j指定的接口記錄日志;而日志輸出通過引入jar包的方式即可指定其他的日志框架。當我們需要改變系統的日志輸出服務時,不用修改代碼,只需要改變引入日志輸出框架jar包。

- 目前提供日志門面的框架包括:slf4j, common-logging
- 完整的日志框架包括:log4j2, logback, log4j, java.util.logging
(完整日志框架是指框架本身包括記錄日志的API和日志輸出的服務)
需要指出一點,門面模式提供了一種日志API和輸出分離的模式,但是除slf4j和common logging之外的其他完整的日志框架,本身就具備同時提供日志API和輸出的服務,當然也是可以直接采用這些框架本身記錄日志的。
slf4j與common-logging
common-logging同樣是一套日志門面,spring框架本身使用的是common logging,與slf4j的區別主要在于與日志輸出服務的綁定機制。
common-logging采用運行時動態綁定的機制,運行時通過一套動態尋找綁定的規則:
- 在進程啟動時嘗試獲取名為"org.apache.commons.logging.Log"的配置屬性),按配置選取對應的日志輸出服務
- 如果沒有獲取到對應配置屬性,會嘗試在系統參數中尋找名為"org.apache.commons.logging.Log"的參數項
- 如果均沒有獲取到,會在classpath下尋找log4j的相關class,如果找到,則使用log4j作為日志輸出服務
- 如果沒有找到log4j,則嘗試使用java.util.logging包作為日志輸出服務
- 如果上述都失敗,則使用SimpleLog作為日志輸出服務,即將所有日志輸出至控制臺標準輸出System.err
common-logging基于classLoader來動態尋找和加載所綁定的日志輸出服務,但這種動態的方式效率不高;另外在一個復雜甚至混亂的依賴環境下,動態查找機制容易引發混亂;而且對于像OSGI這類需要使用自定義classLoader的框架,無法與common-logging一起工作。
slf4j日志輸出服務綁定則相對簡單很多,在編譯時就靜態綁定日志輸出服務,只需要提前引入需要的日志框架,以及引入slf4j到該框架的適配庫,常見的適配庫有log4j-slf4j-impl,slf4j-log4j12,slf4j-jdk14等,slf4j與logback天然集成,不需要適配庫(畢竟是一個作者寫的)。
slf4j的另外一些小優點體現在提供的API上,包括日志參數強制要求String類型,避免不規范代碼;提供支持填充參數的日志模板,而且只會在確實需要輸出日志時才會拼接日志字符串。
logger.error("Failed to format {}", s, e);
slf4j適配到各日志框架
基于slf4j的優勢,目前常見的做法是使用slf4j做日志門面,再結合其他日志輸出服務。目前除了logback之外,其他的日志框架無法直接與slf4j集成,因此需要我們在使用時引入各種適配庫,將基于slf4j API記錄的日志指向我們需要的日志框架進行輸出。下面列了一下slf4j到logback,log4j,java.util.logging,log4j2的適配庫。
.png)
除了slf4j本身需要引入的slf4j-api.jar之外,其它還需要:
- slf4j+logback:
logback-classic.jar
,logback-core.jar
- slf4j+log4j:
slf44j-log4j12.jar
,log4j.jar
- slf4j+jul:
slf4j-jdk14.jar
- slf4j+log4j2:
log4j-slf4j-impl.jar
,log4j-api.jar
,log4j-core.jar
slf4j通過這些適配庫與各個日志框架集成的原理很簡單,首先我們在使用slf4j記錄日志時,會首先初始化一個Logger
:
private static Logger logger=LoggerFactory.getLogger(TestClass.class);
slf4j的LoggerFactory提供的getLogger方法:
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
public static ILoggerFactory getILoggerFactory() {
…… ……
return StaticLoggerBinder.getSingleton().getLoggerFactory();
…… ……
}
可以看到,當我們調用slf4j的LoggerFactory.getLogger()
方法時,適配庫的作用就是:
- 提供
org/slf4j/impl/StaticLoggerBinder.class
類,這個類的作用就是返回一個實現ILoggerFactory
接口的類(例如log4j-slf4j-impl
中返回Log4jLoggerFactory
類); - 提供實現
ILoggerFactory
接口的類,該類實現getLogger()
方法,返回一個具體的logger
實例。
需要注意的是,當我們使用slf4j日志門面之后,只能指定一個slf4j的適配庫,否則會在編譯期間報錯。
各日志體系橋接到slf4j
如果目前應用程序中已經使用了如下混雜方式的API來進行日志的編程:
- commons-logging
- jdk-logging
- log4j
而程序希望統一通過logback進行日志輸出,可以通過將這些日志框架橋接到slf4j,然后由slf4j指定logback做日志輸出的方式,這就需要指定各個日志框架到slf4j的橋接ba橋接包。
- 去掉commons-logging(去不去都可以),使用jcl-over-slf4j將commons-logging的底層日志輸出切換到slf4j;
- 去掉log4j1(必須去掉),使用log4j-over-slf4j,將log4j1的日志輸出切換到slf4j
- 使用jul-to-slf4j,將jul的日志輸出切換到slf4j
下圖是slf4j官網提供的橋接示例圖。

這種橋接的方式的原理也好理解,橋接包中會直接提供與其他日志框架API相同路徑的類,替換掉它們本身的類。例如jcl-over-slf4j會替換掉common-logging中的org.apache.commons.logging.LogFactory
類,這個類會使用slf4j創建logger實例。
常見的log包沖突問題解決
1.項目中引入了slf4j的多個日志輸出框架,導致報錯
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [slf4j-log4j12-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [logback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
原因:slf4j的LoggerFactory中需要調用StaticLoggerBinder類,當多個適配庫存在時,會有多個StaticLoggerBinder存在。
解決:排掉多余的slf4j適配包,只保留需要的日志輸出服務;
2.橋接包相互橋接,例如同時引入了log4j-over-slf4j 與 slf4j-log4j12,導致棧溢出
Exception in thread "main" java.lang.StackOverflowError
at java.util.Hashtable.containsKey(Hashtable.java:306)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:36)
at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
at org.apache.log4j.Category.<init>(Category.java:53)
at org.apache.log4j.Logger..<init>(Logger.java:35)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:249)
at org.apache.log4j.Category..<init>(Category.java:53)
at org.apache.log4j.Logger..<init>(Logger.java:35)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:39)
at org.apache.log4j.LogManager.getLogger(LogManager.java:39)
subsequent lines omitted...
原因:前者將log4j橋接到slf4j,后者將slf4j橋接到log4j,循環橋接,當第一個通過slf4j或log4j獲取的logger被調用時,就會出現StackOverflowError。
解決:明確到底使用哪個日志框架,如果使用slf4j做日志API和輸出,則去掉slf4j-log4j12;如果使用log4j做日志記錄和輸出,則去掉log4j-over-slf4j。
3.項目中已有log4j做日志記錄API,同時又引入log4j-over-slf4j希望將log4j橋接到slf4j,但使用log4j記錄的日志沒有正常輸出?
原因:log4j-over-slf4j橋接包的原理是替代log4j包本身的org.apache.log4j.Logger
類,如果引入了該橋接包,又沒有排除log4j本身的包,導致使用Log4j做日志記錄的地方還是使用log4j做日志輸出,然后項目里面沒有任何關于log4j的日志輸出配置,導致日志輸出失敗。
解決:解決方法很簡單,排除掉log4j的包即可。
另外關于slf4j相關的問題可以參考slf4j官網提供的一些常見問題和原因分析以及解決方法。
4.項目中依賴各種日志框架,有多個門面slf4j,common logging,還有各種其他的日志框架log4j2, log4j, jul等;有用日志門面記錄日志的,也有用非門面日志框架記錄日志的,總之,一片混亂 ~
思路:想給服務提供統一的日志輸出,可以將各種日志API首先橋接到slf4j,然后指定slf4j的日志輸出服務,這樣就算不同的日志記錄API,也可以通過統一的日志輸出服務輸出日志。同時也要記得排除各種不需要的日志jar包,解決各種循環橋接的問題。
參考閱讀:
slf4j、jcl、jul、log4j1、log4j2、logback大總結
slf4j與jul、log4j1、log4j2、logback的集成原理
該讓log4j退休了 - 論Java日志組件的選擇
混亂的 Java 日志體系