1. 程式人生 > >Java日誌體系居然這麼複雜?——架構篇

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日誌體系中如何優雅的使用日誌呢?

其實在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…

 轉載請註明出