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

什麼是logback

logback 用於日誌記錄,能夠將日誌輸出到控制檯、文件、數據庫和郵件等,相比其它全部的日誌系統,logback 更快而且更小,包含了許多獨特而且有用的特性。html

logback 被分紅三個不一樣的模塊:logback-core,logback-classic,logback-access。java

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

本文將介紹如下內容,因爲篇幅較長,可根據須要選擇閱讀:mysql

  1. 如何使用 logback:將日誌輸出到控制檯、文件和數據庫,以及使用 JMX 配置 logback;git

  2. logback 配置文件詳解;github

  3. logback 的源碼分析。sql

如何使用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。express

引入依賴

logack 自然的支持 slf4j,不須要像其餘日誌框架同樣引入適配層(如 log4j 需引入 slf4j-log4j12 )。經過後面的源碼分析可知,logback 只是將適配相關代碼放入了 logback-classic。windows

<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 機制進行配置,本文暫不涉及)。api

在 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 格式文件的配置方式展開。

另外,若是想要自定義配置文件的名字,能夠經過系統屬性指定:

-Dlogback.configurationFile=/path/to/config.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

相關文章
相關標籤/搜索