01.06 Java日誌體系居然這麼複雜?——架構篇

日誌到底是何方神聖?為什麼要使用日誌框架?

想必大家都有過使用 System.out 來進行輸出調試,開發開發環境下這樣做當然很方便,但是線上這樣做就有麻煩了:

  1. 系統一直運行,輸出越來越多,磁盤空間逐漸被寫滿
  2. 不同的業務想要把日誌輸出在不同的位置
  3. 有些場合為了更高性能,儘量控制減少日誌輸出,需要動態調整日誌輸出量
  4. 自動輸出日誌相關信息,比如:日期、線程、方法名稱等等

顯然 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日誌體系居然這麼複雜?——架構篇

那麼如何在混亂的Java日誌體系中如何優雅的使用日誌呢?

其實在Ceki設計的體系下,日誌如同Java的JDBC、Servelt等一樣,定義好標準後實現可以互相切換,問題在於定標準的人各自為政搞出來好多標準,JCL、SLF4j等等,官方(Sun公司)又晚又不給力,發展到現在終於被SLF4j以一種巧妙的方式(橋接、綁定,見下文)統一了,標準使用方式如下圖:

Java日誌體系居然這麼複雜?——架構篇

這個圖截取自 slf4j手冊 ,簡化了多餘部分,很清晰的表示了使用方式:

應用引用SLF4j-API(編碼時使用SLF4j的接口 org.slf4j.Logger ,而非logback或log4j的實現)

  • logbak: slf4j會自動查找logback實現(logback默認實現了slf4j)
  • log4j:使用起來基本一致,只不過多了適配器層,引用了slf4j-log4j12.jar,官方稱為綁定(concrete-bindings),就是將SLF4j-API綁定到log4j最終輸出日誌

具體依賴如下

logback

<code><dependency>
<groupid>ch.qos.logback/<groupid>
<artifactid>logback-classic/<artifactid>
<version>1.2.3/<version>
/<dependency>
/<code>

log4j2

<code><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>
/<code>

得益於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(編碼) 與下層實現(輸出日誌位置、格式統一)。我們來看一下圖示

Java日誌體系居然這麼複雜?——架構篇

上圖左側就是前一張圖的 logback 日誌實現,為了兼容其他日誌,我們需要引用右側的橋接包:xxx-over/to-slf4j.jar ,xxx對應日誌框架,使用 logback 的情況下,除了上文的 logback 依賴,還需要引入以下依賴才能保證所有日誌都被橋接至slf4j。

如何橋接?

logback 如下

<code><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>

<dependency>
<groupid>org.slf4j/<groupid>
<artifactid>log4j-over-slf4j/<artifactid>
/<dependency>
/<code>

log4j2 如下

<code><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>

<dependency>
<groupid>org.slf4j/<groupid>

<artifactid>jcl-over-slf4j/<artifactid>
/<dependency><dependency>
<groupid>org.slf4j/<groupid>
<artifactid>jul-to-slf4j/<artifactid>
/<dependency>
/<code>

SpringBoot 項目引用了一部分依賴,所以使用起來略微有些不同:

logback 如下

<code>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter/<artifactid>
/<dependency><dependency>
<groupid>org.slf4j/<groupid>
<artifactid>jcl-over-slf4j/<artifactid>
/<dependency>
/<code>

log4j2 如下

<code><dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter/<artifactid>

<exclusions>
<exclusion>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-logging/<artifactid>
/<exclusion>
/<exclusions>
/<dependency>

<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-log4j2/<artifactid>
/<dependency>

<dependency>
<groupid>org.slf4j/<groupid>
<artifactid>jcl-over-slf4j/<artifactid>
/<dependency>
/<code>

結束語

以上兩種才是項目中的最佳使用方式,其他筆者不推薦使用。

最後來看一下 slf4j 如何使用:

<code>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");
/<code>

這樣使用我們就可以隨意切換日誌實現而無需改動代碼,操作起來也簡單,只需要按照上文切換依賴即可。至於其他使用細節本文不在贅述,關注後續文章(最佳實踐、配置文件、原理、擴展等)。


分享到:


相關文章: