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());
SLF4J綜述
類似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的類設計也相對比較簡單(也感覺有點零散):
SLF4J實現實例,SLF4J API與SLF4J Simple
由于采用了靜態綁定的方式,而不是像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()方法的實現只是根據解析出來的配置信息,判斷哪些信息需要打印,則打印這些信息,實現比較簡單,不再贅述。
Marker、MarkerFactory、StaticMarkerBinder以及MDC、StaticMDCBinder類
并不是所有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與Commons Logging、Log4J之間的相互轉化
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為什么會慢慢的使用那么廣泛了。