天天看點

深入探索Logback日志架構的原理分析和開發實戰技術指南(中篇)

作者:java易

Logback日志架構

"Logback"是一個開源的日志元件,它的設計者也是"Log4j"的作者。相比于"Log4j",它擁有更好的特性,是以成為了一個取代"Log4j"的優秀的日志架構。如果您正尋找一款優秀的日志元件,那麼"Logback"将是一個不錯的選擇。

slf4j和logback的關系

用SLF4J來寫記錄日志的代碼,而不能用logback去寫日志。如果需要切換其他的日志架構比如log4j,隻需要更改依賴就好了。

slf4j

slf4j元件提供了一個統一的日志對象通路接口,而log4j和logback等日志元件實作了該接口中的标準規定。下面是使用該元件的詳細實作步驟:

  1. 使用LoggerFactory.getLogger(App.class)方法擷取一個Logger對象。
  2. 調用getILoggerFactory()方法擷取具體的LoggerFactory實作類,如Log4j。
  3. LoggerFactory的bind()方法會調用findPossibleStaticLoggerBinderPathSet()方法來查找Slf4j是否找到了具體的日志元件。如果找到了,則會傳回具體的對象,可以進行日志的記錄。如果沒有找到,則會提示沒有找到相關的類。
  4. 擷取到具體對象後(例如Log4j LoggerFactory),調用getLogger()方法擷取一個特定的日志對象,即可以輸出日志。

Slf4j的核心代碼

通過LoggerFactory去拿slf4j提供的一個Logger接口的具體實作。

getLogger方法

slf4j中擷取Logger執行個體的一個重載方法。它接受一個由Class對象描述的類,傳回一個對應的Logger執行個體。

java複制代碼Logger logger = LoggerFactory.getLogger(Object.class);   
           
java複制代碼public static Logger getLogger(Class clazz) {
    Logger logger = getLogger(clazz.getName());
    if (DETECT_LOGGER_NAME_MISMATCH) {
        Class autoComputedCallingClass = Util.getCallingClass();
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
            Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                            autoComputedCallingClass.getName()));
            Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
        }
    }
    return logger;
}
           

函數中的第一行代碼調用了getLogger(String name)方法,該方法是一個重載方法,接受一個字元串作為參數代表Logger執行個體的名稱。實際上,getLogger(Class clazz)方法中本質上完成的任務和getLogger(String name)方法相同:都是用來傳回一個與特定名稱相關聯的Logger執行個體。

接下來,函數會檢測應用程式中,是否存在Logger執行個體名稱與類名稱不比對的情況。如果有這種情況,函數會列印出來一些警告資訊,并提供一個網站位址以檢視更多的解釋。這種情況有時會出現,通常是因為Logger執行個體沒有被正确地命名導緻。

最後,函數傳回本次調用得到的Logger執行個體。

LoggerFactory的bind()方法

bind方法用來實作slf4j的靜态綁定的。它的主要功能是實作對slf4j實作的綁定和初始化。總之,這個函數的主要目的是在應用程式啟動時完成slf4j的靜态綁定,保證日志記錄功能能夠正常工作。

java複制代碼private final static void bind() {
    try {
        SetstaticLoggerBinderPathSet = null;
        if (!isAndroid()) {
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
        // the next line does the binding
        StaticLoggerBinder.getSingleton();
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        reportActualBinding(staticLoggerBinderPathSet);
        fixSubstituteLoggers();
        replayEvents();
        // release all resources in SUBST_FACTORY
        SUBST_FACTORY.clear();
    } 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);
    }
}
           

函數中核心的部分是StaticLoggerBinder.getSingleton(),它調用了org.slf4j.impl.StaticLoggerBinder這個類的getSingleton()方法,這個方法傳回了一個org.slf4j.ILoggerFactory。這個ILoggerFactory可以被用來構造slf4j的Logger執行個體。

在函數的前面,我們可以看到一系列的步驟來查找可能存在的StaticLoggerBinder路徑集合。之後,我們會檢查這些路徑,以確定隻有一個StaticLoggerBinder實作被綁定到類路徑上。如果有多個靜态綁定的檔案存在,就會報告一個MultipleBindingAmbiguity的異常。

在綁定完成後,函數會執行一些資源清理和事件重放的操作,以確定程式的正确性。如果在初始化過程中出現了任何異常,函數會記錄相關資訊,并抛出一個運作時異常以通知調用者。

