日常开发中记录日志有很多框架,如:java.util.logging、Apache log4j、logback等。如果使用某种具体框架,一段时间后需要切换框架那么会很麻烦,因此有了Slf4j(Simple logging Facade for Java)。Slf4j提供了统一的日志接口,你可以根据需求配置不同的实现。这样可以并保持接口稳定,并轻易变更具体日志框架。
在使用时可以引入某个具体实现,例如slf4j-simple需要引入依赖:
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-simpleartifactId>
<version>1.7.25version>
dependency>
使用logback时引入依赖:
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.1.7version>
dependency>
使用时统一通过slf4j-api包中的类org.slf4j.LoggerFactory来获取具体的Logger对象。
public class Main {
public static void main(String[] args){
Logger logger = LoggerFactory.getLogger(Main.class);
logger.info("123");
}
}
通过依赖分析工具可以看到这些实现类都需要依赖slf4j-api包,并实现了包中定义的接口。
slf4j-api包中定义了几个主要的接口和类,如Logger接口、ILoggerFactory接口及LoggerFactory类。
Logger接口定义了日志的使用方法,如trace,info等,具体日志框架提供不同的实现。
ILoggerFactory定义了如何获取logger对象的方法。
LoggerFactory工具类用提供了具体的实现方法最终获取不同框架实现的logger对象。
接下来看看Slf4j是如何根据配置找到不同的日志实现,即获取具体的Logger对象。
Logger logger = LoggerFactory.getLogger(Main.class);
获取一个Logger对象归结为两步
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:357)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:383)
at script.Main.main(Main.java:14)
getILoggerFactory在状态更新为SUCCESSFUL_INITIALIZATION时,会通过StaticLoggerBinder.getSingleton().getLoggerFactory()返回ILoggerFactory实例。
......
import org.slf4j.impl.StaticLoggerBinder;
......
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
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://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
当我们引入不同的依赖,会发现StaticLoggerBinder所在的包也不同。根据源码中导入依赖可以发只有日志框架中的实现包中提供了org/slf4j/impl/StaticLoggerBinder.class实现,那么才能通过StaticLoggerBinder获取到ILoggerFactory具体实现。
即StaticLoggerBinder类实现了slf4j-api中的LoggerFactoryBinder接口,并返回某个具体日志框架ILoggerFactory实例。
那么问题来了,当同时引入多个Slf4j实现会发生什么?到底选择哪个日志框架?接着看一看performInitialization()方法。
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
at org.slf4j.LoggerFactory.findPossibleStaticLoggerBinderPathSet(LoggerFactory.java:296)
at org.slf4j.LoggerFactory.bind(LoggerFactory.java:146)
at org.slf4j.LoggerFactory.performInitialization(LoggerFactory.java:124)
at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:412)
findPossibleStaticLoggerBinderPathSet方法会尝试在引入的依赖中找到所有定义了org/slf4j/impl/StaticLoggerBinder.class的位置信息(URL对象),并保存在Set中(binderPathSet),用于后续的警告。
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
当这个Set大小大于1那么通过reportMultipleBindingAmbiguity方法会生成警告信息。即我们通常都会看到的一条警告:Class path contains multiple SLF4J bindings。
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/sunfei/.m2/repository/org/slf4j/slf4j-simple/1.7.25/slf4j-simple-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/sunfei/.m2/repository/ch/qos/logback/logback-classic/1.1.7/logback-classic-1.1.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
最后通过reportActualBinding方法提示最终使用的日志框架。可以看到还是通过引入的StaticLoggerBinder来确定最终使用的框架。那么当引入多个依赖时,根据依赖顺序,第一个依赖的实现最为最终使用的日志框架。其余的作为警告输出。
private static void reportActualBinding(Set<URL> binderPathSet) {
// binderPathSet can be null under Android
if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
}
}
Slf4j-api包定义统一接口来实现日志操作,而具体的日志实现工具需要在org.slf4j.impl下定义StaticLoggerBinder,这样Slf4j通过定义的工具类并使用StaticLoggerBinder类获取 ILoggerFactory实例,最终得到具体的Logger对象。