源碼詳解系列(七) ------ 全面講解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>

測試

運行測試方法,我們可以在指定目錄看到生成的日志文件。

file_appender_01

查看日志文件,可以看到只打印了 error 等級的日志:

file_appender_02

將日志輸出到數據庫

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 找到。

logback日志表腳本

由於本文使用的是 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的三張日志表

配置文件

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>

測試

運行測試方法,可以看到數據庫中插入了以下數據:

logback日志表數據

使用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_jmx_01

我們可以看到,在屬性中,我們可以查看 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 格式文件的配置方式展開。

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

-Dlogging.config=classpath:logback-sit.xml

如果沒有加載到配置,logback 會調用 BasicConfigurator 進行默認的配置。

configuration

configuration 是 logback.xml 或 logback-test.xml 文件的根節點。

logback_configuration_01

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_configuration_debug.png

如上圖,通過控制台我們可以查看 logback 加載配置的過程,這時,我們嘗試修改 logback 配置文件的內容:

logback_configuration_scan.png

觀察控制台,可以看到配置文件重新加載:

logback_configuration_scan2.png

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 的繼承關系,如下圖:

logback_logger_01

如果我們未指定當前 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 為文件輸出。運行測試方法:

logback_logger_02

可以看到,名為 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 的日志:

logback_LevelFilter

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 的日志:

logback_ThresholdFilter

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>

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

logback_EvaluatorFilter

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_EvaluatorFilter_02

源碼分析

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 類與具體日志實現進行關聯,從而實現門面模式

logback_StaticLoggerBinder_01

接下來再簡單看下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 加載配置的大致圖解:

logback_joran

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

類名 描述
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。

logback_LoggerContext_UML

獲取logger對象

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

logback_Logger_UML

進入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 的源碼分析。

logback_Appender_UML

繼續進入到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)方法,如下。

    public void doAppend(E eventObject) {
        // 避免doAppend方法被重復調用??
        // TODO 這一步不是很理解,同一個線程還能同時調用兩次這個方法?
        if (Boolean.TRUE.equals(guard.get())) {
            return;
        }

        try {
            guard.set(Boolean.TRUE);

            // 過濾當前日志事件是否允許打印
            if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
                return;
            }

            // 調用實現類的方法
            this.append(eventObject);

        } catch (Exception e) {
            if (exceptionCount++ < ALLOWED_REPEATS) {
                addError("Appender [" + name + "] failed to append.", e);
            }
        } finally {
            guard.set(Boolean.FALSE);
        }
    }

進入到OutputStreamAppender.append(E),如下。

    protected void append(E eventObject) {
        // 如果appender未啟動,則直接返回,不處理日志事件
        if (!isStarted()) {
            return;
        }
        subAppend(eventObject);
    }
    protected void subAppend(E event) {
        // 這里又判斷一次??
        if (!isStarted()) {
            return;
        }
        try {
            // 這一步不是很懂 TODO
            if (event instanceof DeferredProcessingAware) {
                ((DeferredProcessingAware) event).prepareForDeferredProcessing();
            }
            
            // 調用encoder的方法將日志事件轉化為字節數組
            byte[] byteArray = this.encoder.encode(event);
            // 打印日志
            writeBytes(byteArray);

        } catch (IOException ioe) {
            this.started = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
        }
    }

看下LayoutWrappingEncoder.encode(E),如下。

    public byte[] encode(E event) {
        // 根據配置格式處理日志事件
        String txt = layout.doLayout(event);
        // 將字符轉化為字節數組並返回
        return convertToBytes(txt);
    }

后面會調用PatternLayout.doLayout(ILoggingEvent)將日志的消息進行處理,這部分內容我就不繼續擴展了,感興趣可以自行研究。

以上是 logback 的源碼基本分析完成,后續有空再作補充。

參考資料

logback中文手冊

相關源碼請移步:https://github.com/ZhangZiSheng001/logback-demo

本文為原創文章,轉載請附上原文出處鏈接: https://www.cnblogs.com/ZhangZiSheng001/p/12246122.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM