天天看點

Java日志通關 - 前世今生

作者:閃念基因
Java日志通關 - 前世今生

導讀

作者日常在與其他同學合作時,經常發現不合理的日志配置以及五花八門的日志記錄方式,後續作者打算在團隊内做一次Java日志的分享,本文是整理出的系列文章第一篇。

寫這篇文章的初衷,是想在團隊内做一次Java日志的分享,因為日常在與其他同學合作時,經常發現不合理的日志配置以及五花八門的日志記錄方式。但在準備分享、補充細節的過程中,我又進一步發現目前日志相關的文章,都隻是專注于某一個方面,或者講曆史和原理,或者解決包沖突,卻都沒有把整個Java日志知識串聯起來。最終這篇文章超越了之前的定位,越寫越豐富,為了讓大家看得不累,我的文章将以系列的形式展示。

一、前言

日志發展到今天,被抽象成了三層:接口層、實作層、适配層:

Java日志通關 - 前世今生
  • 接口層:或者叫日志門面(facade),就是interface,隻定義接口,等着别人實作。
  • 實作層:真正幹活的、能夠把日志内容記錄下來的工具。但請注意它不是上邊接口實作,因為它不感覺也不直接實作接口,僅僅是獨立的實作。
  • 适配層:一般稱為Adapter,它才是上邊接口的implements。因為接口層和适配層并非都出自一家之手,它們之間無法直接比對。而魯迅曾經說過:「計算機科學領域的任何問題都可以通過增加一個中間層來解決」(All problems in computer science can be solved by another level of indirection. -- David Wheeler[1]),是以就有了适配層。

适配層又可以分為綁定(Binding)和橋接(Bridging)兩種能力:

  • 綁定(Binding):将接口層綁定到某個實作層(實作一個接口層,并調用實作層的方法)
  • 橋接(Bridging):将接口層橋接到另一個接口層(實作一個接口層,并調用另一個接口層的接口),主要作用是友善使用者低成本的在各接口層和适配層之間遷移

如果你覺得上面的描述比較抽象生硬,可以先跳過,等把本篇看完自然就明白了。

接下來我們就以時間順序,回顧一下Java日志的發展史,這有助于指導我們後續的實踐,真正做到知其是以然。

二、曆史演進

2.1 标準輸出 (<1999)

Java最開始并沒有專門記錄日志的工具,大家都是用System.out和System.err輸出日志。但它們隻是簡單的資訊輸出,無法區分錯誤級别、無法控制輸出粒度,也沒有什麼管理、過濾能力。随着Java工程化的深入,它們的能力就有些捉襟見肘了。

雖然System.out和System.err預設輸出到控制台,但它們是有能力将輸出儲存到檔案的:

System.setOut(new PrintStream(new FileOutputStream("log.txt", true)));
System.out.println("這句将輸出到 log.txt 檔案中");


System.setErr(new PrintStream(new FileOutputStream("error.txt", true)));
System.err.println("這句将輸出到 error.txt 檔案中");           

2.2 Log4j (1999)

在1996年,一家名為SEMPER的歐洲公司決定開發一款用于記錄日志的工具。經過多次疊代,最終發展成為Log4j。這款工具的主要作者是一位名叫Ceki Gülcü[2]的俄羅斯程式員,請記住他的名字:Ceki,後面還會多次提到他。

到了1999年,Log4j已經被廣泛使用,随着使用者規模的增長,使用者訴求也開始多樣化。于是Ceki在2001年選擇将Log4j開源,希望借助社群的力量将Log4j發展壯大。不久之後Apache基金會向Log4j抛出了橄榄枝,自然Ceki也加入Apache繼續從事 Log4j的開發,從此Log4j改名Apache Log4j[3]并進入發展的快車道。

Log4j相比于System.out提供了更強大的能力,甚至很多思想到現在仍被廣泛接受,比如:

  • 日志可以輸出到控制台、檔案、資料庫,甚至遠端伺服器和電子郵件(被稱做 Appender);
  • 日志輸出格式(被稱做 Layout)允許定制,比如錯誤日志和普通日志使用不同的展現形式;
  • 日志被分為5個級别(被稱作Level),從低到高依次是debug, info, warn, error, fatal,輸出前會校驗配置的允許級别,小于此級别的日志将被忽略。除此之外還有all, off兩個特殊級别,表示完全放開和完全關閉日志輸出;
  • 可以在工程中随時指定不同的記錄器(被稱做Logger),可以為之配置獨立的記錄位置、日志級别;
  • 支援通過properties或者xml檔案進行配置;

