1. 程式人生 > >原始碼詳解系列(七) ------ 全面講解logback的使用和原始碼

原始碼詳解系列(七) ------ 全面講解logback的使用和原始碼

什麼是logback

logback 用於日誌記錄,可以將日誌輸出到控制檯、檔案、資料庫和郵件等,相比其它所有的日誌系統,logback 更快並且更小,包含了許多獨特並且有用的特性。

logback 被分成三個不同的模組:logback-core,logback-classic,logback-access。

  1. logback-core 是其它兩個模組的基礎。
  2. logback-classic 模組可以看作是 log4j 的一個優化版本,它天然的支援 SLF4J。
  3. logback-access 提供了 http 訪問日誌的功能,可以與 Servlet 容器進行整合,例如:Tomcat、Jetty。

本文將介紹以下內容,由於篇幅較長,可根據需要選擇閱讀:

  1. 如何使用 logback:將日誌輸出到控制檯、檔案和資料庫,以及使用 JMX 配置 logback;

  2. logback 配置檔案詳解;

  3. logback 的原始碼分析。

如何使用logback

需求

  1. 使用 logback 將日誌資訊分別輸出到控制檯、檔案、資料庫。
  2. 使用 JMX 方式配置 logback。

工程環境

JDK:1.8.0_231
maven:3.6.1
IDE:Spring Tool Suite 4.3.2.RELEASE
mysql:5.7.28

主要步驟

  1. 搭建環境;
  2. 配置 logback 檔案;
  3. 編寫程式碼:獲取 Logger
    例項,並列印指定等級的日誌;
  4. 測試。

建立專案

專案型別 Maven Project ,打包方式 jar。

引入依賴

logack 天然的支援 slf4j,不需要像其他日誌框架一樣引入適配層(如 log4j 需引入 slf4j-log4j12 )。通過後面的原始碼分析可知,logback 只是將適配相關程式碼放入了 logback-classic。

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- logback+slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <!-- 輸出日誌到資料庫時需要用到 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>
        <!-- 使用資料來源方式輸出日誌到資料庫時需要用到 -->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.4</version>
        </dependency>
    </dependencies>

將日誌輸出到控制檯

配置檔案

配置檔案放在 resources 下,檔名可以為 logback-test.xml 或 logback.xml,實際專案中可以考慮在測試環境中使用 logback-test.xml ,在生產環境中使用 logback.xml( 當然 logback 還支援使用 groovy 檔案或 SPI 機制進行配置,本文暫不涉及)。

在 logback中,logger 可以看成為我們輸出日誌的物件,而這個物件列印日誌時必須遵循 appender 中定義的輸出格式和輸出目的地等。注意,root logger 是一個特殊的 logger。

<configuration>
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!--定義控制檯輸出格式-->
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

另外,即使我們沒有配置,logback 也會預設產生一個 root logger ,併為它配置一個 ConsoleAppender

編寫測試類

為了程式的解耦,一般我們在使用日誌時會採用門面模式,即通過 slf4j 或 commons-logging 來獲取 Logger 物件。

以下程式碼中,匯入的兩個類 LoggerLoggerFactory都定義在 slf4j-api 中,完全不會涉及到 logback 包的類。這時,如果我們想切換 log4j 作為日誌支援,只要修改 pom.xml 和日誌配置檔案就行,專案程式碼並不需要改動。原始碼分析部分將分析 slf4j 如何實現門面模式。

    @Test
    public void test01() {
        Logger logger = LoggerFactory.getLogger(LogbackTest.class);
        
        logger.debug("輸出DEBUG級別日誌");
        logger.info("輸出INFO級別日誌");
        logger.warn("輸出WARN級別日誌");
        logger.error("輸出ERROR級別日誌");
        
    }

注意,這裡獲取的 logger 不是我們配置的 root logger,而是以 cn.zzs.logback.LogbackTest 命名的 logger,它繼承了祖先 root logger 的配置。

測試

執行測試方法,可以看到在控制檯列印如下資訊:

2020-01-16 09:10:40 [main] INFO  ROOT - 輸出INFO級別的日誌
2020-01-16 09:10:40 [main] WARN  ROOT - 輸出WARN級別的日誌
2020-01-16 09:10:40 [main] ERROR ROOT - 輸出ERROR級別的日誌

這時我們會發現,怎麼沒有 debug 級別的日誌?因為我們配置了日誌等級為 info,小於 info 等級的日誌不會被打印出來。日誌等級如下:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < OFF

將日誌輸出到滾動檔案

本例子將在以上例子基礎上修改。測試方法程式碼不需要修改,只要修改配置檔案就可以了。

配置檔案

前面已經講過,appender 中定義日誌的輸出格式和輸出目的地等,所以,要將日誌輸出到滾動檔案,只要修改appender 就行。logback 提供了RollingFileAppender來支援列印日誌到滾動檔案。

以下配置中,設定了檔案大小超過100M後會按指定命名格式生成新的日誌檔案。

<configuration>

    <!-- 定義變數 -->
    <property name="LOG_HOME" value="D:/growUp/test/log" />
    <property name="APP_NAME" value="logback-demo"/>
    
    <!-- 滾動檔案輸出 -->
    <appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 指定日誌檔案的名稱 -->
        <file>${LOG_HOME}/${APP_NAME}/error.log</file>
        
        <!-- 配置追加寫入 -->
        <append>true</append>    
        
        <!-- 級別過濾器 -->
        <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}/${APP_NAME}/notError-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 可選節點,控制保留的歸檔檔案的最大數量,超出數量就刪除舊檔案。
                注意,刪除舊檔案時, 那些為了歸檔而建立的目錄也會被刪除。 -->
            <MaxHistory>50</MaxHistory>
            <!-- 當日志文件超過maxFileSize指定的大小時,根據上面提到的%i進行日誌檔案滾動 -->
            <maxFileSize>100MB</maxFileSize>
            <!-- 設定檔案總大小 -->
            <totalSizeCap>20GB</totalSizeCap>
        </rollingPolicy>
        
        <!-- 日誌輸出格式-->
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="FILE" />
    </root>
</configuration>

測試

執行測試方法,我們可以在指定目錄看到生成的日誌檔案。

檢視日誌檔案,可以看到只打印了 error 等級的日誌:

將日誌輸出到資料庫

logback 提供了DBAppender來支援將日誌輸出到資料庫中。

建立表

logback 為我們提供了三張表用於記錄日誌, 在使用DBAppender之前,這三張表必須存在。

這三張表分別為:logging_event, logging_event_property 與 logging_event_exception。logback 自帶 SQL 指令碼來建立表,這些指令碼在 logback-classic/src/main/java/ch/qos/logback/classic/db/script 資料夾下,相關指令碼也可以再本專案的 resources/script 找到。

由於本文使用的是 mysql 資料庫,執行以下指令碼(注意,官方給的 sql 中部分欄位設定了NOT NULL 的約束,可能存在插入報錯的情況,可以考慮調整):

BEGIN;
DROP TABLE IF EXISTS logging_event_property;
DROP TABLE IF EXISTS logging_event_exception;
DROP TABLE IF EXISTS logging_event;
COMMIT;

BEGIN;
CREATE TABLE logging_event 
  (
    timestmp         BIGINT NOT NULL,
    formatted_message  TEXT NOT NULL,
    logger_name       VARCHAR(254) NOT NULL,
    level_string      VARCHAR(254) NOT NULL,
    thread_name       VARCHAR(254),
    reference_flag    SMALLINT,
    arg0              VARCHAR(254),
    arg1              VARCHAR(254),
    arg2              VARCHAR(254),
    arg3              VARCHAR(254),
    caller_filename   VARCHAR(254),
    caller_class      VARCHAR(254) NOT NULL,
    caller_method     VARCHAR(254) NOT NULL,
    caller_line       CHAR(4) NOT NULL,
    event_id          BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
  );
COMMIT;

BEGIN;
CREATE TABLE logging_event_property
  (
    event_id          BIGINT NOT NULL,
    mapped_key        VARCHAR(254) NOT NULL,
    mapped_value      TEXT,
    PRIMARY KEY(event_id, mapped_key),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
  );
COMMIT;

BEGIN;
CREATE TABLE logging_event_exception
  (
    event_id         BIGINT NOT NULL,
    i                SMALLINT NOT NULL,
    trace_line       VARCHAR(254) NOT NULL,
    PRIMARY KEY(event_id, i),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
  );
COMMIT;

可以看到生成了三個表:

配置檔案

logback 支援使用 DataSourceConnectionSource,DriverManagerConnectionSource 與 JNDIConnectionSource 三種方式配置資料來源 。本文選擇第一種,並使用以 c3p0 作為資料來源(第二種方式文中也會給出)。

這裡需要說明下,因為例項化 c3p0 的資料來源物件ComboPooledDataSource時,會去自動載入 classpath 下名為 c3p0-config.xml 的配置檔案,所以,我們不需要再去指定 dataSource 節點下的引數,如果是 druid 或 dbcp 等則需要指定。

<configuration>

    <!--資料庫輸出-->
    <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
        <!-- 使用jdbc方式 -->
        <!-- <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
            <driverClass>com.mysql.cj.jdbc.Driver</driverClass>
            <url>jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true</url>
            <user>root</user>
            <password>root</password>
        </connectionSource> -->
        <!-- 使用資料來源方式 -->
        <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
           <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
           </dataSource>
        </connectionSource>
    </appender>
        
    <root level="info">
        <appender-ref ref="DB" />
    </root>
</configuration>

測試

執行測試方法,可以看到資料庫中插入了以下資料:

使用JMX配置logback

logback 支援使用 JMX 動態地更新配置。開啟 JMX 非常簡單,只需要增加 jmxConfigurator 節點就可以了,如下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定義變數 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 開啟JMX支援 -->
    <jmxConfigurator />
    
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
        
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

在我們通過 jconsole 連線到伺服器上之後(jconsole 在 JDK 安裝目錄的 bin 目錄下),在 MBeans 面板上,在 "ch.qos.logback.classic.jmx.Configurator" 資料夾下你可以看到幾個選項。如下圖所示:

我們可以看到,在屬性中,我們可以檢視 logback 已經產生的 logger 和 logback 的內部狀態,通過操作,我們可以:

  • 獲取指定 logger 的級別。返回值可以為 null
  • 設定指定的 logger 的級別。想要設定為 null,傳遞 "null" 字串就可以
  • 通過指定的檔案重新載入配置
  • 通過指定的 URL 重新載入配置
  • 使用預設配置檔案重新載入 logback 的配置
  • 或者指定 logger 的有效級別

更多 JMX 相關內容可參考我的另一篇部落格:如何使用JMX來管理程式?

補充--兩種列印方式

實際專案中,有時我們需要對列印的內容進行一定處理,如下:

logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

這種情況會產生構建訊息引數的成本,為了避免以上損耗,可以修改如下:

if(logger.isDebugEnabled()) { 
  logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

當我們列印的是一個物件時,也可以採用以下方法來優化:

// 不推薦
logger.debug("The new entry is " + entry + ".");
// 推薦
logger.debug("The new entry is {}", entry);

配置檔案詳解

前面已經說過, logback 配置檔名可以為 logback-test.xml 、 logback.groovy 或 logback.xml ,除了採用配置檔案方式, logback 也支援使用 SPI 機制載入 ch.qos.logback.classic.spi.Configurator 的實現類來進行配置。以下講解僅針對 xml 格式檔案的配置方式展開。

另外,如果想要自定義配置檔案的名字,可以通過系統屬性指定:

-Dlogback.configurationFile=/path/to/config.xml

如果沒有載入到配置,logback 會呼叫 BasicConfigurator 進行預設的配置。

configuration

configuration 是 logback.xml 或 logback-test.xml 檔案的根節點。

configuration 主要用於配置某些全域性的日誌行為,常見的配置引數如下:

屬性名 描述
debug 是否列印 logback 的內部狀態,開啟有利於排查 logback 的異常。預設 false
scan 是否在執行時掃描配置檔案是否更新,如果更新時則重新解析並更新配置。如果更改後的配置檔案有語法錯誤,則會回退到之前的配置檔案。預設 false
scanPeriod 多久掃描一次配置檔案是否修改,單位可以是毫秒、秒、分鐘或者小時。預設情況下,一分鐘掃描一次配置檔案。

配置方式如下:

<configuration debug="true" scan="true" scanPeriod="60 seconds" >
    
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
       <target>system.err</target>   
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

使用以上配置進行測試:

如上圖,通過控制檯我們可以檢視 logback 載入配置的過程,這時,我們嘗試修改 logback 配置檔案的內容:

觀察控制檯,可以看到配置檔案重新載入:

logger

前面提到過,logger 是為我們列印日誌的物件,這個概念非常重要,有助於更好地理解 logger 的繼承關係。

在以下程式碼中,我們可以在getLogger方法中傳入的是當前類的 Class 物件或全限定類名,本質上獲取到的都是一個 logger 物件(如果該 logger 不存在,才會建立)。

    @Test
    public void test01() {
        Logger logger1 = LoggerFactory.getLogger(LogbackTest.class);
        Logger logger2 = LoggerFactory.getLogger("cn.zzs.logback.LogbackTest");
        System.err.println(logger == logger2);// true
    }

這裡補充一個問題,該 logger 物件以 cn.zzs.logback.LogbackTest 命名,和我們配置檔案中定義的 root logger 並不是同一個,但是為什麼這個 logger 物件卻擁有 root logger 的行為?

這要得益於 logger 的繼承關係,如下圖:

如果我們未指定當前 logger 的日誌等級,logback 會將其日誌等級設定為最近父級的日誌等級。另外,預設情況下,當前 logger 也會繼承最近父級持有的 appender。

下面測試下以上特性,將配置檔案進行如下修改:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds" debug="true">


    <!-- 定義變數 -->
    <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
    <property scope="system" name="APP_NAME" value="logback-demo"/>
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" />
    <!-- 檔案輸出 -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <append>true</append>
        <file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file>
        <immediateFlush>true</immediateFlush>
        <!-- 是否啟用安全寫入 -->
        <prudent>false</prudent>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <logger name="cn.zzs" level="error">
        <appender-ref ref="FILE" />
    </logger>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

這裡自定義了一個 logger,日誌等級是 error,appender 為檔案輸出。執行測試方法:

可以看到,名為 cn.zzs.logback.LogbackTest 的 logger 繼承了名為 cn.zzs 的 logger 的日誌等級和 appender,以及繼承了 root logger 的 appender。

實際專案中,如果不希望繼承父級的 appender,可以配置 additivity="false" ,如下:

    <logger name="cn.zzs" additivity="false">
       <appender-ref ref="FILE" />
    </logger>

注意,因為以下配置都是建立在 logger 的繼承關係上,所以這部分內容必須很好地理解。

appender

appender 用於定義日誌的輸出目的地和輸出格式,被 logger 所持有。logback 為我們提供了以下幾種常用的appender:

類名 描述
ConsoleAppender 將日誌通過 System.out 或者 System.err 來進行輸出,即輸出到控制檯。
FileAppender 將日誌輸出到檔案中。
RollingFileAppender 繼承自 FileAppender,也是將日誌輸出到檔案,但檔案具有輪轉功能。
DBAppender 將日誌輸出到資料庫
SocketAppender 將日誌以明文方式輸出到遠端機器
SSLSocketAppender 將日誌以加密方式輸出到遠端機器
SMTPAppender 將日誌輸出到郵件

本文僅會講解前四種,後四種可參考官方文件。

ConsoleAppender

ConsoleAppender 支援將日誌通過 System.out 或者 System.err 輸出,即輸出到控制檯,常用屬性如下:

屬性名 型別 描述
encoder Encoder 後面單獨講
target String System.out 或 System.err。預設為 System.out
immediateFlush boolean 是否立即重新整理。預設為 true。
withJansi boolean 是否啟用 Jansi 在 windows 使用 ANSI 彩色程式碼,預設值為 false。
在windows電腦上我嘗試開啟這個屬性並引入 jansi 包,但老是報錯,暫時沒有解決方案。

具體配置如下:

    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

FileAppender

FileAppender 支援將日誌輸出到檔案中,常用屬性如下:

屬性名 型別 描述
append boolean 是否追加寫入。預設為 true
encoder Encoder 後面單獨講
immediateFlush boolean 是否立即重新整理。預設為 true。
file String 要寫入檔案的路徑。如果檔案不存在,則新建。
prudent boolean 是否採用安全方式寫入,即使在不同的 JVM 或者不同的主機上執行 FileAppender 例項。預設的值為 false。

具體配置如下:

    <!-- 定義變數 -->
    <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
    <property scope="system" name="APP_NAME" value="logback-demo"/>
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    <timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" />
    
    <!-- 檔案輸出 -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

RollingFileAppender

RollingFileAppender 繼承自 FileAppender,也是將日誌輸出到檔案,但檔案具有輪轉功能。

RollingFileAppender 的屬性如下所示:

屬性名 型別 描述
file String 要寫入檔案的路徑。如果檔案不存在,則新建。
append boolean 是否追加寫入。預設為 true。
immediateFlush boolean 是否立即重新整理。預設為true。
encoder Encoder 後面單獨將
rollingPolicy RollingPolicy 定義檔案如何輪轉。
triggeringPolicy TriggeringPolicy 定義什麼時候發生輪轉行為。如果 rollingPolicy 使用的類已經實現了 triggeringPolicy 介面,則不需要再配置 triggeringPolicy,例如 SizeAndTimeBasedRollingPolicy。
prudent boolean 是否採用安全方式寫入,即使在不同的 JVM 或者不同的主機上執行 FileAppender 例項。預設的值為 false。

具體配置如下:

    <!-- 定義變數 -->
    <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
    <property scope="system" name="APP_NAME" value="logback-demo"/>
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>   
    <!-- 輪轉檔案輸出 -->
    <appender name="FILE-ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        
        <!-- 輪轉策略,它根據時間和檔案大小來制定輪轉策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天輪轉  -->
            <fileNamePattern>${LOG_HOME}/${APP_NAME}/log-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 儲存 30 天的歷史記錄,最大大小為 30GB -->
            <MaxHistory>30</MaxHistory>
            <totalSizeCap>30GB</totalSizeCap>
            <!-- 當日志文件超過100MB的大小時,根據上面提到的%i進行日誌檔案輪轉 -->
            <maxFileSize>100MB</maxFileSize>
        </rollingPolicy>
        
        <!-- 日誌輸出格式-->
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

DBAppender

參見使用例子。

encoder

encoder 負責將日誌事件按照配置的格式轉換為位元組陣列,常用屬性如下:

屬性名 型別 描述
pattern String 日誌列印格式。
outputPatternAsHeader boolean 是否將 pattern 字串插入到日誌檔案頂部。預設false。

針對 pattern 屬性,這裡補充下它的常用轉換字元:

轉換字元 描述
c{length}
lo{length}
logger{length}
輸出 logger 的名字。可以通過 length 縮短其長度。
但是,logger 名字最右邊永遠都會存在。
例如,當我們設定 logger{0}時,cn.zzs.logback.LogbackTest 中的
LogbackTest 永遠不會被刪除
C{length}
class{length}
輸出發出日誌請求的類的全限定名稱。
可以通過 length 縮短其長度。
d{pattern}
date{pattern}
d{pattern, timezone}
date{pattern, timezone}
輸出日誌事件的日期。
可以通過 pattern 設定日期格式,timezone 設定時區。
m / msg / message 輸出與日誌事件相關聯的,由應用程式提供的日誌資訊。
M / method 輸出發出日誌請求的方法名。
p / le / level 輸出日誌事件的級別。
t / thread 輸出生成日誌事件的執行緒名。
n 輸出平臺所依賴的行分割字元。
F / file 輸出發出日誌請求的 Java 原始檔名。
caller{depth}
caller{depthStart..depthEnd}
caller{depth, evaluator-1, ... evaluator-n}
caller{depthStart..depthEnd, evaluator-1, ... evaluator-n}
輸出生成日誌的呼叫者所在的位置資訊。
L / line 輸出發出日誌請求所在的行號。
property{key} 輸出屬性 key 所對應的值。

注意,在拼接 pattren 時,應該考慮使用“有意義的”轉換字元,避免產生不必要的效能開銷。具體配置如下:

    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
       
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <outputPatternAsHeader>true</outputPatternAsHeader>
        </encoder>
    </appender>

其中, 轉換說明符 %-5level 表示日誌事件的級別的字元應該向左對齊,保持五個字元的寬度。

filter

appender 除了定義日誌的輸出目的地和輸出格式,其實也可以對日誌事件進行過濾輸出,例如,僅輸出包含指定字元的日誌。而這個功能需配置 filter。

LevelFilter

LevelFilter 基於級別來過濾日誌事件。修改配置檔案如下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定義變數 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
       
       <!-- 設定過濾器 -->
       <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

執行測試方法,可見,雖然 root logger 的日誌等級是 info,但最終只會列印 error 的日誌:

ThresholdFilter

ThresholdFilter 基於給定的臨界值來過濾事件。如果事件的級別等於或高於給定的臨界,則過濾通過,否則會被攔截。配置如下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定義變數 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
        
        <!-- 設定過濾器 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
          <level>ERROR</level>
        </filter>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

執行測試方法,可見,雖然 root logger 的日誌等級是 info,但最終只會列印 error 的日誌:

EvaluatorFilter

EvaluatorFilter 基於給定的標準來過濾事件。 它採用 Groovy 表示式作為評估的標準。配置如下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

   <!-- 定義變數 -->
   <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
   
   <!-- 控制檯輸出 -->
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
   
      <target>system.err</target>
      
       <encoder charset="utf-8">
           <pattern>${LOG_PATTERN}</pattern>
       </encoder>
       
       <!-- 設定過濾器 -->
       <filter class="ch.qos.logback.core.filter.EvaluatorFilter">      
         <evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator"> 
           <expression>
              e.level.toInt() >= ERROR.toInt() &amp;&amp; 
              !(e.mdc?.get("req.userAgent") =~ /Googlebot|msnbot|Yahoo/ )
           </expression>
         </evaluator>
         <OnMismatch>DENY</OnMismatch>
         <OnMatch>NEUTRAL</OnMatch>
       </filter>
   </appender>
   
   <root level="info">
       <appender-ref ref="STDOUT" />
   </root>
</configuration>

上面的過濾器引用自官網,規則為:讓級別在 ERROR 及以上的日誌事件在控制檯顯示,除非是由於來自 Google,MSN,Yahoo 的網路爬蟲導致的錯誤。

注意,使用 GEventEvaluator 必須引入 groovy 的 jar 包:

        <!-- groovy -->
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy</artifactId>
            <version>3.0.0-rc-3</version>
        </dependency>

執行測試方法,輸出如下結果:

EvaluatorFilter 除了支援 Groovy 表示式,還支援使用 java 程式碼來作為過濾標準,修改配置檔案如下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定義變數 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
        
        <!-- 設定過濾器 -->
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">      
          <evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator -->
            <expression>return message.contains("ERROR");</expression>
          </evaluator>
          <OnMismatch>DENY</OnMismatch>
          <OnMatch>NEUTRAL</OnMatch>
        </filter>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

注意,使用 JaninoEventEvaluator 必須匯入 janino 包,如下:

        <!-- janino -->
        <dependency>
            <groupId>org.codehaus.janino</groupId>
            <artifactId>janino</artifactId>
            <version>3.1.0</version>
        </dependency>

執行測試方法,輸出如下結果:

原始碼分析

logback 非常龐大、複雜,如果要將 logback 所有模組分析完,估計要花相當長的時間,所以,本文還是和以前一樣,僅針對核心程式碼進行分析,當分析的方法存在多個實現時,也只會挑選其中一個進行講解。文中沒有涉及到的部分,感興趣的可以自行研究。

接下來通過解決以下幾個問題來逐步分析 logback 的原始碼:

  1. slf4j 是如何實現門面模式的?
  2. logback 如何載入配置?
  3. 獲取我們所需的 logger?
  4. 如何將日誌列印到控制檯?

slf4j是如何實現門面模式的

slf4j 使用的是門面模式,不管使用什麼日誌實現,專案程式碼都只會用到 slf4j-api 中的介面,而不會使用到具體的日誌實現的程式碼。slf4j 到底是如何實現門面模式的?接下來進行原始碼分析:

在我們的應用中,一般會通過以下方式獲取 Logger 物件,我們就從這個方法開始分析吧:

Logger logger = LoggerFactory.getLogger(LogbackTest.class);

進入到 LoggerFactory.getLogger(Class<?> clazz)方法,如下。在呼叫這個方法時,我們一般會以當前類的 Class 物件作為入參。當然,logback 也允許你使用其他類的 Class 物件作為入參,但是,這樣做可能不利於對 logger 的管理。通過設定系統屬性-Dslf4j.detectLoggerNameMismatch=true,當實際開發中出現該類問題,會在控制檯列印提醒資訊。

    public static Logger getLogger(Class<?> clazz) {
        // 獲取Logger物件,後面繼續展開
        Logger logger = getLogger(clazz.getName());
        // 如果系統屬性-Dslf4j.detectLoggerNameMismatch=true,則會檢查傳入的logger name是不是CallingClass的全限定類名,如果不匹配,會在控制檯列印提醒
        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;
    }

進入到LoggerFactory.getLogger(String name)方法,如下。在這個方法中,不同的日誌實現會返回不同的ILoggerFactory實現類:

    public static Logger getLogger(String name) {
        // 獲取工廠物件,後面繼續展開
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        // 利用工廠物件獲取Logger物件
        return iLoggerFactory.getLogger(name);
    }

進入到getILoggerFactory()方法,如下。INITIALIZATION_STATE代表了初始化狀態,該方法會根據初始化狀態的不同而返回不同的結果。

    static final SubstituteLoggerFactory SUBST_FACTORY = new SubstituteLoggerFactory();
    static final NOPLoggerFactory NOP_FALLBACK_FACTORY = new NOPLoggerFactory(); 
    public static ILoggerFactory getILoggerFactory() {
        // 如果未初始化
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    // 修改狀態為正在初始化
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    // 執行初始化
                    performInitialization();
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        // 如果StaticLoggerBinder類存在,則通過StaticLoggerBinder獲取ILoggerFactory的實現類
        case SUCCESSFUL_INITIALIZATION:
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        // 如果StaticLoggerBinder類不存在,則返回NOPLoggerFactory物件
        // 通過NOPLoggerFactory獲取到的NOPLogger沒什麼用,它的方法幾乎都是空實現
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_FACTORY;
        // 如果初始化失敗,則丟擲異常
        case FAILED_INITIALIZATION:
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        // 如果正在初始化,則SubstituteLoggerFactory物件,這個物件不作擴充套件
        case ONGOING_INITIALIZATION:
            return SUBST_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }

以上方法需要重點關注 StaticLoggerBinder這個類,它並不在 slf4j-api 中,而是在 logback-classic 中,如下圖所示。其實分析到這裡應該可以理解:slf4j 通過 StaticLoggerBinder 類與具體日誌實現進行關聯,從而實現門面模式。

接下來再簡單看下LoggerFactory.performInitialization(),如下。這裡會執行初始化,所謂的初始化就是查詢 StaticLoggerBinder 這個類是不是存在,如果存在會將該類繫結到當前應用,同時,根據不同情況修改INITIALIZATION_STATE。程式碼比較多,我概括下執行的步驟:

  1. 如果 StaticLoggerBinder 存在且唯一,修改初始化狀態為 SUCCESSFUL_INITIALIZATION;
  2. 如果 StaticLoggerBinder 存在但為多個,由 JVM 決定繫結哪個 StaticLoggerBinder,修改初始化狀態為 SUCCESSFUL_INITIALIZATION,同時,會在控制檯列印存在哪幾個 StaticLoggerBinder,並提醒使用者最終選擇了哪一個 ;
  3. 如果 StaticLoggerBinder 不存在,列印提醒,並修改初始化狀態為 NOP_FALLBACK_INITIALIZATION;
  4. 如果 StaticLoggerBinder 存在但 getSingleton() 方法不存在,列印提醒,並修改初始化狀態為 FAILED_INITIALIZATION;
    private final static void performInitialization() {
        // 查詢StaticLoggerBinder這個類是不是存在,如果存在會將該類繫結到當前應用
        bind();
        // 如果檢測存在
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            // 判斷StaticLoggerBinder與當前使用的slf4j是否適配
            versionSanityCheck();
        }
    }
    private final static void bind() {
        try {
            // 使用類載入器在classpath下查詢StaticLoggerBinder類。如果存在多個StaticLoggerBinder類,這時會在控制檯提醒並列出所有路徑(例如同時引入了logback和slf4j-log4j12 的包,就會出現兩個StaticLoggerBinder類)
            Set<URL> staticLoggerBinderPathSet = null;
            if (!isAndroid()) {
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            
            // 這一步只是簡單呼叫方法,但是非常重要。
            // 可以檢測StaticLoggerBinder類和它的getSingleton方法是否存在,如果不存在,分別會丟擲 NoClassDefFoundError錯誤和NoSuchMethodError錯誤
            // 注意,當存在多個StaticLoggerBinder時,應用不會停止,由JVM隨機選擇一個。
            StaticLoggerBinder.getSingleton();
            
            // 修改狀態為初始化成功
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            // 如果存在多個StaticLoggerBinder,會在控制檯提醒使用者實際選擇的是哪一個
            reportActualBinding(staticLoggerBinderPathSet);
            
            // 對SubstituteLoggerFactory的操作,不作擴充套件
            fixSubstituteLoggers();
            replayEvents();
            SUBST_FACTORY.clear();
            
        } catch (NoClassDefFoundError ncde) {
            // 當StaticLoggerBinder不存在時,會將狀態修改為NOP_FALLBACK_INITIALIZATION,並丟擲資訊
            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) {
            // 當StaticLoggerBinder.getSingleton()方法不存在時,會將狀態修改為初始化失敗,並丟擲資訊
            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);
        }
    }

這裡再補充一個問題,slf4j-api 中不包含 StaticLoggerBinder 類,為什麼能編譯通過呢?其實我們專案中用到的 slf4j-api 是已經編譯好的 class 檔案,所以不需要再次編譯。但是,編譯前 slf4j-api 中是包含 StaticLoggerBinder.java 的,且編譯後也存在 StaticLoggerBinder.class ,只是這個檔案被手動刪除了。

logback如何載入配置

前面說過,logback 支援採用 xml、grovy 和 SPI 的方式配置檔案,本文只分析 xml 檔案配置的方式。

logback 依賴於 Joran(一個成熟的,靈活的並且強大的配置框架 ),本質上是採用 SAX 方式解析 XML。因為 SAX 不是本文的重點內容,所以這裡不會去講解相關的原理,但是,這部分的分析需要具備 SAX 的基礎,可以參考我的另一篇部落格: 原始碼詳解系列(三) ------ dom4j的使用和分析(重點對比和DOM、SAX的區別)

logback 載入配置的程式碼還是比較繁瑣,且程式碼量較大,這裡就不一個個方法地分析了,而是採用類圖的方式來講解。下面是 logback 載入配置的大致圖解:

這裡再補充下圖中幾個類的作用:

類名 描述
SaxEventRecorder SaxEvent 記錄器。繼承了 DefaultHandler,所以在解析 xml 時會觸發對應的方法,
這些方法將觸發的引數封裝到 saxEven 中並放入 saxEventList 中
SaxEvent SAX 事件體。用於封裝 xml 事件的引數。
Action 執行的配置動作。
ElementSelector 節點模式匹配器。
RuleStore 用於存放模式匹配器-動作的鍵值對。

結合上圖,我簡單概括下整個執行過程:

  1. 使用 SAX 方式解析 XML,解析過程中根據當前的元素型別,呼叫 DefaultHandler 實現類的方法,構造 SaxEvent 並將其放入集合 saxEventList 中;
  2. 當 XML 解析完成,會呼叫 EventPlayer 的方法,遍歷集合 saxEventList 的 SaxEvent 物件,當該物件能夠匹配到對應的規則,則會執行相應的 Action。

簡單看下LoggerContext

現在回到 StaticLoggerBinder.getLoggerFactory()方法,如下。這個方法返回的 ILoggerFactory 其實就是 LoggerContext。

    private LoggerContext defaultLoggerContext = new LoggerContext();
    public ILoggerFactory getLoggerFactory() {
        // 如果初始化未完成,直接返回defaultLoggerContext
        if (!initialized) {
            return defaultLoggerContext;
        }
        
        if (contextSelectorBinder.getContextSelector() == null) {
            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
        }
        // 如果是DefaultContextSelector,返回的還是defaultLoggerContext
        // 如果是ContextJNDISelector,則可能為不同執行緒提供不同的LoggerContext 物件
        // 主要取決於是否設定系統屬性-Dlogback.ContextSelector=JNDI
        return contextSelectorBinder.getContextSelector().getLoggerContext();
    }

