Any problem in computer science can be solved by anther layer of indirection.
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
日志门面存在的必要性
在java生态中,开始是没有日志的官方标准的,而是社区先出现了各种优秀的日志框架(各种日志框架的出现顺序如下图),而且彼此之间还不兼容;这样的话就会当在项目中引入的其他jar使用的日志框架和自己使用的日志框架不一致的时候,就会出现混乱,很难维护。所以,我们需要一个中间的抽象层,来隐藏各个日志框架的差异性,让对外的使用者对真正的底层日志实现无感,这样日志门面就出现了。
日志门面担当–SLF4J
1、初识SLF4J
SLF4J的意思是酸辣粉?呸,当然不是,你个吃货;它的英文全称是:Simple Logging Facade for Java ,简单的日志门面。我们先来看看SLF4J官网的一张经典的图:
这张图可以说是十分的详细了,不仅很好的说明了SLF4J是一个抽象层,而且还告诉了我们SLF4J如何和市面上各种各样的具体日志实现搭配使用:我们可以看到slf4j处于第二层,是直接面向我们应用的,也就是直接面向我们API调用工程师的;但是,我们发现有的日志框架和slf4j搭配使用只需要三层(比如logback,slf4j-simple等),而有的却需要四层(比如reload4,jul等),这又是为什么呢?
其实,这不就是我们前面说的原因,开始日志是没有规范的,各个社区自己玩自己的,日志门面SLF4J是后面出现的,有的日志框架在你SLF4J出现之前就有的,怎么遵从你slf4j的规范呢?是的,采用适配器模式,添加一个适配层帮助是配到SLF4J规范;而那些开始就遵守SLF4J规范的框架就不需要适配层的存在了,所以他们只需要单层就可以了。
2、SLF4J的规范
好了,说明了日志门面的作用后,我们来具体看看SLF4J指定的规范是怎么样的,了解了规范后,我们就可以在这个规范下实现自己的日志框架了,想想就很屌的样子,一起来看看吧:)
想想我们一般获取到logger实例是通过什么样的方式呢?我猜大家肯定会说:使用注解@SLF4J,确实,Lombook提供的这个编译期注解帮我们省去了自己编写如下代码来获取logger实例了:
Logger log = LoggerFactory.getLogger(Class<?> clazz);
LoggerFactory是SLF4J提供的类,它可以帮助我们获取到真正的日志打印对象Logger,那它到底提供了怎样的规范,可以获取到真正的Logger呢?我们一起来看看代码吧
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
LoggerFactory.getLogger的内部调用了上面静态的getLogger方法,看下这个方法的实现,这不就是一个抽象个工厂的模式吗?工厂的规范由SLF4J提供,角色的规范也由SLF4J提供,那么我们自己的日志框架只要依据工厂规范来实现自己的具体工厂,并且依据角色的规范来实现自己的具体日志实现类就可以了,那么还剩下一个问题就是:因为对于应用来说直接面向的仅仅是SLF4J的API,那么SLF4J如何发现第三方实现的具体工厂和日志实现类呢?其实,对于这种可插拔的功能,我们很容易想到使用SPI机制来实现,但是SLF4J究竟是如何来实现的呢,我们继续往下追踪代码:
来看看getILoggerFactory是如何拿到具体工厂的:
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");
}
这段代码的前半部分是进行初始化,去寻找具体的日志工厂,而后面的switch…case…是来根据初始化后的不同状态来进行返回一些默认的日志工厂或者真正的日志工厂;先来看最重要的初始化方法,初始化方法里面主要的就是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()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
} 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);
} finally {
postBindCleanUp();
}
}
SLF4J去探查具体的日志实现框架的逻辑就发生在findPossibleStaticLoggerBinderPathSet方法中,在方法中SLF4J会去加载org.slf4j.impl.StaticLoggerBinder.java这个类,并且会调用StaticLoggerBinder的静态方法getSingleton来进行绑定,这就是SLF4J提供的绑定规范了。
所以当我们自己的日志框架选择SLF4J作为日志门面,那么我们就必须要在org/slf4j/impl路径下提供一个的StaticLoggerBinder类(建议实现SLF4J提供的LoggerFactoryBinder接口)并要包含静态的getSingleton方法来实现绑定(其实就是返回单例的StaticLoggerBinder对象)。
3.特殊情况
当然,在这期间也会出现一些问题:比如如果我引入了多个第三方日志框架,探查到了多个org.slf4j.impl.StaticLoggerBinder.java类该怎么办呢? 还有就是:如果我们一个日志实现框架也没有引入,那么就不会探查到任何org.slf4j.impl.StaticLoggerBinder.java类,又会发生什么情况呢? 这些问题SLF4J都有考虑到,也都帮我们提供了解决方案如下:
如果我们在项目中引入了多个第三方的日志具体实现,那么SLF4J就会探查到多个org/slf4j/impl/StaticLoggerBinder.class,这时reportMultipleBindingAmbiguity方法会打印出日志提示,而且reportActualBinding会打印出日志告知最终采用的的StaticLoggerBinder。
而当我们没有引入任何的具体日志实现类的时候,也就是SLF4J没有探查到org/slf4j/impl/StaticLoggerBinder.class时,初始化的状态会被赋值为NOP_FALLBACK_INITIALIZATION(INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION),这样就会返回SLF4J默认实现的NOPLoggerFactory日志工厂,但是这个日志工厂提供的日志实现类打印日志时,不做任何操作,没有任何输出的。
写在最后
以上只是简单的介绍了下SLF4J作为日志门面的原理,其中SLF4J中还有一些有趣的点,比如说MDC(Mapped Diagnostic Context),还有为web应用提供的MDCInsertingServletFilter等等。所以还是希望大家有时间多看看SLF4J官网的内容。
另外,我们知道logback是SLF4J的直接实现,而且作者都是同一个人,在log4j2出来之前,SLF4J+logback可谓是日志的最佳配方,毕竟Springboot默认提供的就是它两。那么如何在SLF4J的规范下编写具体的日志实现框架,logback是我们不得不去学习的一个日志实现,考虑篇幅和时间为问题,关于logback的研究我们后续再一起研究,后会有期。