SLF4J--日志門(mén)面擔(dān)當(dāng)

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)了。

java日志歷史.png

日志門(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)典的圖:

slf4j經(jīng)典圖.png

這張圖可以說(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ì)有期。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容