logback 用於日誌記錄,能夠將日誌輸出到控制檯、文件、數據庫和郵件等,相比其它全部的日誌系統,logback 更快而且更小,包含了許多獨特而且有用的特性。html
logback 被分紅三個不一樣的模塊:logback-core,logback-classic,logback-access。java
本文將介紹如下內容,因爲篇幅較長,可根據須要選擇閱讀:mysql
如何使用 logback:將日誌輸出到控制檯、文件和數據庫,以及使用 JMX 配置 logback;git
logback 配置文件詳解;github
logback 的源碼分析。sql
JDK:1.8.0_231
maven:3.6.1
IDE:Spring Tool Suite 4.3.2.RELEASE
mysql:5.7.28數據庫
Logger
實例,並打印指定等級的日誌;項目類型 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
對象。
如下代碼中,導入的兩個類 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>
運行測試方法,能夠看到數據庫中插入瞭如下數據:
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 的內部狀態,經過操做,咱們能夠:
更多 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 是 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 的繼承關係。
在如下代碼中,咱們能夠在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 用於定義日誌的輸出目的地和輸出格式,被 logger 所持有。logback 爲咱們提供瞭如下幾種經常使用的appender:
類名 | 描述 |
---|---|
ConsoleAppender | 將日誌經過 System.out 或者 System.err 來進行輸出,即輸出到控制檯。 |
FileAppender | 將日誌輸出到文件中。 |
RollingFileAppender | 繼承自 FileAppender,也是將日誌輸出到文件,但文件具備輪轉功能。 |
DBAppender | 將日誌輸出到數據庫 |
SocketAppender | 將日誌以明文方式輸出到遠程機器 |
SSLSocketAppender | 將日誌以加密方式輸出到遠程機器 |
SMTPAppender | 將日誌輸出到郵件 |
本文僅會講解前四種,後四種可參考官方文檔。
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 支持將日誌輸出到文件中,經常使用屬性以下:
屬性名 | 類型 | 描述 |
---|---|---|
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 繼承自 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>
參見使用例子。
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 表示日誌事件的級別的字符應該向左對齊,保持五個字符的寬度。
appender 除了定義日誌的輸出目的地和輸出格式,其實也能夠對日誌事件進行過濾輸出,例如,僅輸出包含指定字符的日誌。而這個功能需配置 filter。
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 基於給定的臨界值來過濾事件。若是事件的級別等於或高於給定的臨界,則過濾經過,不然會被攔截。配置以下:
<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 基於給定的標準來過濾事件。 它採用 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 使用的是門面模式,無論使用什麼日誌實現,項目代碼都只會用到 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
。代碼比較多,我歸納下執行的步驟:
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 支持採用 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 | 用於存放模式匹配器-動做的鍵值對。 |
結合上圖,我簡單歸納下整個執行過程:
如今回到 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 的 UML 圖,以下。在 Logger 對象中,持有了父級 logger、子級 logger 和 appender 的引用。
進入LoggerContext.getLogger(String)
方法,以下。這個方法邏輯簡單,可是設計很是巧妙,能夠好好琢磨下。我歸納下主要的步驟:
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://github.com/ZhangZiSheng001/logback-demo
本文爲原創文章,轉載請附上原文出處連接:https://www.cnblogs.com/ZhangZiSheng001/p/12246122.html