SLF4J的全称是Simple Logging Facade for Java,是当前最流行的日志(包装)框架之一,它不是完成的一套日志框架实现,它主要用作各种日志框架(例如java.util.logging,logback,log4j)的简单外观或抽象,允许最终用户在部署时插入具体的日志框架。可以直接将SLF4J看做其他日志框架的统一API,用SLF4J的好处是,如果后期用户需要切换日志框架的实现,不需要改代码,只需要调整部分jar的依赖即可。
那么你一定很好奇,SLF4J是如何做到统一各个日志框架API的?本文试图给你揭露这一“秘密”。
部分已经流行的日志框架出现的比SLF4J早,比如java.util.logging、log4j等,所以为了制定一套统一的API而直接去这些日志实现框架做“手脚”已经不可能,即下面这一条路走不通:
于是乎,SLF4J想到了加一个适配层,加一个适配层的直接好处是使我们适配老的日志组件成为了可能,而且还能在适配层做些特殊化的处理,注意,这里的适配层并不是统一的一层,而是每个日志实现框架都有一个对应的适配层,这样管理起来更加方便:
如果这样做的话,会有多个适配的Jar出现,而且这些Jar的通用部分(比如统一的API)无法收拢来统一实现,为了抽离公共部分,于是有了SLF4J-API这一层:
这样的话,公共部分和定制化的实现都能更自由的处理和管理,那让我们看看真实的SLF4J的架构图:
我们可以看到,SLF4J-API层的实现就是slf4j-api.jar,而适配层的jar都是slf4j开头,例如Log4j的适配层实现是slf4j-log4j12.jar(注意,上图少了个j)。slf4j-api.jar里面有统一个API,其中有两个重要接口,即日志对象接口org.slf4j.Logger、日志工厂接口org.slf4j.ILoggerFactory,当然,slf4j-api.jar里面不会仅有接口,它还有一些实现类,因为SFL4J还肩负一个重要任务,那就是绑定具体的日志框架实现。
我们接下来看看slf4j-api.jar是如何做到绑定具体的日志框架的,前面提到过,SLF4J-API层需要和适配层联动才能做到这一点,一想到动态加载和绑定,Java体系里比较有名的就是SPI了,SPI的全称是Service Provider Interface,即服务提供者接口,其目的是为了在Java体系中能通过动态绑定接口实现类来支持第三方框架或组件的扩展,JVM中还提供了一个定制化的ClassLoader(即java.util.ServiceLoader)来实现这一点(但其实不太好用,功能也不强大,所以像Dubbo等框架都自己实现),SLF4J(注意,SLF4J的老版本是没有使用SPI的)也使用了SPI。我们一般都如下使用SLF4J(代码片段来自SLF4J官网):
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
我们在使用SLF4J的时候,并没有显示的去调用他的初始化方法,所以在调用LoggerFactory.getLogger方法时,SLF4J内部会进行状态检查,如果没有初始化过,就会直接进行初始化,其中的日志框架的绑定就是初始化的最重要内容,我们直接通过IDEA的调用关系图来了解这一过程:
上图中的LogFactory.getProvider中会去判断初始化状态,如果没有,那么将同步进行初始化,代码如下:
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
return logger;
}
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
public static ILoggerFactory getILoggerFactory() {
return getProvider().getLoggerFactory();
}
static SLF4JServiceProvider getProvider() {
// 判断初始化标记
if (INITIALIZATION_STATE == UNINITIALIZED) {
// 如果还未进行初始化,那么同步初始化
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
// 如果初始化成功,那么将返回日志框架实现者
case SUCCESSFUL_INITIALIZATION:
return PROVIDER;
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_PROVIDER;
}
throw new IllegalStateException("Unreachable code");
}
可见,同一时刻有且只有一个线程能去调用初始化方法,即performInitialization下面的bind方法:
private final static void performInitialization() {
// 绑定过程
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}
private final static void bind() {
try {
// 使用SPI的方式来加载日志框架实现(该方式出现于1.7.26及其之后的版本)
List<SLF4JServiceProvider> providersList = findServiceProviders();
reportMultipleBindingAmbiguity(providersList);
if (providersList != null && !providersList.isEmpty()) {
// 这里很关键,说明通过SPI机制,加载的第一个适配层将是SLF4J选定绑定的日志框架实现
PROVIDER = providersList.get(0);
PROVIDER.initialize();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 输出日志,当可选日志框架大于一时,告知我们实际绑定的日志框架
reportActualBinding(providersList);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_PROVIDER.getSubstituteLoggerFactory().clear();
} else {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("No SLF4J providers were found.");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_PROVIDERS_URL + " for further details.");
// 通过指定类来实现加载,该适配方式比较原始和暴力,1.7.26版本之前只有该适配方式,由于这里是新版本的代码,所以如果SPI没有加载到,那么流转到这里将导致不打印任何日志也不会报错,但会打印日志提示
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
}
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
从上面的代码片段来看,我们发现了两个重要方法,即 findServiceProviders 和 findPossibleStaticLoggerBinderPathSet,findServiceProviders是标准的通过SPI来加载日志实现框架的方法,而findPossibleStaticLoggerBinderPathSet方法是为了兜底老版本的实现,需要注意的是,老版本没有SPI的方式,如果将1.7.26版本之后的slf4j-api.jar和老版本的适配层jar一起使用,流程会走到了这里,将匹配到NOPLogger,导致不输出日志也不报错(但启动时会有日志提示)。这里还有一个关键点,即PROVIDER = providersList.get(0)的调用,说明了对于SPI机制,加载的第一个适配层将是SLF4J选定绑定的日志框架实现!我们看下findServiceProviders的实现:
private static List<SLF4JServiceProvider> findServiceProviders() {
// SLF4JServiceProvider是SLF4J提供的SPI接口,如果适配层实现了SPI规范和该接口,那么可以在这里被发现和加载
ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
for (SLF4JServiceProvider provider : serviceLoader) {
providerList.add(provider);
}
return providerList;
}
这里的是标准的Java SPI的使用方式,适配层的jar只要实现了SLF4JServiceProvider接口和SPI规范的一些配置即可被slf4j-api.jar发现并加载,例如在Log4j的适配slf4j-log4j12.jar中是这样做的:
如果通过SPI没有发现任何日志框架的实现怎么办(比如1.7.26之前的老版本)?所以在这里仍然保留了老版本的加载方式,即findPossibleStaticLoggerBinderPathSet方法的实现:
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
// 尝试去加载 org/slf4j/impl/StaticLoggerBinder.class
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;
}
这里可以看出,SFL4J尝试去发现所有StaticLoggerBinder类资源文件(注意,这里没有使用ClassLoader.loadClass方法是为了能发现所有的StaticLoggerBinder),StaticLoggerBinder是老版本SLF4J和适配层之间的连接契约,即适配层只要有StaticLoggerBinder这个类,就有机会被slf4j-api.jar来发现,我们看老版本的slf4j-log4j12.jar(例如1.7.25版本)中的StaticLoggerBinder实现(新版本已经不用这种方式,也没有这个类):
public class StaticLoggerBinder implements LoggerFactoryBinder {
/**
* The unique instance of this class.
*
*/
private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
/**
* Return the singleton of this class.
*
* @return the StaticLoggerBinder singleton
*/
public static final StaticLoggerBinder getSingleton() {
return SINGLETON;
}
/**
* Declare the version of the SLF4J API this implementation is compiled against.
* The value of this field is modified with each major release.
*/
// 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 = Log4jLoggerFactory.class.getName();
/**
* The ILoggerFactory instance returned by the {@link #getLoggerFactory}
* method should always be the same object
*/
private final ILoggerFactory loggerFactory;
private StaticLoggerBinder() {
loggerFactory = new Log4jLoggerFactory();
try {
@SuppressWarnings("unused")
Level level = Level.TRACE;
} catch (NoSuchFieldError nsfe) {
Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
}
}
public ILoggerFactory getLoggerFactory() {
return loggerFactory;
}
public String getLoggerFactoryClassStr() {
return loggerFactoryClassStr;
}
}
可以看出,StaticLoggerBinder是作为单例来使用的,最重要的方法就是getLoggerFactory,用来直接返回包装过Log4j的日志工厂接口ILoggerFactory(之前提到过的slf4j-api.jar的两个最重要的接口之一)的实现类。那么读者会有疑问,在老版本中,如果有多个适配实现,会使用哪一个呢?于是我们得看老版本的LoggerFacotry的bind方法实现方式,这里给出1.7.25版本的bind方法:
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
// 扫描所有的StaticLoggerBinder类文件
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// 显示的绑定,ClassLoader先加载谁就先绑定谁
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
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.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
INITIALIZATION_STATE = FAILED_INITIALIZATION;
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.");
}
throw nsme;
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
其中我们发现了关键的StaticLoggerBinder.getSingleton()调用,所以在老版本中,如果存在多个适配层的实现,那么JVM先加载哪个适配层的StaticLoggerBinder类,那么就相当于SLF4J绑定了哪个适配层。
这里做一个总结,本文简单分析了SLF4J的架构设计,已经新老版本slf4j-api.jar是如何绑定底层的日志框架实现的,希望帮助同学了解内部的一些设计考量和实现。