随着Log4j的成功,Apache又孵化了Log4Net[4]、Log4cxx[5]、Log4php[6]産品,開源社群也模仿推出了如Log4c[7]、Log4cpp[8]、Log4perl[9]等衆多項目。從中也可以印證Log4j在日志處理領域的江湖影響力。

不過Log4j有比較明顯的性能短闆,在Logback和Log4j 2推出後逐漸式微,最終Apache在2015年宣布終止開發Log4j并全面遷移至Log4j 2[10](可參考【2.7 Log4j 2 (2012)】)。

2.3 JUL (2002.2)

随着Java工程的發展,Sun也意識到日志記錄非常重要,認為這個能力應該由JRE原生支援。是以在1999年Sun送出了JSR 047[11]提案,标題就叫「Logging API Specification」。不過直到2年後的2002年,Java官方的日志系統才随Java 1.4釋出。這套系統稱做Java Logging API,包路徑是java.util.logging,簡稱JUL。

在某些追溯曆史的文章中提到,「Apache曾希望将 Log4j加入到JRE中作為預設日志實作,但傲慢的Sun沒有答應,反而很快推出了自己的日志系統」。對于這個說法我并沒有找到出處,無法确認其真實性。

不過從實際推出的産品來看,更晚面世的JUL無論是功能還是性能都落後于Log4j,頗有因被寄予厚望而倉促釋出的味道,也許那個八卦并非空穴來風,哈哈。雖然在2004年推出的Java 5.0 (1.5) [12]上JUL進步不小,但它在Log4j面前仍無太多亮點,廣大開發者并沒有遷移的動力,導緻JUL始終未成氣候。

我們在後文沒有推薦JUL的計劃,是以這裡也不多介紹了(主要是我也不會)。

2.4 JCL (2002.8)

在Log4j和JUL之外,當時市面上還有像Apache Avalon[13](一套服務端開發架構)、 Lumberjack[14](一套跑在JDK 1.2/1.3上的開源日志工具)等日志工具。

對于獨立且輕量的項目來說,開發者可以根據喜好使用某個日志方案即可。但更多情況是一套業務系統依賴了大量的三方工具,而衆多三方工具會各自使用不同的日志實作,當它們被內建在一起時,必然導緻日志記錄混亂。

為此Apache在2002年推出了一套接口Jakarta Commons Logging[15],簡稱 JCL,它的主要作者仍然是Ceki。這套接口主動支援了Log4j、JUL、Apache Avalon、Lumberjack等衆多日志工具。開發者如果想列印日志,隻需調用JCL的接口即可,至于最終使用的日志實作則由最上層的業務系統決定。我們可以看到,這其實就是典型的接口與實作分離設計。

但因為是先有的實作(Log4j、JUL)後有的接口(JCL),是以JCL配套提供了接口與實作的适配層(沒有使用它的最新版,原因會在【1.2.7 Log4j2 (2012)】提到):

Java日志通關 - 前世今生

簡單介紹一下JCL自帶的幾個适配層/實作層:

  • AvalonLogger/LogKitLogger:用于綁定Apache Avalon的适配層,因為Avalon 不同時期的日志包名不同,适配層也對應有兩個
  • Jdk13LumberjackLogger:用于綁定Lumberjack的适配層
  • Jdk14Logger:用于綁定JUL(因為JUL從JDK 1.4開始提供)的适配層
  • Log4JLogger:用于綁定Log4j的适配層
  • NoOpLog:JCL自帶的日志實作,但它是空實作,不做任何事情
  • SimpleLog:JCL自帶的日志實作 ,讓使用者哪怕不依賴其他工具也能列印出日志來,隻是功能非常簡單

當時項目字首取名Jakarta,是因為它屬于Apache與Sun共同推出的Jakarta Project[16]項目(郵件[17])。現在JCL作為Apache Commons[18]的子項目,叫 Apache Commons Logging,與我們常用的Commons Lang[19]、Commons Collections [20]等是師兄弟。但JCL的簡寫命名被保留了下來,并沒有改為ACL。

2.5 Slf4j (2005)

Log4j的作者Ceki看到了很多Log4j和JCL的不足,但又無力推動項目快速疊代,加上對Apache的管理不滿,認為自己失去了對Log4j項目的控制權(部落格[21]、郵件[22]),于是在2005年選擇自立門戶,并很快推出了一款新作品Simple Logging Facade for Java[23],簡稱Slf4j。

Slf4j也是一個接口層,接口設計與JCL非常接近(畢竟有師承關系)。相比JCL有一個重要的差別是日志實作層的綁定方式:JCL是動态綁定,即在運作時執行日志記錄時判定合适的日志實作;而Slf4j選擇的是靜态綁定,應用編譯時已經确定日志實作,性能自然更好。這就是常被提到的classloader問題,更詳細地讨論可以參考What is the issue with the runtime discovery algorithm of Apache Commons Logging[24]以及Ceki自己寫的文章Taxonomy of class loader problems encountered when using Jakarta Commons Logging[25]。

在推出Slf4j的時候,市面上已經有了另一套接口層JCL,為了将選擇權交給使用者(我猜也為了挖JCL的牆角),Slf4j推出了兩個橋接層:

  • jcl-over-slf4j:作用是讓已經在使用JCL的使用者友善的遷移到Slf4j 上來,你以為調的是JCL接口,背後卻又轉到了Slf4j接口。我說這是在挖JCL的牆角不過分吧?
  • slf4j-jcl:讓在使用Slf4j的使用者友善的遷移到JCL上,自己的牆角也挖,主打的就是一個公平公正公開。

Slf4j通過推出各種适配層,基本滿足了使用者的所有場景,我們來看一下它的全家桶:

Java日志通關 - 前世今生

網上介紹Slf4j的文章,經常會引用它官網上的兩張圖:

Java日志通關 - 前世今生
Java日志通關 - 前世今生

感興趣的同學也可以參考。

這裡解釋一下slf4j-log4j12這個名字,它表示Slf4j + Log4j 1.2(Log4j的最後一個版本) 的适配層。類似的,slf4j-jdk14表示Slf4j + JDK 1.4(就是 JUL)的适配層。

2.6 Logback (2006)

然而Ceki的目标并不止于Slf4j,面對自己一手創造的Log4j,作為原作者自然是知道它存在哪些問題的。于是在2006年Ceki又推出了一款日志記錄實作方案:Logback[26]。無論是易用度、功能、還是性能,Logback 都要優于Log4j,再加上天然支援Slf4j而不需要額外的适配層,自然擁趸者衆。目前Logback已經成為Java社群最被廣泛接受的日志實作層(Logback自己在2021年的統計是48%的市占率[27])。

相比于Log4j,Logback提供了很多我們現在看起來理所當然的新特性:

  • 支援日志檔案切割滾動記錄、支援異步寫入
  • 針對曆史日志,既支援按時間或按硬碟占用自動清理,也支援自動壓縮以節省硬碟空間
  • 支援分支文法,通過<if>, <then>, <else>可以按條件配置不同的日志輸出邏輯,比如判斷僅在開發環境輸出更詳細的日志資訊
  • 大量的日志過濾器,甚至可以做到通過登入使用者Session識别每一位使用者并輸出獨立的日志檔案
  • 異常堆棧支援列印jar包資訊,讓我們不但知道調用出自哪個檔案哪一行,還可以知道這個檔案來自哪個jar包

Logback主要由三部分組成(網上各種文章在介紹classic和access時都描述的語焉不詳,我不得不直接翻官網文檔找更明确的解釋):

  • logback-core:記錄/輸出日志的核心實作
  • logback-classic:适配層,完整實作了Slf4j接口
  • logback-access[28]:用于将Logback內建到Servlet容器(Tomcat、Jetty)中,讓這些容器的HTTP通路日志也可以經由強大的Logback輸出

2.7 Log4j 2 (2012)

看着Slf4j + Logback搞的風生水起,Apache自然不會坐視不理,終于在2012年憋出一記大招:Apache Log4j 2[29],它自然也有不少亮點:

  • 插件化結構[30],使用者可以自己開發插件,實作Appender、Logger、Filter完成擴充
  • 基于LMAX Disruptor的異步化輸出[31],在多線程場景下相比Logback有10倍左右的性能提升,Apache官方也把這部分作為主要賣點加以宣傳,詳細可以看Log4j 2 Performance[32]。

Log4j 2主要由兩部分組成:

  • log4j-core:核心實作,功能類似于logback-core
  • log4j-api:接口層,功能類似于Slf4j,裡面隻包含Log4j 2的接口定義

