工作十年的老程序員告訴你為什么使用SLF4J,及如何使用SLF4J日志框架

背景

想必小伙伴們都用過日志铝噩,雖然日志看起來可有可無衡蚂,但是等到出問題的時候,就比較棘手骏庸。所以說日志框架使用好不好毛甲,規(guī)范不規(guī)范,直接影響了解決生產(chǎn)環(huán)境故障的效率具被,日志框架選的不好玻募、用的不好,有可能影響環(huán)境的性能硬猫,也有可能影響排查問題的難易程度补箍。

那么如何來正確使用日志框架呢?

阿里Java開發(fā)手冊--日志規(guī)約第一條: 【強制】應(yīng)用中不可直接使用日志系統(tǒng)(Log4j啸蜜、Logback)中的 API,而應(yīng)依賴使用日志框架 SLF4J 中的 API辈挂,使用門面模式的日志框架衬横,有利于維護和各個類的日志處理方式統(tǒng)一。

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
private static final Logger logger = LoggerFactory.getLogger(Abc.class);

為什么會有此規(guī)范呢终蒂?我們先來了解下日志框架蜂林。

日志框架

Java中的日志框架分如下幾種:

  • Log4j Apache Log4j是一個基于Java的日志記錄工具遥诉。它是由Ceki Gülcü首創(chuàng)的,現(xiàn)在則是Apache軟件基金會的一個項目噪叙。
    不支持占位符
    具體日志實現(xiàn)

  • Log4j 2 Apache Log4j 2是apache開發(fā)的一款Log4j的升級產(chǎn)品矮锈。
    不支持占位符
    具體日志實現(xiàn)

  • Commons Logging Apache基金會所屬的項目,是一套Java日志接口睁蕾,之前叫Jakarta Commons Logging苞笨,簡稱JCL 后更名為Commons Logging。
    不支持占位符
    日志門面

  • Logback 一套日志組件的實現(xiàn)(slf4j陣營)子眶。
    不支持占位符
    具體日志實現(xiàn)

  • Jul (Java Util Logging),自Java1.4以來的官方日志實現(xiàn)瀑凝。
    不支持占位符
    具體日志實現(xiàn)

  • Slf4j 類似于Commons Logging,是一套簡易Java日志門面臭杰,本身并無日志的實現(xiàn)粤咪。(Simple Logging Facade for Java,縮寫Slf4j)渴杆。
    支持占位符
    日志門面

重點來看下Slf4j
官方說明

Simple Logging Facade for Java(SLF4J): 可用作各種日志框架的簡單外觀或抽象寥枝,例如java.util.logging,logback和log4j磁奖。SLF4J允許最終用戶在部署時插入所需的日志記錄框架囊拜。

為什么阿里要強制 依賴使用日志框架 SLF4J 中的 API 或者說Slf4j的特點: 除了得益于使用了門面模式,還有一個重要特性 支持占位符点寥。

門面模式

門面(Facade)模式艾疟,對外隱藏了系統(tǒng)的復(fù)雜性,并向客戶端提供了可以訪問的接口敢辩,門面模式的好處是將客戶端和子系統(tǒng)松耦合蔽莱,方便子系統(tǒng)的擴展和維護。
正是門面模式這樣的特點戚长,使用Slf4j門面盗冷,不管日志組件使用的是log4j還是logback等等,對于調(diào)用者而言并不關(guān)心使用的是什么日志組件同廉,而且對于日志組件的更換或者升級仪糖,調(diào)用的地方也不要做任何修改。

占位符

Slf4j中有一個很重要的特性:占位符迫肖,{}可以拼接任意字符串锅劝,相比如其他框架的優(yōu)點即不需要用+來拼接字符串,也就不會創(chuàng)建新的字符串對象蟆湖。

使用注意點:

基本原則:嵌入式組件(如庫和框架)不應(yīng)聲明對任何SLF4J綁定的依賴性故爵,而只依賴于slf4j-api。
如果庫聲明了對slf4j-api特定綁定的傳遞依賴時隅津,該種綁定強加給最終用戶诬垂, 并非SLF4J的目的劲室。
請注意,聲明對綁定的非傳遞依賴性(例如结窘,scope用于test 或 provide)不會影響最終用戶很洋。

