作者 | 書瀾
來源 | 凌雲時刻(微信號:linuxpk)
前言
日誌的有無雖然不影響應用程序的運行結果,但是沒有日誌的應用程序是不完整的,甚至可以說是有缺陷的。優雅的日誌系統可以記錄操作軌跡,監控系統運行狀況以及回溯系統故障。在工作中,部分工程師對主流的日誌框架仍然是一知半解,日常應用還停留在複製粘貼的層面,因此寫作本文,希望對讀者有所幫助。
本系列文章分為上、中、下三篇,將全面系統地介紹 Java 日誌框架,主要內容有:
- 日誌的意義與價值
- Java 日誌框架進化史
- 日誌門面與日誌系統
- 日誌框架的使用選擇
- 日誌使用中需要遵循的規範及注意事項
- 日誌使用示例及常見報錯
本篇為上篇,將詳細解讀主流 Java 日誌框架。
日誌的意義與價值
- 在開發調試階段: 日誌系統有助於更快的定位問題。
- 在應用運維階段: 日誌系統有助於記錄大部分的異常信息,通過收集日誌信息可以對系統的運行狀態進行實時監控預警。
- 在數據分析階段: 日誌中通常包含大量的用戶數據,包括點擊行為、興趣偏好等,基於這些數據可以對用戶進行“畫像”,進而助力戰略決策。隨著大數據技術日漸成熟,海量日誌分析已經在互聯網公司得到廣泛應用。
Java 日誌框架進化史
在開發過程中,工程師不得不面對一個很現實的問題:Java “混亂”的日誌框架體系。為什麼說“混亂”呢?原因在於早期 Java 日誌框架沒有制定統一的標準,使得很多應用程序會同時使用多種日誌框架。Java 日誌框架的發展歷程大致可分為以下幾個階段:
Log4j
Apache Log4j 是一種基於 Java 的日誌記錄工具,它是 Apache 軟件基金會的一個項目。在 jdk1.3 之前,還沒有現成的日誌框架,Java 工程師只能使用原始的 System.out.println(), System.err.println() 或者 e.printStackTrace()。通過把 debug 日誌寫到 StdOut 流,錯誤日誌寫到 ErrOut 流,以此記錄應用程序的運行狀態。這種原始的日誌記錄方式缺陷明顯,不僅無法實現定製化,而且日誌的輸出粒度不夠細。鑑於此,1999 年,大牛 Ceki Gülcü 創建了 Log4j 項目,並幾乎成為了 Java 日誌框架的實際標準。
JUL
Log4j 作為 Apache 基金會的一員,Apache 希望將 Log4j 引入 jdk,不過被 sun 公司拒絕了。隨後,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。
Commons Logging
為了解耦日誌接口與實現,2002 年 Apache 推出了 JCL(Jakarta Commons Logging),也就是 Commons Logging。Commons Logging 定義了一套日誌接口,具體實現則由 Log4j 或 JUL 來完成。Commons Logging 基於動態綁定來實現日誌的記錄,在使用時只需要用它定義的接口編碼即可,程序運行時會使用 ClassLoader 尋找和載入底層的日誌庫,因此可以自由選擇由 log4j 或 JUL 來實現日誌功能。
Slf4j&Logback
大牛 Ceki Gülcü 與 Apache 基金會關於 Commons-Logging 制定的標準存在分歧,後來,Ceki Gülcü 離開 Apache 並先後創建了 Slf4j 和 Logback 兩個項目。Slf4j 是一個日誌門面,只提供接口,可以支持 Logback、JUL、log4j 等日誌實現,Logback 提供具體的實現,它相較於 log4j 有更快的執行速度和更完善的功能。
Log4j 2
為了維護在 Java 日誌江湖的地位,防止 JCL、Log4j 被 Slf4j、Logback 組合取代 ,2014 年 Apache 推出了 Log4j 2。Log4j 2 與 log4j 不兼容,經過大量深度優化,其性能顯著提升。
日誌門面與日誌系統
在上文中已經提及,目前常用的日誌框架有 Log4j,Log4j 2,Commons Logging,Slf4j,Logback,JUL。這些日誌框架可以分為兩種類型:門面日誌和日誌系統。
- 日誌門面:只提供日誌相關的接口定義,即相應的 API,而不提供具體的接口實現。日誌門面在使用時,可以動態或者靜態地指定具體的日誌框架實現,解除了接口和實現的耦合,使用戶可以靈活地選擇日誌的具體實現框架。
- 日誌系統:只提供日誌相關的接口定義,即相應的 API,而不提供具體的接口實現。日誌門面在使用時,可以動態或者靜態地指定具體的日誌框架實現,解除了接口和實現的耦合,使用戶可以靈活地選擇日誌的具體實現框架。
如上圖所示,Commons-Logging 和 Slf4j 屬於日誌門面框架,Log4j、Logback、和 JUL 則屬於具體的日誌系統框架。閱讀至此,想必讀者一定疑惑——為何如此設計?為何不簡單一點?為何分成了門面和實現?
在回答上述問題之前,我們先一起簡單回顧一下門面模式(軟件設計模式的一種,也稱外觀模式、正面模式)。門面模式的核心為:外部客戶端與一個子系統的通信,必須通過一個統一的外觀對象進行,使得子系統更易於使用,其本質就是為子系統中的一組接口提供一個統一的高層接口,如下圖所示:
門面模式的核心是門面對象 Facade,它有如下幾個特點:
- 知道所有子模塊的責任和功能
- 將客戶端發來的請求委派到子系統中,本身沒有具體業務邏輯
- 不參與子系統內業務邏輯的實現
瞭解過門面模式的基本信息,再回到最初的問題——為什麼日誌框架要使用門面模式呢?其實答案很簡單,在工程開發中常遇到這樣的場景:
- 1.我們自己的系統中使用了 Logback 這個日誌系統
- 2.我們的系統使用了 A.jar,A.jar 中使用的日誌系統為 Log4j
- 3.我們的系統又使用了 B.jar,而 B.jar 中使用的日誌系統為 JUL
在上述場景中,我們的系統需要同時支持並維護 Logback、Log4j、JUL 三種日誌框架,其繁瑣程度不言而喻。為了解決這個問題,可以引入一個適配層,由適配層決定具體使用哪一種日誌系統,應用程序中的調用者只管打印日誌,而不必關心日誌是如何被打印出來的,如此,問題迎刃而解。顯然,Slf4j 和 Commons-Logging 就是這種適配層,而 JUL、Log4j 和 Logback 等就是打印日誌的具體實現。換言之,日誌門面(適配層)只需要提供日誌的接口,日誌系統的具體實現則交由其它日誌框架,這樣就避免了需要維護複雜日誌系統的問題。
避免環形依賴
Slf4j 的作者 Ceki Gülcü 當年因為覺得 Commons-Logging 的 API 設計的不好,性能也不夠高,因而設計了 Slf4j。而他為了 Slf4j 能夠兼容各種類型的日誌系統實現,還設計了相當多的 adapter 和 bridge 來連接,如下圖所示:
這些 adapter 和 bridge 在此就不做詳細介紹,讀者需要時可自行查閱上圖找到對應的 jar 包。這裡只想引出一個由此產生的問題,那就是日誌框架的循環依賴問題。具體而言,如果在應用中使用 Slf4j 作為日誌門面,就需要引入 slf4j-api-xx.jar,如果同時又引入了 slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar 這幾個包,在這種情況下,調用 slf4j-api 就會出現死循環(如下圖所示)。
鑑於此,在引入日誌框架依賴的時候要盡力避免,比如以下組合就不能同時出現:
- 1.jcl-over-slf4j 和 slf4j-jcl
- 2.log4j-over-slf4j 和 slf4j-log4j12
- 3.jul-to-slf4j 和 slf4j-jdk14
日誌框架的使用選擇
###日誌框架之間的關係###
在介紹日誌框架的使用之前,簡要回顧一下前面四節的內容。Commons Logging 和 Slf4j 是日誌門面。Log4j 和 Logback 則是具體的日誌實現方案。可以簡單的理解為接口與接口的實現,調用者只需要關注接口而無需關注具體的實現,從而做到解耦。在整個日誌框架中主要包括日誌門面、日誌適配器、日誌庫三個部分,它們之間的關係如下圖所示:
###日誌框架的使用選擇 ###
比較常用的組合使用方式是 Slf4j 與 Logback 組合使用,Commons Logging 與 Log4j 組合使用,Logback 必須配合 Slf4j 使用。由於 Logback 和 Slf4j 是同一個作者,其兼容性不言而喻。這裡順便介紹一個小故事:Apache 曾試圖說服 Log4j 以及其它的日誌來按照 Commons-Logging 的標準編寫,但是由於 Commons-Logging 的類加載機制在實際應用中存在問題(它使用 ClassLoader 尋找和載入底層的日誌庫),實現起來也不友好,因此 Log4j 的作者便開發了 Slf4j,與 Commons-Logging 兩分天下。
關於如何選擇日誌框架,如果是新的項目 (沒有歷史包袱,無需切換日誌框架),建議使用 Slf4j 與 Logback 組合,這樣有如下的幾個優點:
1.Slf4j 實現機制決定 Slf4j 限制較少,使用範圍更廣。相較於 Commons-Logging,Slf4j 在編譯期間便靜態綁定本地的 Log 庫,其通用性要好得多。
2.Logback 擁有更好的性能。Logback 聲稱:某些關鍵操作,比如判定是否記錄一條日誌語句的操作,其性能得到了顯著的提高,這個操作在 Logback 中只需 3 納秒,而在 Log4j 則需要 30 納秒。
3.Slf4j 支持參數化,使用佔位符號,代碼更為簡潔,如下例子。
4.Logback 的所有文檔是免費提供的,Log4j 只提供部分免費文檔而需要用戶去購買付費文檔。
5.MDC (Mapped Diagnostic Contexts) 用 Filter,將當前用戶名等業務信息放入MDC 中,在日誌 format 定義中即可使用該變量。具體而言,在診斷問題時,通常需要打出日誌。如果使用 Log4j,則只能降低日誌級別,但是這樣會打出大量的日誌,影響應用性能;如果使用 Logback,保持原定日誌級別而過濾某種特殊情況,如 Alice 這個用戶登錄,日誌將打在 DEBUG 級別而其它用戶可以繼續打在 WARN 級別。實現這個功能只需加 4 行 XML 配置。
6.自動壓縮日誌。RollingFileAppender 在產生新文件的時候,會自動壓縮已經打出來的日誌文件。壓縮過程是異步的,因此在壓縮過程中應用幾乎不會受影響。
舉例說明:如果直接使用 Slf4j 和 Logback 組合,可通過如下配置進行集成:
對於已有工程,需要根據所使用的日誌庫來確定門面適配器從而使用 Slf4j。Slf4j 的設計思想比較簡潔,使用了 Facade 設計模式,Slf4j 本身只提供了一個 slf4j-api-version.jar 包,這個 jar 中主要是日誌的抽象接口,jar 包中本身並沒有對抽象出來的接口做實現。對於不同的日誌實現方案(例如 Logback,Log4j 等),封裝出不同的橋接組件(例如 logback-classic-version.jar,slf4j-log4j12-version.jar),這樣使用過程中可以靈活地選取自己項目裡的日誌實現。
舉例說明,如果已有工程中使用了 Log4j 日誌庫,可通過如下配置進行集成:
下面是 Slf4j 與其它日誌組件調用關係圖:
具體的接入方式參見下圖:
如果老代碼中直接使用非 Slf4j 日誌庫提供的接口打印日誌,需要引入日誌庫適配器來橋接遺留的 api。在實際環境中我們經常會遇到不同的組件使用的日誌框架不同的情況,例如 Spring Framework 使用的是日誌組件是 Commons Logging,XSocket 依賴的則是 Java Util Logging。
如果在同一項目中使用不同的組件時,如何解決不同組件依賴的日誌組件不一致的情況呢?這就需要統一日誌方案,統一使用 Slf4j,把他們的日誌輸出重定向到 Slf4j,然後 Slf4j 又會根據綁定器把日誌交給具體的日誌實現工具。Slf4j 帶有幾個橋接模塊,可以重定向 Log4j,JCL 和 java.util.logging 中的 Api 到 Slf4j。
舉例說明:如果老代碼中直接使用了 Log4j 日誌庫接口打印日誌,需引入如下配置:
橋接方式參加下圖:
###排除項目中依賴的第三方包的日誌依賴###
在實際使用過程中,項目會根據需要引入一些第三方組件,例如常用的 Spring,而 Spring 本身的日誌實現使用了 Commons Logging,如果想使用 Slf4j+Logback 組合,這時候需要在項目中將 Commons Logging 排除掉,通常會用到以下 3 種方案,各有利弊,可以根據項目的實際情況選擇最適合自己項目的解決方案。
- 方案一:採用 maven 的 exclusion 方案
這種方案優點是 exclusion 是 maven 原生提供的,不足之處是如果有多個組件都依賴了 commons-logging,則需要在很多處增加 exclusion,比較繁瑣。
- 方案二:在 maven 聲明 commons-logging 的 scope 為 provided
這種方案雖然簡潔,但也有缺點,在調試代碼時有可能導致 IDE 將 commons-logging 放置在 classpath 下,從而導致程序運行時出現異常。
- 方案三:在 maven 私服中增加類似於 99.0-does-not-exist 這種虛擬的版本號
這種方案好處在於聲明方式比較簡單,用 IDE 調試代碼時也不會出現問題,不足之處是 99.0-does-not-exist 這種版本是 maven 中央倉庫中可能不存在,需要發佈到自己的 maven 私服中。