Java日誌體系居然這麼複雜?——架構篇
本文是一個系列,歡迎關注
日誌到底是何方神聖?為什麼要使用日誌框架?
想必大家都有過使用System.out
來進行輸出除錯,開發開發環境下這樣做當然很方便,但是線上這樣做就有麻煩了:
- 系統一直執行,輸出越來越多,磁碟空間逐漸被寫滿
- 不同的業務想要把日誌輸出在不同的位置
- 有些場合為了更高效能,儘量控制減少日誌輸出,需要動態調整日誌輸出量
- 自動輸出日誌相關資訊,比如:日期、執行緒、方法名稱等等
顯然System.out
解決不了我們的問題,但是我們遇到的問題一定會有前人遇到過,日誌也不例外,其中就有一個大牛 Ceki,整個Java的日誌體系幾乎都有Ceki參與或者受到了Ceki的深度影響。當然Java日誌體系的複雜度也有一部分原因是拜這位大牛所賜。
Java日誌的恩怨情仇
- 1996年早期,歐洲安全電子市場專案組決定編寫它自己的程式跟蹤API(Tracing API)。經過不斷的完善,這個API終於成為一個十分受歡迎的Java日誌軟體包,即Log4j(由Ceki建立)。
- 後來Log4j成為Apache基金會專案中的一員,Ceki也加入Apache組織。後來Log4j近乎成了Java社群的日誌標準。據說Apache基金會還曾經建議Sun引入Log4j到Java的標準庫中,但Sun拒絕了。
- 2002年Java1.4釋出,Sun推出了自己的日誌庫JUL(Java Util Logging),其實現基本模仿了Log4j的實現。在JUL出來以前,Log4j就已經成為一項成熟的技術,使得Log4j在選擇上佔據了一定的優勢。
- 接著,Apache推出了Jakarta Commons Logging,JCL只是定義了一套日誌介面(其內部也提供一個Simple Log的簡單實現),支援執行時動態載入日誌元件的實現,也就是說,在你應用程式碼裡,只需呼叫Commons Logging的介面,底層實現可以是Log4j,也可以是Java Util Logging。
- 後來(2006年),Ceki不適應Apache的工作方式,離開了Apache。然後先後建立了Slf4j(日誌門面介面,類似於Commons Logging)和Logback(Slf4j的實現)兩個專案,並回瑞典建立了QOS公司,QOS官網上是這樣描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一個通用,可靠,快速且靈活的日誌框架)。
- Java日誌領域被劃分為兩大陣營:Commons Logging陣營和Slf4j陣營。
- Commons Logging在Apache大樹的籠罩下,有很大的使用者基數。但有證據表明,形式正在發生變化。2013年底有人分析了GitHub上30000個專案,統計出了最流行的100個Libraries,可以看出Slf4j的發展趨勢更好。
- Apache眼看有被Logback反超的勢頭,於2012-07重寫了Log4j 1.x,成立了新的專案Log4j 2, Log4j 2具有Logback的所有特性。
- 如今日誌框架已經發展為:Slf4j作為API,實現分為logback與log4j(Commons Logging因為效率和API設計等問題,現在逐漸淡出舞臺了)
讓我們來瞻仰一下大神,哈哈:
那麼如何在混亂的Java日誌體系中如何優雅的使用日誌呢?
其實在Ceki設計的體系下,日誌如同Java的JDBC、Servelt等一樣,定義好標準後實現可以互相切換,問題在於定標準的人各自為政搞出來好多標準,JCL、SLF4j等等,官方(Sun公司)又晚又不給力,發展到現在終於被SLF4j以一種巧妙的方式(橋接、繫結,見下文)統一了,標準使用方式如下圖:
這個圖擷取自slf4j手冊,簡化了多餘部分,很清晰的表示了使用方式:
應用引用SLF4j-API(編碼時使用SLF4j的介面org.slf4j.Logger
,而非logback或log4j的實現)
- logbak: slf4j會自動查詢logback實現(logback預設實現了slf4j)
- log4j:使用起來基本一致,只不過多了介面卡層,引用了slf4j-log4j12.jar,官方稱為繫結(concrete-bindings),就是將SLF4j-API繫結到log4j最終輸出日誌
具體依賴如下
logback
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
log4j2
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> </dependency>
得益於maven的依賴傳遞機制,我們不需要顯示宣告依賴SLF4j-API.jar。
可以看到,log4j 多依賴了 log4j-slf4j-impl.jar,其實就是上圖所示的介面卡層,讀者可能好奇,為什麼使用 log4j 會有介面卡層?其原因在於,slf4j 並不是官方規範,所以沒人遵守(也就是自己的日誌框架中沒有原生實現org.slf4j.Logger
介面,如 log4j ),而繫結層( log4j-slf4j-impl.jar)的作用就是通過靜態查詢的方式將使用log4j作為實現(具體原理請關注後續文章),這樣就是實現了不依賴log4j而使用log4j輸出日誌(面向介面程式設計的最佳實踐,Ceki 大神就是用這套思想將 slf4j 做成了 Java 日誌的標準,爛牌翻盤的典範)。
上面這一段講解了繫結(concrete-bindings)思想,是本文的精髓,讀者一定要理解這裡,後面還有橋接思想與之類似,請繼續閱讀。
小結
至此我們已經完成了日誌的整合,但是事情真的這麼簡單嗎?
先梳理一下,如此混亂的日誌體系下(slf4j,jul,jcl,logback,log4j)會不會會產生什麼問題?答案是一定的,各種第三方庫使用了不同的日誌框架,如果我們依賴 Spring ,Spring(非boot)的預設日誌實現是JCL、又或者我們已有專案已經使用了Log4j,想使用logback的話,難道要逐個類改程式碼嗎(官方有遷移工具)?我們能不能只用一種框架來處理JUL(java.util.logging)、JCL(Jakarta Commons Logging)、Log4j1、Log4j2 呢?
答案是肯定的,Ceki 的 Slf4j 給出瞭解決方案,就是上文所說的橋接( Bridging legacy),簡單來說就是劫持以上所以第三方日誌輸出並重定向至 SLF4j,最終實現統一日誌上層 API(編碼) 與下層實現(輸出日誌位置、格式統一)。我們來看一下圖示
上圖左側就是前一張圖的 logback 日誌實現,為了相容其他日誌,我們需要引用右側的橋接包:xxx-over/to-slf4j.jar ,xxx對應日誌框架,使用 logback 的情況下,除了上文的 logback 依賴,還需要引入以下依賴才能保證所有日誌都被橋接至slf4j。
如何橋接?
logback 如下
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> </dependency> <!-- log4j 橋接包,slf4j官方實現,另有log4j官方實現,二選一即可 log4j-to-slf4j--> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> </dependency>
log4j2 如下
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> </dependency> <!-- 以下是橋接包,使用了log4j作為底層實現, 不能再橋接log4j,否則會出現無限遞迴的情況(具體原因請關注後續文章) --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> </dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> </dependency>
SpringBoot 專案引用了一部分依賴,所以使用起來略微有些不同:
logback 如下
<!-- logback作為內建實現,使用相對簡單 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency><!-- 引入缺少的橋接包 --><dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> </dependency>
log4j2 如下
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <!-- 使用log4j2要排除logback依賴 --> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- Spring已經寫好了一個log4j2-starter但缺少橋接包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <!-- 引入缺少的橋接包 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> </dependency>
結束語
以上兩種才是專案中的最佳使用方式,其他筆者不推薦使用。
最後來看一下 slf4j 如何使用:
static final org.slf4j.Logger logger = LoggerFactory.getLogger(TestLog.class); logger.trace("A TRACE Message"); logger.debug("A DEBUG Message"); logger.info("An INFO Message"); logger.warn("A WARN Message"); logger.error("An ERROR Message");
這樣使用我們就可以隨意切換日誌實現而無需改動程式碼,操作起來也簡單,只需要按照上文切換依賴即可。至於其他使用細節本文不在贅述,關注後續文章(最佳實踐、配置檔案、原理、擴充套件等)。
如果覺得寫的不錯,求關注、求點贊、求轉發,如果有問題或者文中有錯誤,歡迎留言討論。
掃描關注公眾號,第一時間獲得更新
參考:
Java-日誌的江湖
http://www.slf4j.org/manual.html
http://www.slf4j.org/legacy.html
www.baeldung.com/spring-boot…
轉載請註明出