源碼分析

首先使用靜態(tài)工廠來獲取Logger對象,傳入的class隧枫,最終會轉(zhuǎn)化為name喉磁,每個類的日志處理可能不同,所以根據(jù)傳入類的名字來判斷類的實現(xiàn)方式

public static Logger getLogger(Class clazz) {
    return getLogger(clazz.getName());
}

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

真正核心的在getILoggerFactory()中悠垛,首先判斷初始化的狀態(tài)INITIALIZATION_STATE线定,如果沒有初始化UNINITIALIZED,那么會更改狀態(tài)為正在初始化ONGOING_INITIALIZATION确买,并執(zhí)行初始化performInitialization()斤讥,初始化完成之后,判斷初始化的狀態(tài)湾趾,如果初始化成功SUCCESSFUL_INITIALIZATION芭商,那么會通過StaticLoggerBinder獲取日志工廠getLoggerFactory(),這里又涉及到了單例模式搀缠。

public static ILoggerFactory getILoggerFactory() {
    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("org.slf4j.LoggerFactory could not be successfully initialized. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
        case ONGOING_INITIALIZATION:
            return TEMP_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}

接著我們分析performInitialization是如何初始化的铛楣,首先是執(zhí)行bind()方法,然后判斷如果狀態(tài)為初始化成功SUCCESSFUL_INITIALIZATION艺普,執(zhí)行版本檢查簸州,主要是檢查jdk版本與slf4j的版本,看是否匹配歧譬。

private static final void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == 3) {
        versionSanityCheck();
    }
}

bind()方法岸浑,首先獲取實現(xiàn)日志的加載路徑,檢查路徑是否合法瑰步,然后初始化StaticLoggerBinder的對象矢洲,尋找合適的實現(xiàn)方式使用。

private static final void bind() {
    try {
        Set staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
        reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);

        StaticLoggerBinder.getSingleton();
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        reportActualBinding(staticLoggerBinderPathSet);
        emitSubstituteLoggerWarning();
    } 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 http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.");
        } else {
            failedBinding(ncde);
            throw ncde;
        }
    } catch (NoSuchMethodError nsme) {
        String msg = nsme.getMessage();
        if ((msg != null) && (msg.indexOf("org.slf4j.impl.StaticLoggerBinder.getSingleton()") != -1)) {
            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);
    }
}

可以看出缩焦,bind()方法中最重要的方法就是尋找實現(xiàn)方式findPossibleStaticLoggerBinderPathSet读虏,具體方法實現(xiàn)如下:

private static Set findPossibleStaticLoggerBinderPathSet() {
    Set staticLoggerBinderPathSet = new LinkedHashSet();
    try {
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration paths;
        Enumeration paths;
        if (loggerFactoryClassLoader == null) {
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else {
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }

        while (paths.hasMoreElements()) {
            URL path = (URL)paths.nextElement();
            staticLoggerBinderPathSet.add(path);
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
}

Slf4j的絕妙之處就在于此,類加載器加載類袁滥,也就是說尋找StaticLoggerBinder.class文件盖桥,然后只要實現(xiàn)了這個類的日志組件,都可以作為一種實現(xiàn)题翻,如果有多個實現(xiàn)葱轩,那么誰先加載就使用誰,這個地方涉及JVM的類加載機制藐握。

橋接

Slf4j與其他日志組件的橋接(Bridge)

  • slf4j-log4j12-1.7.13.jar
    log4j1.2版本的橋接器
  • slf4j-jdk14-1.7.13.jar
    java.util.logging的橋接器
  • slf4j-nop-1.7.13.jar
    NOP橋接器
  • slf4j-simple-1.7.13.jar
    一個簡單實現(xiàn)的橋接器
  • slf4j-jcl-1.7.13.jar
    Jakarta Commons Logging 的橋接器. 這個橋接器將SLF4j所有日志委派給JCL
  • logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar)
    slf4j的原生實現(xiàn)靴拱,logback直接實現(xiàn)了slf4j的接口,因此使用slf4j與logback的結(jié)合使用也意味更小的內(nèi)存與計算開銷

Slf4j Manual中有一張圖清晰的展示了接入方式猾普,如下:

slf4j bound to 其它log框架

Bridging legacy APIs(橋接遺留的api)

  • log4j-over-slf4j-version.jar * 將log4j重定向到slf4j
  • jcl-over-slf4j-version.jar
    將commos logging里的Simple Logger重定向到slf4j
  • jul-to-slf4j-version.jar
    將Java Util Logging重定向到slf4j
slf4j bound to log 替換

橋接注意事項

在使用slf4j橋接時要注意避免形成死循環(huán)袜炕,在項目依賴的jar包中不要存在以下情況

  • log4j-over-slf4j.jar和slf4j-log4j12.jar同時存在
    從名字上就能看出,前者重定向給后者初家,后者又委派給前者偎窘,會形成死循環(huán)
  • jul-to-slf4j.jar和slf4j-jdk14.jar同時存在
    從名字上就能看出,前者重定向給后者溜在,后者又委派給前者陌知,會形成死循環(huán)

小結(jié)

  • 切記阿里Java開發(fā)手冊--日志規(guī)約第一條
    【強制】應(yīng)用中不可直接使用日志系統(tǒng)(Log4j、Logback)中的 API掖肋,而應(yīng)依賴使用日志框架 SLF4J 中的 API

  • Log使用基本原則
    在嵌入式組件(如庫和框架)開發(fā)時仆葡,不應(yīng)聲明對任何SLF4J綁定實現(xiàn)的依賴性,而只依賴于slf4j-api志笼。

  • 為了更好的了解Slf4j沿盅,你需要了解:
    1. JVM類加載機制
    2. 設(shè)計模式:門面模式、橋接模式

  • 簡單總結(jié)Slf4j的原理:
    1. 通過工廠類纫溃,提供一個的接口腰涧,用戶可以通過這個門面,直接使用API實現(xiàn)日志的記錄紊浩。
    2. 而具體實現(xiàn)由Slf4j來尋找加載窖铡,尋找的過程,就是通過類加載加載org/slf4j/impl/StaticLoggerBinder.class的文件坊谁,只要實現(xiàn)了這個文件的日志實現(xiàn)系統(tǒng)费彼,都可以作為一種實現(xiàn)方式。
    3. 如果找到很多種方式呜袁,那么就尋找一種默認的方式敌买。
    4. 這就是日志接口的工作方式,簡單高效阶界,關(guān)鍵是完全解耦虹钮,不需要日志實現(xiàn)部分提供任何的修改配置,只需要符合接口的標準就可以加載進來膘融,有利于維護和各個類的日志處理方式統(tǒng)一芙粱。

參考:
https://www.slf4j.org/manual.html
https://www.slf4j.org/faq.html#slf4j_compatible

歡迎關(guān)注公眾號,有更多精彩文章

架構(gòu)那些事兒

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市氧映,隨后出現(xiàn)的幾起案子春畔,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件律姨,死亡現(xiàn)場離奇詭異振峻,居然都是意外死亡,警方通過查閱死者的電腦和手機择份,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門扣孟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人荣赶,你說我怎么就攤上這事凤价。” “怎么了拔创?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵利诺,是天一觀的道長。 經(jīng)常有香客問我剩燥,道長慢逾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任躏吊,我火速辦了婚禮氛改,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘比伏。我一直安慰自己胜卤,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布赁项。 她就那樣靜靜地躺著葛躏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悠菜。 梳的紋絲不亂的頭發(fā)上舰攒,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音悔醋,去河邊找鬼摩窃。 笑死,一個胖子當著我的面吹牛芬骄,可吹牛的內(nèi)容都是我干的猾愿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼账阻,長吁一口氣:“原來是場噩夢啊……” “哼蒂秘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淘太,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤姻僧,失蹤者是張志新(化名)和其女友劉穎规丽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撇贺,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡赌莺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了显熏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雄嚣。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖喘蟆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鼓鲁,我是刑警寧澤蕴轨,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站骇吭,受9級特大地震影響橙弱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜燥狰,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一棘脐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧龙致,春花似錦蛀缝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至榛了,卻和暖如春在讶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霜大。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工构哺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人战坤。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓曙强,卻偏偏與公主長得像,于是被迫代替她去往敵國和親湖笨。 傳聞我的和親對象是個殘疾皇子旗扑,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348