下面簡單看下 LoggerContext 的 UML 圖。它不僅作為獲取 logger 的工廠,還綁定了一些全域性的 Object、property 和 LifeCycle。

獲取logger物件

這裡先看下 Logger 的 UML 圖,如下。在 Logger 物件中,持有了父級 logger、子級 logger 和 appender 的引用。

進入LoggerContext.getLogger(String)方法,如下。這個方法邏輯簡單,但是設計非常巧妙,可以好好琢磨下。我概括下主要的步驟:

  1. 如果獲取的是 root logger,直接返回;
  2. 如果獲取的是 loggerCache 中快取的 logger,直接返回;
  3. 迴圈獲取 logger name 中包含的所有 logger,如果不存在就建立並放入快取;
  4. 返回 logger name 對應的 logger。
    public final Logger getLogger(final String name) {

        if (name == null) {
            throw new IllegalArgumentException("name argument cannot be null");
        }

        // 如果獲取的是root logger,直接返回
        if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
            return root;
        }

        int i = 0;
        Logger logger = root;

        // 在loggerCache中快取著已經建立的logger,如果存在,直接返回
        Logger childLogger = (Logger) loggerCache.get(name);
        if (childLogger != null) {
            return childLogger;
        }

        // 如果還找不到,就需要建立
        // 注意,要獲取以cn.zzs.logback.LogbackTest為名的logger,名為cn、cn.zzs、cn.zzs.logback的logger不存在的話也會被建立
        String childName;
        while (true) {
            // 從起始位置i開始,獲取“.”的位置
            int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
            // 擷取logger的名字
            if (h == -1) {
                childName = name;
            } else {
                childName = name.substring(0, h);
            }
            // 修改起始位置,以獲取下一個“.”的位置
            i = h + 1;
            synchronized (logger) {
                // 判斷當前logger是否存在以childName命名的子級
                childLogger = logger.getChildByName(childName);
                if (childLogger == null) {
                    // 通過當前logger來建立以childName命名的子級
                    childLogger = logger.createChildByName(childName);
                    // 放入快取
                    loggerCache.put(childName, childLogger);
                    // logger總數量+1
                    incSize();
                }
            }
            // 當前logger修改為子級logger
            logger = childLogger;
            // 如果當前logger是最後一個,則跳出迴圈
            if (h == -1) {
                return childLogger;
            }
        }
    }

