原始碼詳解系列(七) ------ 全面講解logback的使用和原始碼
什麼是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 格式檔案的配置方式展開。
另外,如果想要自定義配置檔案的名字,可以通過系統屬性指定:
-Dlogback.configurationFile=/path/to/config.xml
如果沒有載入到配置,logback 會呼叫 BasicConfigurator 進行預設的配置。
configuration
configuration 是 logback.xml 或 logback-test.xml 檔案的根節點。
configuration 主要用於配置某些全域性的日誌行為,常見的配置引數如下:
屬性名 | 描述 |
---|---|
debug | 是否列印 logback 的內部狀態,開啟有利於排查 logback 的異常。預設 false |
scan | 是否在執行時掃描配置檔案是否更新,如果更新時則重新解析並更新配置。如果更改後的配置檔案有語法錯誤,則會回退到之前的配置檔案。預設 false |
scanPeriod | 多久掃描一次配置檔案是否修改,單位可以是毫秒、秒、分鐘或者小時。預設情況下,一分鐘掃描一次配置檔案。 |
配置方式如下:
<configuration debug="true" scan="true" scanPeriod="60 seconds" >
<!-- 控制檯輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
使用以上配置進行測試:
如上圖,通過控制檯我們可以檢視 logback 載入配置的過程,這時,我們嘗試修改 logback 配置檔案的內容:
觀察控制檯,可以看到配置檔案重新載入:
logger
前面提到過,logger 是為我們列印日誌的物件,這個概念非常重要,有助於更好地理解 logger 的繼承關係。
在以下程式碼中,我們可以在getLogger
方法中傳入的是當前類的 Class 物件或全限定類名,本質上獲取到的都是一個 logger 物件(如果該 logger 不存在,才會建立)。
@Test
public void test01() {
Logger logger1 = LoggerFactory.getLogger(LogbackTest.class);
Logger logger2 = LoggerFactory.getLogger("cn.zzs.logback.LogbackTest");
System.err.println(logger == logger2);// true
}
這裡補充一個問題,該 logger 物件以 cn.zzs.logback.LogbackTest 命名,和我們配置檔案中定義的 root logger 並不是同一個,但是為什麼這個 logger 物件卻擁有 root logger 的行為?
這要得益於 logger 的繼承關係,如下圖:
如果我們未指定當前 logger 的日誌等級,logback 會將其日誌等級設定為最近父級的日誌等級。另外,預設情況下,當前 logger 也會繼承最近父級持有的 appender。
下面測試下以上特性,將配置檔案進行如下修改:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds" debug="true">
<!-- 定義變數 -->
<property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
<property scope="system" name="APP_NAME" value="logback-demo"/>
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制檯輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" />
<!-- 檔案輸出 -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<append>true</append>
<file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file>
<immediateFlush>true</immediateFlush>
<!-- 是否啟用安全寫入 -->
<prudent>false</prudent>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<logger name="cn.zzs" level="error">
<appender-ref ref="FILE" />
</logger>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
這裡自定義了一個 logger,日誌等級是 error,appender 為檔案輸出。執行測試方法:
可以看到,名為 cn.zzs.logback.LogbackTest 的 logger 繼承了名為 cn.zzs 的 logger 的日誌等級和 appender,以及繼承了 root logger 的 appender。
實際專案中,如果不希望繼承父級的 appender,可以配置 additivity="false" ,如下:
<logger name="cn.zzs" additivity="false">
<appender-ref ref="FILE" />
</logger>
注意,因為以下配置都是建立在 logger 的繼承關係上,所以這部分內容必須很好地理解。
appender
appender 用於定義日誌的輸出目的地和輸出格式,被 logger 所持有。logback 為我們提供了以下幾種常用的appender:
類名 | 描述 |
---|---|
ConsoleAppender | 將日誌通過 System.out 或者 System.err 來進行輸出,即輸出到控制檯。 |
FileAppender | 將日誌輸出到檔案中。 |
RollingFileAppender | 繼承自 FileAppender,也是將日誌輸出到檔案,但檔案具有輪轉功能。 |
DBAppender | 將日誌輸出到資料庫 |
SocketAppender | 將日誌以明文方式輸出到遠端機器 |
SSLSocketAppender | 將日誌以加密方式輸出到遠端機器 |
SMTPAppender | 將日誌輸出到郵件 |
本文僅會講解前四種,後四種可參考官方文件。
ConsoleAppender
ConsoleAppender 支援將日誌通過 System.out 或者 System.err 輸出,即輸出到控制檯,常用屬性如下:
屬性名 | 型別 | 描述 |
---|---|---|
encoder | Encoder | 後面單獨講 |
target | String | System.out 或 System.err。預設為 System.out |
immediateFlush | boolean | 是否立即重新整理。預設為 true。 |
withJansi | boolean | 是否啟用 Jansi 在 windows 使用 ANSI 彩色程式碼,預設值為 false。 在windows電腦上我嘗試開啟這個屬性並引入 jansi 包,但老是報錯,暫時沒有解決方案。 |
具體配置如下:
<!-- 控制檯輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
FileAppender
FileAppender 支援將日誌輸出到檔案中,常用屬性如下:
屬性名 | 型別 | 描述 |
---|---|---|
append | boolean | 是否追加寫入。預設為 true |
encoder | Encoder | 後面單獨講 |
immediateFlush | boolean | 是否立即重新整理。預設為 true。 |
file | String | 要寫入檔案的路徑。如果檔案不存在,則新建。 |
prudent | boolean | 是否採用安全方式寫入,即使在不同的 JVM 或者不同的主機上執行 FileAppender 例項。預設的值為 false。 |
具體配置如下:
<!-- 定義變數 -->
<property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
<property scope="system" name="APP_NAME" value="logback-demo"/>
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" />
<!-- 檔案輸出 -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
RollingFileAppender
RollingFileAppender 繼承自 FileAppender,也是將日誌輸出到檔案,但檔案具有輪轉功能。
RollingFileAppender 的屬性如下所示:
屬性名 | 型別 | 描述 |
---|---|---|
file | String | 要寫入檔案的路徑。如果檔案不存在,則新建。 |
append | boolean | 是否追加寫入。預設為 true。 |
immediateFlush | boolean | 是否立即重新整理。預設為true。 |
encoder | Encoder | 後面單獨將 |
rollingPolicy | RollingPolicy | 定義檔案如何輪轉。 |
triggeringPolicy | TriggeringPolicy | 定義什麼時候發生輪轉行為。如果 rollingPolicy 使用的類已經實現了 triggeringPolicy 介面,則不需要再配置 triggeringPolicy,例如 SizeAndTimeBasedRollingPolicy。 |
prudent | boolean | 是否採用安全方式寫入,即使在不同的 JVM 或者不同的主機上執行 FileAppender 例項。預設的值為 false。 |
具體配置如下:
<!-- 定義變數 -->
<property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
<property scope="system" name="APP_NAME" value="logback-demo"/>
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 輪轉檔案輸出 -->
<appender name="FILE-ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 輪轉策略,它根據時間和檔案大小來制定輪轉策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 按天輪轉 -->
<fileNamePattern>${LOG_HOME}/${APP_NAME}/log-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!-- 儲存 30 天的歷史記錄,最大大小為 30GB -->
<MaxHistory>30</MaxHistory>
<totalSizeCap>30GB</totalSizeCap>
<!-- 當日志文件超過100MB的大小時,根據上面提到的%i進行日誌檔案輪轉 -->
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<!-- 日誌輸出格式-->
<encoder charset="utf-8">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
DBAppender
參見使用例子。
encoder
encoder 負責將日誌事件按照配置的格式轉換為位元組陣列,常用屬性如下:
屬性名 | 型別 | 描述 |
---|---|---|
pattern | String | 日誌列印格式。 |
outputPatternAsHeader | boolean | 是否將 pattern 字串插入到日誌檔案頂部。預設false。 |
針對 pattern 屬性,這裡補充下它的常用轉換字元:
轉換字元 | 描述 |
---|---|
c{length} lo{length} logger{length} |
輸出 logger 的名字。可以通過 length 縮短其長度。 但是,logger 名字最右邊永遠都會存在。 例如,當我們設定 logger{0}時,cn.zzs.logback.LogbackTest 中的 LogbackTest 永遠不會被刪除 |
C{length} class{length} |
輸出發出日誌請求的類的全限定名稱。 可以通過 length 縮短其長度。 |
d{pattern} date{pattern} d{pattern, timezone} date{pattern, timezone} |
輸出日誌事件的日期。 可以通過 pattern 設定日期格式,timezone 設定時區。 |
m / msg / message | 輸出與日誌事件相關聯的,由應用程式提供的日誌資訊。 |
M / method | 輸出發出日誌請求的方法名。 |
p / le / level | 輸出日誌事件的級別。 |
t / thread | 輸出生成日誌事件的執行緒名。 |
n | 輸出平臺所依賴的行分割字元。 |
F / file | 輸出發出日誌請求的 Java 原始檔名。 |
caller{depth} caller{depthStart..depthEnd} caller{depth, evaluator-1, ... evaluator-n} caller{depthStart..depthEnd, evaluator-1, ... evaluator-n} |
輸出生成日誌的呼叫者所在的位置資訊。 |
L / line | 輸出發出日誌請求所在的行號。 |
property{key} | 輸出屬性 key 所對應的值。 |
注意,在拼接 pattren 時,應該考慮使用“有意義的”轉換字元,避免產生不必要的效能開銷。具體配置如下:
<!-- 控制檯輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="utf-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<outputPatternAsHeader>true</outputPatternAsHeader>
</encoder>
</appender>
其中, 轉換說明符 %-5level 表示日誌事件的級別的字元應該向左對齊,保持五個字元的寬度。
filter
appender 除了定義日誌的輸出目的地和輸出格式,其實也可以對日誌事件進行過濾輸出,例如,僅輸出包含指定字元的日誌。而這個功能需配置 filter。
LevelFilter
LevelFilter 基於級別來過濾日誌事件。修改配置檔案如下:
<configuration scan="true" scanPeriod="10 seconds" debug="true">
<!-- 定義變數 -->
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制檯輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<!-- 設定過濾器 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
執行測試方法,可見,雖然 root logger 的日誌等級是 info,但最終只會列印 error 的日誌:
ThresholdFilter
ThresholdFilter 基於給定的臨界值來過濾事件。如果事件的級別等於或高於給定的臨界,則過濾通過,否則會被攔截。配置如下:
<configuration scan="true" scanPeriod="10 seconds" debug="true">
<!-- 定義變數 -->
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制檯輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<!-- 設定過濾器 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
執行測試方法,可見,雖然 root logger 的日誌等級是 info,但最終只會列印 error 的日誌:
EvaluatorFilter
EvaluatorFilter 基於給定的標準來過濾事件。 它採用 Groovy 表示式作為評估的標準。配置如下:
<configuration scan="true" scanPeriod="10 seconds" debug="true">
<!-- 定義變數 -->
<property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制檯輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<target>system.err</target>
<encoder charset="utf-8">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<!-- 設定過濾器 -->
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator">
<expression>
e.level.toInt() >= ERROR.toInt() &&
!(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)