阿里Java開發手冊思考(三)

題圖:by pixel2013 From pixabay

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

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

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

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

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

日志框架

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

  • Log4j Apache Log4j是一個基于Java的日志記錄工具。它是由Ceki Gülcü首創的,現在則是Apache軟件基金會的一個項目。

  • Log4j 2 Apache Log4j 2是apache開發的一款Log4j的升級產品。

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

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

  • Logback 一套日志組件的實現(slf4j陣營)。

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

使用示例

  • 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
    • 不支持占位符
    • 具體日志實現
  • Log4j
    • 不支持占位符
    • 具體日志實現
  • Logback
    • 不支持占位符
    • 具體日志實現
  • Commons Logging
    • 不支持占位符
    • 日志門面
  • Slf4j
    • 支持占位符
    • 日志門面

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

門面模式

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

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

源碼分析

此處應有代(zhang)碼(sheng):

首先使用靜態工廠來獲取Logger對象,傳入的class,最終會轉化為name,每個類的日志處理可能不同,所以根據傳入類的名字來判斷類的實現方式

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

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

真正核心的在getILoggerFactory()中,首先判斷初始化的狀態INITIALIZATION_STATE,如果沒有初始化UNINITIALIZED,那么會更改狀態為正在初始化ONGOING_INITIALIZATION,并執行初始化performInitialization(),初始化完成之后,判斷初始化的狀態,如果初始化成功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是如何初始化的,首先是執行bind()方法,然后判斷如果狀態為初始化成功SUCCESSFUL_INITIALIZATION,執行版本檢查,主要是檢查jdk版本與slf4j的版本,看是否匹配。

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

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

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()方法中最重要的方法就是尋找實現方式findPossibleStaticLoggerBinderPathSet,具體方法實現如下:

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文件,然后只要實現了這個類的日志組件,都可以作為一種實現,如果有多個實現,那么誰先加載就使用誰,這個地方涉及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
    • 一個簡單實現的橋接器
  • 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的原生實現,logback直接實現了slf4j的接口,因此使用slf4j與logback的結合使用也意味更小的內存與計算開銷

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
  • 橋接注意事項

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

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

總結

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

    • JVM類加載機制
    • 設計模式:門面模式、橋接模式
  • 簡單總結Slf4j的原理:

    • 通過工廠類,提供一個的接口,用戶可以通過這個門面,直接使用API實現日志的記錄。
    • 而具體實現由Slf4j來尋找加載,尋找的過程,就是通過類加載加載org/slf4j/impl/StaticLoggerBinder.class的文件,只要實現了這個文件的日志實現系統,都可以作為一種實現方式。
    • 如果找到很多種方式,那么就尋找一種默認的方式。
    • 這就是日志接口的工作方式,簡單高效,關鍵是完全解耦,不需要日志實現部分提供任何的修改配置,只需要符合接口的標準就可以加載進來,有利于維護和各個類的日志處理方式統一
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容