進入Logger.createChildByName(String)方法,如下。

    Logger createChildByName(final String childName) {
        // 判斷要建立的logger在名字上是不是與當前logger為父子,如果不是會丟擲異常
        int i_index = LoggerNameUtil.getSeparatorIndexOf(childName, this.name.length() + 1);
        if (i_index != -1) {
            throw new IllegalArgumentException("For logger [" + this.name + "] child name [" + childName
                            + " passed as parameter, may not include '.' after index" + (this.name.length() + 1));
        }
        // 建立子logger集合
        if (childrenList == null) {
            childrenList = new CopyOnWriteArrayList<Logger>();
        }
        Logger childLogger;
        // 建立新的logger
        childLogger = new Logger(childName, this, this.loggerContext);
        // 將logger放入集合中
        childrenList.add(childLogger);
        // 設定有效日誌等級
        childLogger.effectiveLevelInt = this.effectiveLevelInt;
        return childLogger;
    }

logback 在類的設計上非常值得學習, 使得許多程式碼邏輯也非常簡單易懂。

列印日誌到控制檯

這裡以Logger.debug(String)為例,如下。這裡需要注意 TurboFilter 和 Filter 的區別,前者是全域性的,每次發起日誌記錄請求都會被呼叫,且在日誌事件建立前呼叫,而後者是附加的,作用範圍較小。因為實際專案中 TurboFilter 使用較少,這裡不做擴充套件,感興趣可參考這裡。

    public static final String FQCN = ch.qos.logback.classic.Logger.class.getName();
    public void debug(String msg) {
        filterAndLog_0_Or3Plus(FQCN, null, Level.DEBUG, msg, null, null);
    }
    private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                    final Throwable t) {
        // 使用TurboFilter過濾當前日誌,判斷是否通過
        final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
        //  返回NEUTRAL表示沒有TurboFilter,即無需過濾
        if (decision == FilterReply.NEUTRAL) {
            // 如果需要列印日誌的等級小於有效日誌等級,則直接返回
            if (effectiveLevelInt > level.levelInt) {
                return;
            }
        } else if (decision == FilterReply.DENY) {
            // 如果不通過,則不列印日誌,直接返回
            return;
        }
        // 建立LoggingEvent
        buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
    }

進入Logger.buildLoggingEventAndAppend(String, Marker, Level, String, Object[], Throwable),如下。 logback 中,日誌記錄請求會被構造成日誌事件 LoggingEvent,傳遞給對應的 appender 處理。

    private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                    final Throwable t) {
        // 構造日誌事件LoggingEvent
        LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
        // 設定標記
        le.setMarker(marker);
        // 通知LoggingEvent給當前logger持有的和繼承的appender
        callAppenders(le);
    }

進入到Logger.callAppenders(ILoggingEvent),如下。

    public void callAppenders(ILoggingEvent event) {
        int writes = 0;
        // 通知LoggingEvent給當前logger的持有的和繼承的appender處理日誌事件
        for (Logger l = this; l != null; l = l.parent) {
            writes += l.appendLoopOnAppenders(event);
            // 如果設定了logger的additivity=false,則不會繼續查詢父級的appender
            // 如果沒有設定,則會一直查詢到root logger
            if (!l.additive) {
                break;
            }
        }
        // 當前logger未設定appender,在控制檯列印提醒
        if (writes == 0) {
            loggerContext.noAppenderDefinedWarning(this);
        }
    }
    private int appendLoopOnAppenders(ILoggingEvent event) {
        if (aai != null) {
            // 呼叫AppenderAttachableImpl的方法處理日誌事件
            return aai.appendLoopOnAppenders(event);
        } else {
            // 如果當前logger沒有appender,會返回0
            return 0;
        }
    }

在繼續分析前,先看下 Appender 的 UML 圖(注意,Appender 還有很多實現類,這裡只列出了常用的幾種)。Appender 持有 Filter 和 Encoder 到引用,可以分別對日誌進行過濾和格式轉換。

本文僅涉及到 ConsoleAppender 的原始碼分析。

繼續進入到AppenderAttachableImpl.appendLoopOnAppenders(E),如下。這裡會遍歷當前 logger 持有的 appender,並呼叫它們的 doAppend 方法。

    public int appendLoopOnAppenders(E e) {
        int size = 0;
        // 獲得當前logger的所有appender
        final Appender<E>[] appenderArray = appenderList.asTypedArray();
        final int len = appenderArray.length;
        for (int i = 0; i < len; i++) {
            // 呼叫appender的方法
            appenderArray[i].doAppend(e);
            size++;
        }
        // 這個size為appender的數量
        return size;
    }

為了簡化分析,本文僅分析列印日誌到控制檯的過程,所以進入到UnsynchronizedAppenderBase.doAppend(E)