一、slf4j和log4j、logback的关系
slf4j(simple logging facade for java)简单日志门面,只是为各种日志实现提供了日志使用的接口,并没有具体的日志实现。
log4j和logback是具体的日志实现,不使用slf4j我们也可以直接使用log4j或者logback实现日志记录。slf4j相当于是各种日志接口的接口,调用的时候,直接使用slf4j的日志接口,具体的日志实现只需要通过提供不同的日志实现的jar和配置文件,就可以灵活的实现日志的切换,对于应用代码无感知。
通过一张图看看slf4j和log4j、logback的关系
可以看出来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