一、slf4j和log4j、logback的關系
slf4j(simple logging facade for java)簡單日志門面,隻是為各種日志實作提供了日志使用的接口,并沒有具體的日志實作。
log4j和logback是具體的日志實作,不使用slf4j我們也可以直接使用log4j或者logback實作日志記錄。slf4j相當于是各種日志接口的接口,調用的時候,直接使用slf4j的日志接口,具體的日志實作隻需要通過提供不同的日志實作的jar和配置檔案,就可以靈活的實作日志的切換,對于應用代碼無感覺。
通過一張圖看看slf4j和log4j、logback的關系
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM5gDOxUjMxIDMxcDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
可以看出來slf4j和apache的common-log的功能是一緻的,為各種日志實作提供一個統一的通路接口,門面模式。
二、slf4j的使用
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Test {
private static final Logger log = LoggerFactory.getLogger(Test.class);
public static void main(String[] args) {
for (;;) {
log.info("hello {}", "world");
}
}
}
程式代碼中引用的是slf4j-api-xxx.jar包裡的類,僅僅有slf4j-api-xxx.jar是不夠的,還需要在classpath下提供log4j(slf4j-log4j12-xxx.jar+log4j-xxx.jar)或者logback(logback-classic-xxx.jar+logback-core-xxx.jar)的實作。
三、代碼層面看slf4j和log4j、logback的關系
看看上面例子中的LoggerFactory.getLogger(xxx.class)方法是如何擷取到日志實作的,跟蹤getLogger()方法
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
首先傳回一個ILoggerFactory(已經是具體的實作了),然後擷取Logger。繼續跟進getILoggerFactory()方法
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITILIZATION;
performInitialization();
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITILIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITILIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITILIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITILIZATION:
// support re-entrant behavior.
// See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
return TEMP_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
如果還沒初始化就先初始化,如果初始化成功就通過 StaticLoggerBinder的執行個體傳回LoggerFactory。看下初始化方法performInitialization()方法
private final static void performInitialization() {
singleImplementationSanityCheck();
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITILIZATION) {
versionSanityCheck();
}
}
檢查classpath下所有org/slf4j/impl/StaticLoggerBinder.class資源,然後bind()。看下檢查和綁定的過程
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
private static void singleImplementationSanityCheck() {
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class
.getClassLoader();
Enumeration paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader
.getResources(STATIC_LOGGER_BINDER_PATH);
}
List implementationList = new ArrayList();
while (paths.hasMoreElements()) {
URL path = (URL) paths.nextElement();
implementationList.add(path);
}
if (implementationList.size() > 1) {
Util.report("Class path contains multiple SLF4J bindings.");
for (int i = 0; i < implementationList.size(); i++) {
Util.report("Found binding in [" + implementationList.get(i) + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
}
org/slf4j/impl/StaticLoggerBinder.class有什麼特殊的嗎?
特殊之處在于它并不是在slf4j-api-xxx.jar中,它是在log4j的日志擴充卡(slf4j-log4j12-xxx.jar)或者是logback的日志擴充卡(logback-classic-xxx.jar)中,每種日志實作的jar裡面都會提供一個包路徑和類名稱全都相同的StaticLoggerBinder類。再看一眼bind()方法
private final static void bind() {
try {
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITILIZATION;
emitSubstituteLoggerWarning();
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (msg != null && msg.indexOf("org/slf4j/impl/StaticLoggerBinder") != -1) {
INITIALIZATION_STATE = NOP_FALLBACK_INITILIZATION;
Util
.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_STATICLOGGERBINDER_URL
+ " for further details.");
} else {
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_INITILIZATION;
Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
Util.report("Your binding is version 1.5.5 or earlier.");
Util.report("Upgrade your binding to version 1.6.x. or 2.0.x");
}
throw nsme;
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
關鍵的地方是紅色标注的,通過StaticLoggerBinder.getSingleton()實作綁定,這個類的執行個體要麼是log4j提供的jar裡面的StaticLoggerBinder類生成的對象,要麼是logback提供的jar裡面的StaticLoggerBinder類生成的對象。slf4j官方推薦隻能放一種日志實作到classpath中,那也就是程式中真正使用的日志實作。slf4j怎麼擷取到一種具體的日志也就明了了。
四、假設在classpath下既提供了log4j的實作,又提供了logback的實作,那麼slf4j具體使用的是哪種日志?
這個問題,涉及到了java的類加載機制,這又稍微複雜點。簡單說下java類加載機制就是:雙親委派機制。在不重寫自定義的classloader的情況下,Java虛拟機預設提供的是啟動類加載器(bootstrap class loader)、擴充類加載器(extensions class loader)、系統類加載器(system class loader),也是三層父子關系,使用者還可以實作自定義的類加載器。
不同的類加載器負責加載特定路徑的類,如下圖:
當目前類加載器需要加載一個類的時候,先通路父類加載器有沒有,一直通路到啟動類加載器,如果沒有加載,目前類加載器才去加載這個類。而classpath下面的類是由系統類加載器加載的。系統類加載器會按照類在classpath中的次序加載類,對于相同包路徑相同名字的類,類加載器隻會加載classpath第一次出現的那個,至于classpath後面再出現同路徑同名的類,classloader會忽略,不會再加載。
是以,如果classpath下即提供了log4j的實作,也提供了logback的實作,那麼看它們(slf4j-log4j12-xxx.jar、logback-classic-xxx.jar)在classpath下出現的次序,誰在前面,slf4j就使用誰來實作日志記錄。
下面的一個小工具可以判斷classpath下的一個class是從哪個jar裡面加載的
public class JWhich {
/**
* Prints the absolute pathname of the class file containing the specified class name,
* as prescribed by the current classpath.
* @param className Name of the class.
*/
public static void which(String className) {
if (!className.startsWith("/")) {
className = "/" + className;
}
className = className.replace('.', '/');
className = className + ".class";
java.net.URL classUrl = new JWhich().getClass().getResource(className);
if (classUrl != null) {
System.out.println("\nClass '" + className + "' found in \n'" + classUrl.getFile() + "'");
} else {
System.out.println("\nClass '" + className + "' not found in \n'" + System.getProperty("java.class.path") + "'");
}
}
public static void main(String args[]) {
if (args.length > 0) {
JWhich.which(args[0]);
} else {
System.err.println("Usage: java JWhich <classname>");
}
}
}
用法 java -classpath classpath JWhich com.xxx.XXX.class