你會發現Log4j 2的設計别具一格,提供JCL和Slf4j之外的第三個接口層(log4j-api,雖然隻是自己的接口),它在官網API Separation[33]一節中解釋說,這樣設計可以允許使用者在一個項目中同時使用不同的接口層與實作層。

不過目前大家一般把Log4j 2作為實作層看待,并引入JCL或Slf4j作為接口層。特别是JCL,在時隔近十年後,于2023年底推出了1.3.0 版[34],增加了針對Log4j 2的适配。還記得我們在【1.2.4 JCL (2002.8)】中沒有用最新版的JCL做介紹嗎,就是因為這個十年之後的版本把那些已經「作古」的日志适配層@Deprecated掉了。

多說一句,其實Logback和Slf4j就像log4j-core和log4j-api的關系一下,目前如果你想用Logback也隻能借助Slf4j。但誰讓它們生逢其時呢,大家就會分别讨論認為是兩個産品。

雖然Log4j 2釋出至今已有十年(本文寫于2024年),但它仍然無法撼動Logback的江湖地位,我個人總結下來主要有兩點:

  • Log4j 2雖然頂着Log4j的名号,但卻是一套完全重寫的日志系統,無法隻通過修改Log4j版本号完成更新,曆史使用者更新意願低
  • Log4j 2比Logback晚面世6年,卻沒有提供足夠亮眼及差異化的能力(前邊介紹的兩個亮點對普通使用者并沒有足夠吸引力),而Slf4j+Logback這套組合已經非常優秀,先發優勢明顯

比如,曾有人建議Spring Boot将日志系統從Logback切換到Log4j2[35],但被Phil Webb[36](Spring Boot核心貢獻者)否決。他在回複中給出的原因包括:Spring Boot需要保證向前相容以友善使用者更新,而切換Log4j 2是破壞性的;目前絕大部分使用者并未面臨日志性能問題,Log4j 2所推崇的性能優勢并非架構與使用者的核心關切;以及如果使用者想在Spring Boot中切換到Log4j 2也很友善(如需切換可參考 官方文檔[37])。

2.8 spring-jcl (2017)

因為目前大部分應用都基于Spring/Spring Boot搭建,是以我額外介紹一下spring-jcl [38]這個包,目前Spring Boot用的就是spring-jcl + Logback這套方案。

Spring曾在它的官方Blog《Logging Dependencies in Spring》[39]中提到,如果可以重來,Spring會選擇李白Slf4j而不是JCL作為預設日志接口。

現在Spring又想支援Slf4j,又要保證向前相容以支援JCL,于是從5.0(Spring Boot 2.0)開始提供了spring-jcl這個包。它頂着Spring的名号,代碼中包名卻與JCL 一緻(org.apache.commons.logging),作用自然也與JCL一緻,但它額外适配了Slf4j,并将Slf4j放在查找的第一順位,進而做到了「既要又要」(你可以回到【1.2.4 JCL (2002.8)】節做一下對比)。

Java日志通關 - 前世今生

如果你是基于Spring Initialize [40]新建立的應用,可以不必管這個包,它已經在背後默默工作了;如果你在項目開發過程中遇到包沖突,或者需要自己選擇日志接口和實作,則可以把spring-jcl當作JCL對待,大膽排除即可。

2.9 其他

除了我們上邊提到的日志解決方案,還有一些不那麼常見的,比如:

  • Flogger[41]:由Google在2018年推出的日志接口層。首字母F的含義是Fluent,這也正是它的最大特點:鍊式調用(或者叫流式API,Slf4j 2.0也支援Fluent API 了,我們會在後續系列文章中介紹)
  • JBoss Logging[42]:由RedHat在約2010年推出,包含完整的接口層、實作層、适配層
  • slf4j-reload4j[43]:Ceki基于Log4j 1.2.7 fork出的版本,旨在解決Log4j的安全問題,如果你的項目還在使用Log4j且不想遷移,建議平替為此版本。(但也不是所有安全問題都能解決,具體可以參考上邊的連結)

因為這些日志架構我們在實際開發中用的很少,此文也不再贅述了(主要是我也不會)。

三、總結

曆史介紹完了,但故事并沒有結束。兩個接口(JCL、Slf4j)四個實作(Log4j、JUL、Logback、Log4j2),再加上無數的适配層,它們之間串聯成了一個網,我專門畫了一張圖:

Java日志通關 - 前世今生

解釋/補充一下這張圖:

  1. 相同顔色的子產品擁有相同的groupId,可以參考圖例中給出的具體值。
  2. JCL的适配層是直接在它自己的包中提供的,詳情我們在前邊已經介紹過,可以回【1.2.4 JCL (2002.8)】檢視。
  3. 要想使用Logback,就一定繞不開Slf4j(引用它的适配層也算);同樣的,要想使用 Log4j 2,那它的log4j-api也繞不開。

如果你之前在看「1.1 前言」時覺得過于抽象,那麼此時建議你再回頭看一下,相信會有更多體會。

從這段曆史,我也發現了幾個有趣的細節:

  • 在Log4j 2面世前後的很長一段時間,Slf4j及Logback因為沒有競争對手而更新緩慢。英雄沒有對手隻能慢慢垂暮,隻有棋逢對手才能笑傲江湖。
  • 技術人的善良與倔強:面世晚的産品都針對前輩産品提供支援;面世早的産品都不搭理它的「後輩」。
  • 計算機科學領域的任何問題都可以通過增加一個中間層來解決,如果不行就兩個(橋接層幹的事兒)。
  • Ceki一人肩挑Java日志半壁江山25年(還在增長ing),真神人也。(當然在代碼界有很多這樣的神人,比如Linus Torvalds[44]維護Linux至今已有33年,雖然後期主要作為産品經理參與,再比如已故的Bram Moolenaar[45]老爺子持續維護 Vim 32年之久)。

參考連結:

[1]https://codedocs.org/what-is/david-wheeler-computer-scientist
[2]https://github.com/ceki
[3]https://logging.apache.org/log4j/1.2/
[4]https://logging.apache.org/log4net/
[5]https://logging.apache.org/log4cxx/
[6]https://logging.apache.org/log4php/
[7]https://log4c.sourceforge.net/
[8]https://log4cpp.sourceforge.net/
[9]https://mschilli.github.io/log4perl/
[10]https://news.apache.org/foundation/entry/apache_logging_services_project_announces
[11]https://jcp.org/en/jsr/detail
[12]https://www.java.com/releases/
[13]https://avalon.apache.org/
[14]https://javalogging.sourceforge.net/
[15]https://commons.apache.org/proper/commons-logging/
[16]https://jakarta.apache.org/
[17]https://lists.apache.org/thread/53otcqljjfnvjs3hv8m4ldzlgz59yk6k
[18]https://commons.apache.org/
[19]https://commons.apache.org/proper/commons-lang/
[20]https://commons.apache.org/proper/commons-collections/
[21]http://ceki.blogspot.com/2010/05/forces-and-vulnerabilites-of-apache.html
[22]https://lists.apache.org/thread/dyzmtholjdlf3h32vvl85so8sbj3v0qz
[23]https://www.slf4j.org/
[24]https://stackoverflow.com/questions/3222895/what-is-the-issue-with-the-runtime-discovery-algorithm-of-apache-commons-logging
[25]https://articles.qos.ch/classloader.html
[26]https://logback.qos.ch/
[27]https://qos.ch/
[28]https://logback.qos.ch/access.html
[29]https://logging.apache.org/log4j/2.x/
[30]https://logging.apache.org/log4j/2.x/manual/extending.html
[31]https://logging.apache.org/log4j/2.x/manual/async.html
[32]https://logging.apache.org/log4j/2.x/performance.html
[33]https://logging.apache.org/log4j/2.x/manual/api-separation.html
[34]https://commons.apache.org/proper/commons-logging/changes-report.html
[35]https://github.com/spring-projects/spring-boot/issues/16864
[36]https://spring.io/team/philwebb
[37]https://docs.spring.io/spring-boot/docs/3.2.x/reference/html/howto.html
[38]https://docs.spring.io/spring-framework/reference/core/spring-jcl.html
[39]https://spring.io/blog/2009/12/04/logging-dependencies-in-spring
[40]https://start.spring.io/
[41]https://google.github.io/flogger/
[42]https://github.com/jboss-logging
[43]https://reload4j.qos.ch/
[44]https://github.com/torvalds
[45]https://moolenaar.net/           

作者:尚左

來源-微信公衆号:阿裡雲開發者

出處:https://mp.weixin.qq.com/s/eIiu08fVk194E0BgGL5gow