什么是logback
logback 用於日志記錄,可以將日志輸出到控制台、文件、數據庫和郵件等,相比其它所有的日志系統,logback 更快並且更小,包含了許多獨特並且有用的特性。
logback 被分成三個不同的模塊:logback-core,logback-classic,logback-access。
- logback-core 是其它兩個模塊的基礎。
- logback-classic 模塊可以看作是 log4j 的一個優化版本,它天然的支持 SLF4J。
- logback-access 提供了 http 訪問日志的功能,可以與 Servlet 容器進行整合,例如:Tomcat、Jetty。
本文將介紹以下內容,由於篇幅較長,可根據需要選擇閱讀:
-
如何使用 logback:將日志輸出到控制台、文件和數據庫,以及使用 JMX 配置 logback;
-
logback 配置文件詳解;
-
logback 的源碼分析。
如何使用logback
需求
- 使用 logback 將日志信息分別輸出到控制台、文件、數據庫。
- 使用 JMX 方式配置 logback。
工程環境
JDK:1.8.0_231
maven:3.6.1
IDE:Spring Tool Suite 4.3.2.RELEASE
mysql:5.7.28
主要步驟
- 搭建環境;
- 配置 logback 文件;
- 編寫代碼:獲取
Logger
實例,並打印指定等級的日志; - 測試。
創建項目
項目類型 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
對象。
以下代碼中,導入的兩個類 Logger
、 LoggerFactory
都定義在 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&characterEncoding=utf8&serverTimezone=GMT%2B8&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 格式文件的配置方式展開。
另外,如果想要自定義配置文件的名字,可以通過系統屬性指定:
-Dlogging.config=classpath:logback-sit.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() &&
!(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 的源碼:
- slf4j 是如何實現門面模式的?
- logback 如何加載配置?
- 獲取我們所需的 logger?
- 如何將日志打印到控制台?
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
。代碼比較多,我概括下執行的步驟:
- 如果 StaticLoggerBinder 存在且唯一,修改初始化狀態為 SUCCESSFUL_INITIALIZATION;
- 如果 StaticLoggerBinder 存在但為多個,由 JVM 決定綁定哪個 StaticLoggerBinder,修改初始化狀態為 SUCCESSFUL_INITIALIZATION,同時,會在控制台打印存在哪幾個 StaticLoggerBinder,並提醒用戶最終選擇了哪一個 ;
- 如果 StaticLoggerBinder 不存在,打印提醒,並修改初始化狀態為 NOP_FALLBACK_INITIALIZATION;
- 如果 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 | 用於存放模式匹配器-動作的鍵值對。 |
結合上圖,我簡單概括下整個執行過程:
- 使用 SAX 方式解析 XML,解析過程中根據當前的元素類型,調用 DefaultHandler 實現類的方法,構造 SaxEvent 並將其放入集合 saxEventList 中;
- 當 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)
方法,如下。這個方法邏輯簡單,但是設計非常巧妙,可以好好琢磨下。我概括下主要的步驟:
- 如果獲取的是 root logger,直接返回;
- 如果獲取的是 loggerCache 中緩存的 logger,直接返回;
- 循環獲取 logger name 中包含的所有 logger,如果不存在就創建並放入緩存;
- 返回 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)
方法,如下。
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 的源碼基本分析完成,后續有空再作補充。
參考資料
本文為原創文章,轉載請附上原文出處鏈接: https://www.cnblogs.com/ZhangZiSheng001/p/12246122.html