java複制代碼static SetfindPossibleStaticLoggerBinderPathSet() {
    // use Set instead of list in order to deal with bug #138
    // LinkedHashSet appropriate here because it preserves insertion order
    // during iteration
    SetstaticLoggerBinderPathSet = new LinkedHashSet();
    try {
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumerationpaths;
        if (loggerFactoryClassLoader == null) {
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else {
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }
        while (paths.hasMoreElements()) {
            URL path = paths.nextElement();
            staticLoggerBinderPathSet.add(path);
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
}
           

在調用getLogger()方法時,實際上會在classpath下查找STATIC_LOGGER_BINDER_PATH這個值。對于所有的slf4j實作,它們的jar包路徑下都一定會存在"org/slf4j/impl/StaticLoggerBinder.class",這個路徑是非常重要的,它指明了找到slf4j實作所必需的檔案。

是以,可以說這個地方是整個slf4j的核心。對于任何使用slf4j的開發人員來說,都需要了解和掌握這個類路徑,并確定它在項目的classpath下存在。隻有這樣,才能夠正确地通路和使用所有的slf4j實作。

slf4j logback配置

pom.xml添加logback依賴。

xml複制代碼<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<version>1.7.25</version>
</dependency>
<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-classic</artifactId>
	<version>1.2.3</version>
</dependency>
<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-core</artifactId>
	<version>1.2.3</version>
</dependency>
           

log4j和logback的關系

log4j和logback就是兩個受歡迎的日志架構,同時logback同樣是由log4j的作者設計完成的,擁有更好的特性,用來取代log4j的一個日志架構。而slf4j是java的一個日志門面,實作了日志架構一些通用的api,log4j和logback是具體的日志架構,他們可以單獨的使用,也可以綁定slf4j一起使用。

Logback的配置檔案

配置檔案讀取順序

logback通常使用logback.xml進行配置,在啟動時,logback會按照以下順序加載配置檔案:

  1. 如果程式啟動時指定了logback.configurationFile屬性,則使用該屬性指定的配置檔案。例如:java -Dlogback.configurationFile=/path/to/mylogback.xml Test,這樣執行Test類的時候就會加載/path/to/mylogback.xml配置。
  2. 查找classpath下的logback.groovy檔案。
  3. 查找classpath下的logback-test.xml檔案。
  4. 查找classpath下的logback.xml檔案。
  5. 如果所有的檔案都不存在,則使用BasicConfigurator自動配置,将日志記錄輸出到控制台。

Logback配置詳解

配置logback-spring.xml

resources下配置的spring-logback.xml

xml複制代碼<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">

    <!-- 動态日志級别 -->
    <jmxConfigurator/>

    <!-- 定義日志檔案 輸出位置 -->
    <property name="log.home_dir" value="/usr/local/springboot/log"/>
    <property name="log.app_name" value="http-demo"/>
    <!-- 日志最大的曆史 30天 -->
    <property name="log.maxHistory" value="30"/>
    <property name="log.level" value="debug"/>
    <property name="log.maxSize" value="5MB" />

    <!-- ConsoleAppender 控制台輸出日志 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                <!-- 設定日志輸出格式 -->
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] %logger - %msg%n
            </pattern>
        </encoder>
    </appender>

    <!-- ERROR級别日志 -->
    <!-- 滾動記錄檔案,先将日志記錄到指定檔案,當符合某個條件時,将日志記錄到其他檔案 RollingFileAppender -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 過濾器,隻記錄WARN級别的日志 -->
        <!-- 果日志級别等于配置級别,過濾器會根據onMath 和 onMismatch接收或拒絕日志。 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 設定過濾級别 -->
            <level>ERROR</level>
            <!-- 用于配置符合過濾條件的操作 -->
            <onMatch>ACCEPT</onMatch>
            <!-- 用于配置不符合過濾條件的操作 -->
            <onMismatch>DENY</onMismatch>
        </filter>
        <!-- 最常用的滾動政策,它根據時間來制定滾動政策.既負責滾動也負責觸發滾動 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--日志輸出位置 可相對、和絕對路徑 -->
            <fileNamePattern>
                ${log.home_dir}/error/%d{yyyy-MM-dd}/${log.app_name}-%i.log
            </fileNamePattern>
            <!-- 可選節點,控制保留的歸檔檔案的最大數量,超出數量就删除舊檔案,假設設定每個月滾動,且<maxHistory>是6,
            則隻儲存最近6個月的檔案,删除之前的舊檔案。注意,删除舊檔案是,那些為了歸檔而建立的目錄也會被删除 -->
            <maxHistory>${log.maxHistory}</maxHistory>
            <!--日志檔案最大的大小-->
            <MaxFileSize>${log.maxSize}</MaxFileSize>
        </rollingPolicy>
        <encoder>
            <pattern>
                <!-- 設定日志輸出格式 -->
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <!-- INFO級别日志 appender -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${log.home_dir}/info/%d{yyyy-MM-dd}/${log.app_name}-%i.log</fileNamePattern>
            <maxHistory>${log.maxHistory}</maxHistory>
            <MaxFileSize>${log.maxSize}</MaxFileSize>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %logger - %msg%n</pattern>
        </encoder>
    </appender>


    <!-- DEBUG級别日志 appender -->
    <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${log.home_dir}/debug/%d{yyyy-MM-dd}/${log.app_name}-%i.log</fileNamePattern>
            <maxHistory>${log.maxHistory}</maxHistory>
            <MaxFileSize>${log.maxSize}</MaxFileSize>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!--設定一個向上傳遞的appender,所有級别的日志都會輸出-->
    <appender name="app" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${log.home_dir}/app/%d{yyyy-MM-dd}/${log.app_name}-%i.log</fileNamePattern>
            <maxHistory>${log.maxHistory}</maxHistory>
            <MaxFileSize>${log.maxSize}</MaxFileSize>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!--org.springframework.web包下的類的日志輸出-->
    <logger name="org.springframework.web" additivity="false" level="WARN">
        <appender-ref ref="WARN"/>
    </logger>
    <!--com.zgd包下的類的日志輸出-->
    <logger name="com.zgd" additivity="false" level="DEBUG" >
        <appender-ref ref="app" />
        <appender-ref ref="ERROR" />
        <!--列印控制台-->
        <appender-ref ref="CONSOLE" />
    </logger>

    <!-- root級别   DEBUG -->
    <root>
        <!-- 列印debug級别日志及以上級别日志 -->
        <level value="${log.level}"/>
        <!-- 控制台輸出 -->
        <appender-ref ref="CONSOLE"/>
        <!-- 不管什麼包下的日志都輸出檔案 -->
        <!--<appender-ref ref="ERROR"/>-->
    </root>
</configuration>
           
  1. 級别:從高到低 OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、ALL。
  2. 日志輸出規則:根據目前ROOT級别,日志輸出時,隻有目前級别高于ROOT級别時才會輸出。
  3. 每個配置的filter用來過濾掉輸出檔案裡面低級别的日志資訊。
  4. 當scan屬性設定為true時,配置檔案如果發生改變,将會被重新加載。預設值為true。
  5. scanPeriod屬性用于設定監測配置檔案是否有修改的時間間隔。如果沒有給出時間機關,将預設機關為毫秒。當scan為true時,此屬性生效。預設的時間間隔為1分鐘。
  6. debug屬性用于列印出logback内部日志資訊,實時檢視logback運作狀态。預設值為false。

slf4j log4j logback的聯系

log4j是一個流行的日志架構,而logback則是由log4j的作者設計的一個日志架構,它有更多的特性,被認為是log4j的替代品。slf4j是Java的一個日志門面,提供了一些通用的API,可以用于實作不同的具體日志架構,log4j和logback都可以獨立使用,也可以與slf4j配合使用。

深入探索Logback日志架構的原理分析和開發實戰技術指南(中篇)
  1. 如果應用隻導入“slf4j-api.jar日志門面”,沒有導入slf4j實作,則所有記錄日志的方法都無效,不會進行日志記錄,這稱為“SLF4J unbound”。
  2. 如果導入了“slf4j-api.jar日志門面”及logback-classic日志實作的jar包,則調用SLF4J後,底層會使用logback來實作記錄日志,這稱為“SLF4J bound to logback-classic”。
  3. 如果導入了“slf4j-api.jar日志門面”及log4j日志實作的jar包,并在中間導入一個适配包“slf4j-log412.jar”,則應用調用SLF4J記錄日志時,會先使用适配包,最終使用log4j實作進行記錄日志,這稱為“SLF4J bound to log4j”。

使用SLF4J無法進行完整的日志記錄操作,SLF4J隻是一個外觀。是以,通常情況下,與log4j或logback一起使用,以實作最大相容性,并便于切換日志系統。

繼續閱讀