阿里Java開發(fā)手冊(cè)思考(三)

題圖:by pixel2013 From pixabay

上期我們分享了Java中if/else復(fù)雜邏輯的處理

本期我們將分享Java中日志的處理(上)

想必大家都用過日志,雖然日志看起來可有可無,但是等到出問題的時(shí)候,日志就派上了大用場(chǎng),所以說日志打得好不好,規(guī)范不規(guī)范,直接影響了解決生產(chǎn)環(huán)境故障的效率,日志打的不好,有可能影響環(huán)境的性能,也有可能影響排查問題的難易程度,有可能排查問題的時(shí)間比寫代碼的時(shí)間還有多。

那么我們就來分析下阿里Java開發(fā)手冊(cè)--日志規(guī)約第一條:
【強(qiáng)制】應(yīng)用中不可直接使用日志系統(tǒng)(Log4j、Logback)中的 API,而應(yīng)依賴使用日志框架 SLF4J 中的 API,使用門面模式的日志框架,有利于維護(hù)和各個(gè)類的日志處理方式統(tǒng)一。

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

日志框架

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

  • Log4j Apache Log4j是一個(gè)基于Java的日志記錄工具。它是由Ceki Gülcü首創(chuàng)的,現(xiàn)在則是Apache軟件基金會(huì)的一個(gè)項(xiàng)目。

  • Log4j 2 Apache Log4j 2是apache開發(fā)的一款Log4j的升級(jí)產(chǎn)品。

  • Commons Logging Apache基金會(huì)所屬的項(xiàng)目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名為Commons Logging。

  • Slf4j 類似于Commons Logging,是一套簡(jiǎn)易Java日志門面,本身并無日志的實(shí)現(xiàn)。(Simple Logging Facade for Java,縮寫Slf4j)。

  • Logback 一套日志組件的實(shí)現(xiàn)(slf4j陣營(yíng))。

  • Jul (Java Util Logging),自Java1.4以來的官方日志實(shí)現(xiàn)。

使用示例

  • Jul
import java.util.logging.Logger;

private static final Logger logger = Logger.getLogger("name");
...
try {
...
} catch (Exception e) {
    logger.error(".....error");
}

if(logger.isDebugEnabled()) {
    logger.debug("....." + name);
}
  • Log4j
import org.apache.log4j.Logger;

private static final Logger logger = Logger.getLogger(Abc.class.getNeme());
...
try {
...
} catch (Exception e) {
    logger.error(".....error");
}

if(logger.isDebugEnabled()) {
    logger.debug("....." + name);
}
  • Commons Logging
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

private static final Log logger = LogFactory.getLogger(Abc.class);
...
try {
...
} catch (Exception e) {
    logger.error(".....error");
}

if(logger.isDebugEnabled()) {
    logger.debug("....." + name);
}
  • Slf4j
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger logger = LoggerFactory.getLogger(Abc.class);
...
try {
...
} catch (Exception e) {
    logger.error(".....error {}", e.getMessage(), e);
}

logger.debug(".....{}", name);
  • Jul
    • 不支持占位符
    • 具體日志實(shí)現(xiàn)
  • Log4j
    • 不支持占位符
    • 具體日志實(shí)現(xiàn)
  • Logback
    • 不支持占位符
    • 具體日志實(shí)現(xiàn)
  • Commons Logging
    • 不支持占位符
    • 日志門面
  • Slf4j
    • 支持占位符
    • 日志門面

Slf4j中有一個(gè)很重要的特性:占位符,{}可以拼接任意字符串,相比如其他框架的優(yōu)點(diǎn)即不需要用+來拼接字符串,也就不會(huì)創(chuàng)建新的字符串對(duì)象,所以像log4j中需要加isDebugEnabled()的判斷就是這個(gè)道理,在slf4j中就不需要加判斷。

門面模式

門面(Facade)模式,又稱外觀模式,對(duì)外隱藏了系統(tǒng)的復(fù)雜性,并向客戶端提供了可以訪問的接口,門面模式的好處是將客戶端和子系統(tǒng)松耦合,方便子系統(tǒng)的擴(kuò)展和維護(hù)。

正是門面模式這樣的特點(diǎn),使用Slf4j門面,不管日志組件使用的是log4j還是logback等等,對(duì)于調(diào)用者而言并不關(guān)心使用的是什么日志組件,而且對(duì)于日志組件的更換或者升級(jí),調(diào)用的地方也不要做任何修改。

源碼分析

此處應(yīng)有代(zhang)碼(sheng):

首先使用靜態(tài)工廠來獲取Logger對(duì)象,傳入的class,最終會(huì)轉(zhuǎn)化為name,每個(gè)類的日志處理可能不同,所以根據(jù)傳入類的名字來判斷類的實(shí)現(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,那么會(huì)更改狀態(tài)為正在初始化ONGOING_INITIALIZATION,并執(zhí)行初始化performInitialization(),初始化完成之后,判斷初始化的狀態(tài),如果初始化成功SUCCESSFUL_INITIALIZATION,那么會(huì)通過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()方法,首先獲取實(shí)現(xiàn)日志的加載路徑,檢查路徑是否合法,然后初始化StaticLoggerBinder的對(duì)象,尋找合適的實(shí)現(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()方法中最重要的方法就是尋找實(shí)現(xiàn)方式findPossibleStaticLoggerBinderPathSet,具體方法實(shí)現(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;
}

注意??!前方高能?。?/strong>

Slf4j的絕妙之處就在于此,類加載器加載類,也就是說尋找StaticLoggerBinder.class文件,然后只要實(shí)現(xiàn)了這個(gè)類的日志組件,都可以作為一種實(shí)現(xiàn),如果有多個(gè)實(shí)現(xiàn),那么誰先加載就使用誰,這個(gè)地方涉及JVM的類加載機(jī)制

橋接

  • 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
    • 一個(gè)簡(jiǎn)單實(shí)現(xiàn)的橋接器
  • slf4j-jcl-1.7.13.jar
    • Jakarta Commons Logging 的橋接器. 這個(gè)橋接器將SLF4j所有日志委派給JCL
  • logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar)
    • slf4j的原生實(shí)現(xiàn),logback直接實(shí)現(xiàn)了slf4j的接口,因此使用slf4j與logback的結(jié)合使用也意味更小的內(nèi)存與計(jì)算開銷

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

橋接
  • 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
橋接遺留api
  • 橋接注意事項(xiàng)

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

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

總結(jié)

  • 為了更好的了解Slf4j,你需要了解:

    • JVM類加載機(jī)制
    • 設(shè)計(jì)模式:門面模式、橋接模式
  • 簡(jiǎn)單總結(jié)Slf4j的原理:

    • 通過工廠類,提供一個(gè)的接口,用戶可以通過這個(gè)門面,直接使用API實(shí)現(xiàn)日志的記錄。
    • 而具體實(shí)現(xiàn)由Slf4j來尋找加載,尋找的過程,就是通過類加載加載org/slf4j/impl/StaticLoggerBinder.class的文件,只要實(shí)現(xiàn)了這個(gè)文件的日志實(shí)現(xiàn)系統(tǒng),都可以作為一種實(shí)現(xiàn)方式。
    • 如果找到很多種方式,那么就尋找一種默認(rèn)的方式。
    • 這就是日志接口的工作方式,簡(jiǎn)單高效,關(guān)鍵是完全解耦,不需要日志實(shí)現(xiàn)部分提供任何的修改配置,只需要符合接口的標(biāo)準(zhǔn)就可以加載進(jìn)來,有利于維護(hù)和各個(gè)類的日志處理方式統(tǒng)一。
最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 歷史 log4j可以當(dāng)之無愧地說是Java日志框架的元老,1999年發(fā)布首個(gè)版本,2012年發(fā)布最后一個(gè)版本,20...
    kelgon閱讀 10,290評(píng)論 3 53
  • 問題 在項(xiàng)目啟動(dòng)時(shí),發(fā)現(xiàn)打印了大量的debug日志,但是src/main/resources下明明有l(wèi)og4j.x...
    Mr胡桃閱讀 22,745評(píng)論 2 11
  • 概述 在項(xiàng)目開發(fā)中,為了跟蹤代碼的運(yùn)行情況,常常要使用日志來記錄信息。在Java世界,有很多的日志工具庫來實(shí)現(xiàn)日志...
    靜默虛空閱讀 1,975評(píng)論 1 9
  • 前言 最近學(xué)習(xí)開java web服務(wù)器開發(fā),開始學(xué)習(xí)java,處理業(yè)務(wù)邏輯,但對(duì)其中的日志比較好奇,之前沒怎么接觸...
    九風(fēng)萍舟閱讀 3,415評(píng)論 1 6
  • 在項(xiàng)目開發(fā)過程中,我們可以通過 debug 查找問題。而在線上環(huán)境我們查找問題只能通過打印日志的方式查找問題。因此...
    Java架構(gòu)閱讀 3,572評(píng)論 2 41

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