本文是一個系列,歡迎關注
日志到底是何方神聖?為什么要使用日志框架?
想必大家都有過使用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…
轉載請注明出處。