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

史培培發表於2019-03-04

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

上期我們分享了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的檔案,只要實現了這個檔案的日誌實現系統,都可以作為一種實現方式。
    • 如果找到很多種方式,那麼就尋找一種預設的方式。
    • 這就是日誌介面的工作方式,簡單高效,關鍵是完全解耦,不需要日誌實現部分提供任何的修改配置,只需要符合介面的標準就可以載入進來,有利於維護和各個類的日誌處理方式統一

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

微信公眾號:碼上論劍
請關注我的個人技術微信公眾號,訂閱更多內容

相關文章