Any problem in computer science can be solved by anther layer of indirection.
計(jì)算機(jī)科學(xué)領(lǐng)域的任何問(wèn)題都可以通過(guò)增加一個(gè)間接的中間層來(lái)解決
日志門(mén)面存在的必要性
在java生態(tài)中,開(kāi)始是沒(méi)有日志的官方標(biāo)準(zhǔn)的,而是社區(qū)先出現(xiàn)了各種優(yōu)秀的日志框架(各種日志框架的出現(xiàn)順序如下圖),而且彼此之間還不兼容;這樣的話就會(huì)當(dāng)在項(xiàng)目中引入的其他jar使用的日志框架和自己使用的日志框架不一致的時(shí)候,就會(huì)出現(xiàn)混亂,很難維護(hù)。所以,我們需要一個(gè)中間的抽象層,來(lái)隱藏各個(gè)日志框架的差異性,讓對(duì)外的使用者對(duì)真正的底層日志實(shí)現(xiàn)無(wú)感,這樣日志門(mén)面就出現(xiàn)了。

日志門(mén)面擔(dān)當(dāng)--SLF4J
1、初識(shí)SLF4J
SLF4J的意思是酸辣粉?呸,當(dāng)然不是,你個(gè)吃貨;它的英文全稱是:Simple Logging Facade for Java ,簡(jiǎn)單的日志門(mén)面。我們先來(lái)看看SLF4J官網(wǎng)的一張經(jīng)典的圖:

這張圖可以說(shuō)是十分的詳細(xì)了,不僅很好的說(shuō)明了SLF4J是一個(gè)抽象層,而且還告訴了我們SLF4J如何和市面上各種各樣的具體日志實(shí)現(xiàn)搭配使用:我們可以看到slf4j處于第二層,是直接面向我們應(yīng)用的,也就是直接面向我們API調(diào)用工程師的;但是,我們發(fā)現(xiàn)有的日志框架和slf4j搭配使用只需要三層(比如logback,slf4j-simple等),而有的卻需要四層(比如reload4,jul等),這又是為什么呢?
其實(shí),這不就是我們前面說(shuō)的原因,開(kāi)始日志是沒(méi)有規(guī)范的,各個(gè)社區(qū)自己玩自己的,日志門(mén)面SLF4J是后面出現(xiàn)的,有的日志框架在你SLF4J出現(xiàn)之前就有的,怎么遵從你slf4j的規(guī)范呢?是的,采用適配器模式,添加一個(gè)適配層幫助是配到SLF4J規(guī)范;而那些開(kāi)始就遵守SLF4J規(guī)范的框架就不需要適配層的存在了,所以他們只需要單層就可以了。
2、SLF4J的規(guī)范
好了,說(shuō)明了日志門(mén)面的作用后,我們來(lái)具體看看SLF4J指定的規(guī)范是怎么樣的,了解了規(guī)范后,我們就可以在這個(gè)規(guī)范下實(shí)現(xiàn)自己的日志框架了,想想就很屌的樣子,一起來(lái)看看吧:)
想想我們一般獲取到logger實(shí)例是通過(guò)什么樣的方式呢?我猜大家肯定會(huì)說(shuō):使用注解@SLF4J,確實(shí),Lombook提供的這個(gè)編譯期注解幫我們省去了自己編寫(xiě)如下代碼來(lái)獲取logger實(shí)例了:
Logger log = LoggerFactory.getLogger(Class<?> clazz);
LoggerFactory是SLF4J提供的類,它可以幫助我們獲取到真正的日志打印對(duì)象Logger,那它到底提供了怎樣的規(guī)范,可以獲取到真正的Logger呢?我們一起來(lái)看看代碼吧
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
LoggerFactory.getLogger的內(nèi)部調(diào)用了上面靜態(tài)的getLogger方法,看下這個(gè)方法的實(shí)現(xiàn),這不就是一個(gè)抽象個(gè)工廠的模式嗎?工廠的規(guī)范由SLF4J提供,角色的規(guī)范也由SLF4J提供,那么我們自己的日志框架只要依據(jù)工廠規(guī)范來(lái)實(shí)現(xiàn)自己的具體工廠,并且依據(jù)角色的規(guī)范來(lái)實(shí)現(xiàn)自己的具體日志實(shí)現(xiàn)類就可以了,那么還剩下一個(gè)問(wèn)題就是:因?yàn)閷?duì)于應(yīng)用來(lái)說(shuō)直接面向的僅僅是SLF4J的API,那么SLF4J如何發(fā)現(xiàn)第三方實(shí)現(xiàn)的具體工廠和日志實(shí)現(xiàn)類呢?其實(shí),對(duì)于這種可插拔的功能,我們很容易想到使用SPI機(jī)制來(lái)實(shí)現(xiàn),但是SLF4J究竟是如何來(lái)實(shí)現(xiàn)的呢,我們繼續(xù)往下追蹤代碼:
來(lái)看看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");
}
這段代碼的前半部分是進(jìn)行初始化,去尋找具體的日志工廠,而后面的switch...case...是來(lái)根據(jù)初始化后的不同狀態(tài)來(lái)進(jìn)行返回一些默認(rèn)的日志工廠或者真正的日志工廠;先來(lái)看最重要的初始化方法,初始化方法里面主要的就是bind方法來(lái)進(jìn)行綁定具體的日志工廠實(shí)現(xiàn)類:
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去探查具體的日志實(shí)現(xiàn)框架的邏輯就發(fā)生在findPossibleStaticLoggerBinderPathSet方法中,在方法中SLF4J會(huì)去加載org.slf4j.impl.StaticLoggerBinder.java這個(gè)類,并且會(huì)調(diào)用StaticLoggerBinder的靜態(tài)方法getSingleton來(lái)進(jìn)行綁定,這就是SLF4J提供的綁定規(guī)范了。
所以當(dāng)我們自己的日志框架選擇SLF4J作為日志門(mén)面,那么我們就必須要在org/slf4j/impl路徑下提供一個(gè)的StaticLoggerBinder類(建議實(shí)現(xiàn)SLF4J提供的LoggerFactoryBinder接口)并要包含靜態(tài)的getSingleton方法來(lái)實(shí)現(xiàn)綁定(其實(shí)就是返回單例的StaticLoggerBinder對(duì)象)。
3.特殊情況
當(dāng)然,在這期間也會(huì)出現(xiàn)一些問(wèn)題:比如如果我引入了多個(gè)第三方日志框架,探查到了多個(gè)org.slf4j.impl.StaticLoggerBinder.java類該怎么辦呢? 還有就是:如果我們一個(gè)日志實(shí)現(xiàn)框架也沒(méi)有引入,那么就不會(huì)探查到任何org.slf4j.impl.StaticLoggerBinder.java類,又會(huì)發(fā)生什么情況呢? 這些問(wèn)題SLF4J都有考慮到,也都幫我們提供了解決方案如下:
如果我們?cè)陧?xiàng)目中引入了多個(gè)第三方的日志具體實(shí)現(xiàn),那么SLF4J就會(huì)探查到多個(gè)org/slf4j/impl/StaticLoggerBinder.class,這時(shí)reportMultipleBindingAmbiguity方法會(huì)打印出日志提示,而且reportActualBinding會(huì)打印出日志告知最終采用的的StaticLoggerBinder。
而當(dāng)我們沒(méi)有引入任何的具體日志實(shí)現(xiàn)類的時(shí)候,也就是SLF4J沒(méi)有探查到org/slf4j/impl/StaticLoggerBinder.class時(shí),初始化的狀態(tài)會(huì)被賦值為NOP_FALLBACK_INITIALIZATION(INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION),這樣就會(huì)返回SLF4J默認(rèn)實(shí)現(xiàn)的NOPLoggerFactory日志工廠,但是這個(gè)日志工廠提供的日志實(shí)現(xiàn)類打印日志時(shí),不做任何操作,沒(méi)有任何輸出的。
寫(xiě)在最后
以上只是簡(jiǎn)單的介紹了下SLF4J作為日志門(mén)面的原理,其中SLF4J中還有一些有趣的點(diǎn),比如說(shuō)MDC(Mapped Diagnostic Context),還有為web應(yīng)用提供的MDCInsertingServletFilter等等。所以還是希望大家有時(shí)間多看看SLF4J官網(wǎng)的內(nèi)容。
另外,我們知道logback是SLF4J的直接實(shí)現(xiàn),而且作者都是同一個(gè)人,在log4j2出來(lái)之前,SLF4J+logback可謂是日志的最佳配方,畢竟Springboot默認(rèn)提供的就是它兩。那么如何在SLF4J的規(guī)范下編寫(xiě)具體的日志實(shí)現(xiàn)框架,logback是我們不得不去學(xué)習(xí)的一個(gè)日志實(shí)現(xiàn),考慮篇幅和時(shí)間為問(wèn)題,關(guān)于logback的研究我們后續(xù)再一起研究,后會(huì)有期。