commons logging+log4j一直是java日志的經典組合,以至于很多伺服器都使用了類似的配置,像websphere、以前的tomcat都使用commons logging作為日志輸出架構,而據說jboss則直接commons logging和log4j一起使用了(這個估計是為了解決commons logging中經常在這類伺服器上遇到的classloader問題)。然而log4j的開發團隊對commons logging貌似不滿意(可以從log4j manual中看出一些端倪),因而log4j團隊開發了自己的日志門面架構slf4j(simple logging façade for java),貌似他們對自己開發的log4j的性能也不滿意,然後又弄出了個logback,關鍵執行語句的性能要比log4j快10倍以上(官網資料,我本人還沒有仔細看過logback的代碼,更沒有測試過,不知道具體性能能提高多少),這是後話,等過幾年看logback代碼後再仔細讨論。
以我個人了解,slf4j的出現是為了解決commons logging存在的兩個問題:
1. commons logging存在的classloader問題,即當伺服器本身引入commons logging時,如果在伺服器中載入commons logging包,則該包中的類由伺服器的classloader加載,進而在加載log4j中的logger類時會出現classnotfoundexception,因為伺服器中的classloader沒法找到web app下的jar包。對于父classloader優先的類加載機制來說,目前的一個解決方案是使用commons-logging-api-1.1.1.jar包,該包的log實作類隻包含jdk14logger、simplelog、nooplog三個類,因而在加載這幾個類時會使用web app中的commons logging包,進而解決classnotfoundexception的問題,然而這種方式對那些實作child classloader first的伺服器來說,由webclassloader父classloader加載的類在使用日志時會存在問題,因為它們的log接口由加載自身類的classloader加載,而log4jlogger類卻由webclassloader加載。具體關于commons logging中存在的問題我會在另外一篇文章中詳細說明。
2. 在使用commons logging時,我們經常會看到以下方法的寫法:
if (logger.isdebugenabled()) {
logger.info("loading xml bean definitions from " + encodedresource.getresource());
}
存在isdebugenabled()的判斷邏輯是為了在避免多餘的字元串拼接,即如果不存在isdebugenabled()判斷,即使目前日志級别為error時,在遇到logger.info()調用時,它還會先拼接日志消息的字元串,然後進入該方法内,才發現這個日志語句不用列印。而這種多餘的拼接不僅浪費了多餘的cpu操作,而且會增加gc的負擔。slf4j則提供以下的方式來解決這個問題:
logger.info("loading xml bean definitions from {}", encodedresource.getresource());
類似commons logging,slf4j在使用時通過loggerfactory得到命名的logger執行個體,然後通過該logger執行個體調用相應的方法列印日志:
final logger logger = loggerfactory.getlogger("levin.logging.slf4j");
logger.info("using slf4j, current time is {}", new date());
然而不同于commons logging的動态綁定機制,slf4j則采用了一種靜态綁定的機制,即每個支援slf4j的logging架構必須存在一個繼承自loggerfactorybinder接口的staticloggerbinder類:
public interface loggerfactorybinder {
public iloggerfactory getloggerfactory();
public string getloggerfactoryclassstr();
loggerfactory調用staticloggerbinder類中的getloggerfactory()方法傳回相應的iloggerfactory執行個體:
public interface iloggerfactory {
public logger getlogger(string name);
最後通過iloggerfactory執行個體擷取logger執行個體:
public interface logger {
public string getname();
...(trace)
public boolean isdebugenabled();
public void debug(string msg);
public void debug(string format, object arg);
public void debug(string format, object arg1, object arg2);
public void debug(string format, object... arguments);
public void debug(string msg, throwable t);
public boolean isdebugenabled(marker marker);
public void debug(marker marker, string msg);
public void debug(marker marker, string format, object arg);
public void debug(marker marker, string format, object arg1, object arg2);
public void debug(marker marker, string format, object... arguments);
public void debug(marker marker, string msg, throwable t);
...(info)
...(warn)
...(error)
也正是因為這個設計,slf4j在classpath下隻支援一個橋接包(slf4j-simple-<version>.jar、slf4j-log4j12-<version>.jar、slf4j-jdk14-<version>.jar、logback-classic-<version>.jar等)。如果在classpath下存在多個橋接包,則具體用哪個就要看這幾個橋接包的加載順序了,實際中會使用先加載的橋接包。同時slf4j會列印使用哪個橋接包,哪些橋接包沒有使用。這種靜态綁定的設計比commons logging在可擴充性上具有更加靈活的機制,對“可插拔”的支援也更加高效。如果要支援一個新的logging架構,commons logging需要通過在屬性配置檔案、或虛拟機屬性中配置支援這個新的logging架構的實作類(實作log接口);而slf4j則隻需要編寫一個五個相應的類:
1. 實作logger接口的類
2. 實作iloggerfactory接口的類
3. 實作loggerfactorybinder接口的類staticloggerbinder類(必須使用staticloggerbinder類名),并且存在一個靜态的getsingleton()方法。
4. 實作markerfactorybinder類的staticmarkerbinder類(必須使用staticmarkerbinder類名),可選。一般也會存在一個靜态的singleton字段,不過也是可選的。
5. 實作staticmdcbinder類,可選。一般也會存在一個靜态的singleton字段,也可選。
slf4j的類設計也相對比較簡單(也感覺有點零散):

由于采用了靜态綁定的方式,而不是像commons logging中的動态綁定,slf4j中loggerfactory的實作要比commons logging中logfactory的實作要簡單的多。即loggerfactory調用getiloggerfactory()方法,該方法會初始化loggerfactory,即通過在bind()方法中加載classpath中的staticloggerbinder類,并根據加載結果設定目前loggerfactory的初始化狀态,進而在getiloggerfactory()方法中通過目前loggerfactory的狀态判斷傳回的iloggerfactory執行個體。簡單的示意圖如下:
bind()方法的主要源碼如下:
private final static void bind() {
try {
...
//實作綁定
staticloggerbinder.getsingleton();
initialization_state = successful_initialization;
} catch (noclassdeffounderror ncde) {
string msg = ncde.getmessage();
//判斷是否是因為沒有找到staticloggerbinder類引起的異常
//此時,使用noploggerfactory類傳回給getiloggerfactory(),不列印任何日志
if (messagecontainsorgslf4jimplstaticloggerbinder(msg)) {
initialization_state = nop_fallback_initialization;
...
} else {
// initialization_state = failed_initialization
failedbinding(ncde);
throw ncde;
}
} catch (java.lang.nosuchmethoderror nsme) {
string msg = nsme.getmessage();
if (msg != null && msg.indexof("org.slf4j.impl.staticloggerbinder.getsingleton()") != -1) {
initialization_state = failed_initialization;
throw nsme;
} catch (exception e) {
//initialization_state = failed_initialization;
failedbinding(e);
throw new illegalstateexception("unexpected initialization failure", e);
}
}
即bind()方法使用調用staticloggerbinder.getsingleton()方法來實作綁定,如果該方法調用成功,則将初始化狀态設定為successful_initialization,如果因為沒有找到staticloggerbinder類而引起的異常,則将狀态設定為nop_fallback_initialization,否則将狀态設定為failed_initialization,并抛出異常。如果在目前classpath下存在多個橋接jar包,在實作綁定前後會記錄存在哪些可使用的橋接jar包,綁定了那個iloggerfactory類。
在bind()傳回後,performinitialization()方法會再做一些版本檢查,即staticloggerbinder可以定義一個靜态的requested_api_version字段,表示該staticloggerbinder支援的slf4j版本,如果該版本不在loggerfactory定義的相容版本清單中(api_compatibility_list),slf4j會列印警告資訊,并列出目前loggerfactory相容的版本清單。而後在getiloggerfactory()方法中會根據目前loggerfactory的初始化狀态來決定傳回的iloggerfactory執行個體:
public static iloggerfactory getiloggerfactory() {
if (initialization_state == uninitialized) {
initialization_state = ongoing_initialization;
performinitialization();
switch (initialization_state) {
case successful_initialization:
return staticloggerbinder.getsingleton().getloggerfactory();
case nop_fallback_initialization:
return nop_fallback_factory;
case failed_initialization:
throw new illegalstateexception(unsuccessful_init_msg);
case ongoing_initialization:
// support re-entrant behavior.
// see also http://bugzilla.slf4j.org/show_bug.cgi?id=106
return temp_factory;
throw new illegalstateexception("unreachable code");
當loggerfactory成功初始化,則傳回綁定的staticloggerbinder中的iloggerfactory執行個體;如果為nop_fallback_initialization(沒有找到橋接jar),則傳回noploggerfactory,它傳回一個單例的noplogger執行個體,該類不會列印任何日志;如果初始化狀态為failed_initialization,抛出illegalstateexception異常;如果初始化狀态為ongoing_initialization,則傳回substituteloggerfactory類執行個體,該狀态發生在一個線程正在初始化loggerfactory,而另一個線程已經開始請求擷取iloggerfactory執行個體,substituteloggerfactory會記錄目前請求的logger名稱,然後傳回noplogger執行個體。所有這些在loggerfactory初始化時被忽略的logger name會在loggerfactory初始化成功以後被report出來(在system.err流中列印出來)。
slf4j實作了一個簡單的日志系統:slf4j-simple-<version>.jar。要實作一個相容slf4j的日志系統,基本的需要三個類:
1. staticloggerbinder類,實作loggerfactorybinder接口。它實作單例模式,存在getsingleton()靜态方法,存在requested_api_verion靜态字段,不用final避免編譯器的優化(将值直接寫入源碼中,而不使用該字段)。傳回的iloggerfactory執行個體也一直使用同一個執行個體(simpleloggerfactory)。
public class staticloggerbinder implements loggerfactorybinder {
private static final staticloggerbinder singleton = new staticloggerbinder();
public static final staticloggerbinder getsingleton() {
return singleton;
// to avoid constant folding by the compiler, this field must *not* be final
public static string requested_api_version = "1.6.99"; // !final
private static final string loggerfactoryclassstr = simpleloggerfactory.class.getname();
private final iloggerfactory loggerfactory;
private staticloggerbinder() {
loggerfactory = new simpleloggerfactory();
public iloggerfactory getloggerfactory() {
return loggerfactory;
public string getloggerfactoryclassstr() {
return loggerfactoryclassstr;
}
2. 實作iloggerfactory接口的simpleloggerfactory。它有一個loggermap字段緩存所有之前建立的simplelogger執行個體,以logger name為key,實作每個相同名字的logger執行個體隻需要建立一次。
public class simpleloggerfactory implements iloggerfactory {
final static simpleloggerfactory instance = new simpleloggerfactory();
map loggermap;
public simpleloggerfactory() {
loggermap = new hashmap();
public logger getlogger(string name) {
logger slogger = null;
// protect against concurrent access of the loggermap
synchronized (this) {
slogger = (logger) loggermap.get(name);
if (slogger == null) {
slogger = new simplelogger(name);
loggermap.put(name, slogger);
return slogger;
3. simplelogger類,實作logger接口。simplelogger繼承自markerignoringbase類,該基類忽略所有存在marker參數的日志列印方法。simplelogger将日志級别分成五個級别:trace、debug、info、warn、error,這些級别對應的int值一次增大。simplelogger還支援對simplelogger.properties配置檔案的解析,它支援的key值有:
org.slf4j.simplelogger.defaultloglevel
org.slf4j.simplelogger.showdatetime
org.slf4j.simplelogger.datetimeformat
org.slf4j.simplelogger.showthreadname
org.slf4j.simplelogger.showlogname
org.slf4j.simplelogger.showshortlogname
org.slf4j.simplelogger.logfile
org.slf4j.simplelogger.levelinbrackets
org.slf4j.simplelogger.warnlevelstring(warn提示字元,預設“warn”)
同時simplelogger還支援為特定的logger name字首(以”.”作為分隔符)指定level:
org.slf4j.simplelogger.log.<lognameprefix>
并且所有這些key都可以定義在系統屬性中。
simplelogger類的實作主要分成兩步:初始化和列印日志:
a. 初始化
加載配置檔案,使用加載的配置檔案初始化類字段,即對應以上simplelogger.properties支援的key;儲存目前logger name;計算目前logger實際的level,即如果沒有為該logger name(或其以“.”分隔的字首)配置特定的level,則使用預設配置的level,否則,使用具體的日志,并儲存計算出的level值,如果沒有找到level配置,使用預設值info。
b. 列印日志
對使用format字元串的日志列印方法,調用formatandlog()方法,其内部先調用messageformatter.arrayformat()方法,然後調用log()方法實作列印資訊。log()方法的實作隻是根據解析出來的配置資訊,判斷哪些資訊需要列印,則列印這些資訊,實作比較簡單,不再贅述。
并不是所有logging系統支援這些功能,對它們支援最全面的當屬logback架構了,因而這些類将會在介紹logback架構時一起讨論。在slf4j-simple-<version>.jar,staticmarkerbinder傳回basicmarkerfactory執行個體,而staticmdcbinder傳回nopmdcadapter執行個體。
如slf4j-log4j12-<version>.jar,slf4j-jdk14-<version>.jar,slf4j-jcl-<version>.jar等,它們的實作類似slf4j-simple-<version>.jar的實作,并且更加簡單,因而它們對logger的實作将大部分的邏輯代理給了底層實作架構,因而這裡不再贅述。
slf4j支援上層是slf4j架構,底層還是通過commons logging的動态查找機制,隻要将slf4j-jcl-<version>.jar包加入classpath中即可(當然slf4j-api-<version>.jar也要存在)。
另外slf4j還支援上層是commons logging,而底層交給slf4j提供的靜态綁定機制查找真正的日志實作架構,隻需要将jcl-over-slf4j-<version>.jar包加入到classpath中,此時不需要引入commons-logging-<version>.jar包。它的實作隻是重寫了commons logging架構,并在logfactory中隻使用slf4jlog或slf4jlocationawarelog類。
不過需要注意,slf4j-jcl-<version>.jar包和jcl-over-slf4j-<version>.jar兩個包不能同時出現在classpath中,不然會引起循環調用而導緻棧溢出的問題,因而slf4j-jcl-<version>.jar在初始化時就會檢測這個限制,并抛出異常。
最後slf4j還支援log4j作為上層,而底層交給slf4j靜态綁定要真正實作日志列印的架構,可以将log4j-over-slf4j-<version>.jar包加入到classpath中。其實作也類似jcl-over-slf4j-<version>.jar的實作,重寫大部分的log4j的内部邏輯,而在logger類實作中,将真正的日志列印邏輯代理給slf4j的loggerfactory。
最後給我現在在公司開發的這個系統吐個槽,我們隊日志并沒有統一的管理,有人用commons logging,也有人直接用log4j,其實所有代碼都沒有統一管理,很是換亂,不過slf4j竟然可以滿足這種情況的遷移,即可以将log4j-over-slf4j-<version>.jar和jcl-over-slf4j-<version>.jar包同時放到classpath下。而到這個時候我才意識到為什麼slf4j為什麼會慢慢的使用那麼廣泛了。