Java日誌框架

轉載自:http://www.javashuo.com/article/p-qgnyfndm-bp.htmlhtml

1、Java日誌框架概述

對於一個應用程序來講日誌記錄是必不可少的一部分。線上問題追蹤,基於日誌的業務邏輯統計分析等都離不日誌。java領域存在多種日誌框架,目前經常使用的日誌框架包括Log4j 1,Log4j 2,Commons Logging,Slf4j,Logback,Jul。java

一、日誌框架類別

  • Log4j Apache Log4j是一個基於Java的日誌記錄工具。它是由Ceki Gülcü獨創的,如今則是Apache軟件基金會的一個項目。 Log4j是幾種Java日誌框架之一。
  • Log4j 2 Apache Log4j 2是apache開發的一款Log4j的升級產品。
  • Commons Logging Apache基金會所屬的項目,是一套Java日誌接口,以前叫Jakarta Commons Logging,後改名爲Commons Logging。
  • Slf4j 相似於Commons Logging,是一套簡易Java日誌門面,自己並沒有日誌的實現。(Simple Logging Facade for Java,縮寫Slf4j)。
  • Logback 一套日誌組件的實現(Slf4j陣營)。
  • Jul (Java Util Logging),自Java1.4以來的官方日誌實現。

看了上面的介紹是否會以爲比較混亂,這些日誌框架之間有什麼異同,都是由誰在維護,在項目中應該如何選擇日誌框架,應該如何使用? 下文會逐一介紹。git

二、日誌框架歷史

  • 1996年早期,歐洲安全電子市場項目組決定編寫它本身的程序跟蹤API(Tracing API)。通過不斷的完善,這個API終於成爲一個十分受歡迎的Java日誌軟件包,即Log4j。後來Log4j成爲Apache基金會項目中的一員。github

  • 期間Log4j近乎成了Java社區的日誌標準。聽說Apache基金會還曾經建議Sun引入Log4j到java的標準庫中,但Sun拒絕了。spring

  • 2002年Java1.4發佈,Sun推出了本身的日誌庫JUL(Java Util Logging),其實現基本模仿了Log4j的實現。在JUL出來之前,Log4j就已經成爲一項成熟的技術,使得Log4j在選擇上佔據了必定的優點。apache

  • 接着,Apache推出了Jakarta Commons Logging,JCL只是定義了一套日誌接口(其內部也提供一個Simple Log的簡單實現),支持運行時動態加載日誌組件的實現,也就是說,在你應用代碼裏,只需調用Commons Logging的接口,底層實現能夠是Log4j,也能夠是Java Util Logging。設計模式

  • 後來(2006年),Ceki Gülcü不適應Apache的工做方式,離開了Apache。而後前後建立了Slf4j(日誌門面接口,相似於Commons Logging)和Logback(Slf4j的實現)兩個項目,並回瑞典建立了QOS公司,QOS官網上是這樣描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一個通用,可靠,快速且靈活的日誌框架)。api

  • 現今,Java日誌領域被劃分爲兩大陣營:Commons Logging陣營和Slf4j陣營。
    Commons Logging在Apache大樹的籠罩下,有很大的用戶基數。但有證據代表,形式正在發生變化。2013年末有人分析了GitHub上30000個項目,統計出了最流行的100個Libraries,能夠看出Slf4j的發展趨勢更好:安全

    java_populor_jar

  • Apache眼看有被Logback反超的勢頭,於2012-07重寫了Log4j 1.x,成立了新的項目Log4j 2, Log4j 2具備Logback的全部特性。框架

三、日誌框架關係

  • Log4j 2與Log4j 1發生了很大的變化,Log4j 2不兼容Log4j 1。
  • Commons Logging和Slf4j是日誌門面(門面模式是軟件工程中經常使用的一種軟件設計模式,也被稱爲正面模式、外觀模式。它爲子系統中的一組接口提供一個統一的高層接口,使得子系統更容易使用)。Log4j和Logback則是具體的日誌實現方案。能夠簡單的理解爲接口與接口的實現,調用者只須要關注接口而無需關注具體的實現,作到解耦。
  • 比較經常使用的組合使用方式是Slf4j與Logback組合使用,Commons Logging與Log4j組合使用。
  • Logback必須配合Slf4j使用。因爲Logback和Slf4j是同一個做者,其兼容性不言而喻。

四、Commons Logging與Slf4j對比

Commons Logging實現機制

Commons Logging是經過動態查找機制,在程序運行時,使用本身的ClassLoader尋找和載入本地具體的實現。詳細策略能夠查看commons-logging-*.jar包中的org.apache.commons.logging.impl.LogFactoryImpl.java文件。因爲Osgi不一樣的插件使用獨立的ClassLoader,Osgi的這種機制保證了插件互相獨立, 其機制限制了Commons Logging在Osgi中的正常使用。

Slf4j實現機制

Slf4j在編譯期間,靜態綁定本地的Log庫,所以能夠在Osgi中正常使用。它是經過查找類路徑下org.slf4j.impl.StaticLoggerBinder,而後在StaticLoggerBinder中進行綁定。

五、項目中日誌框架選擇

若是是在一個新的項目中建議使用Slf4j與Logback組合,這樣有以下的幾個優勢。

  • Slf4j實現機制決定Slf4j限制較少,使用範圍更廣。因爲Slf4j在編譯期間,靜態綁定本地的LOG庫使得通用性要比Commons Logging要好。
  • Logback擁有更好的性能。Logback聲稱:某些關鍵操做,好比斷定是否記錄一條日誌語句的操做,其性能獲得了顯著的提升。這個操做在Logback中須要3納秒,而在Log4J中則須要30納秒。LogBack建立記錄器(logger)的速度也更快:13毫秒,而在Log4J中須要23毫秒。更重要的是,它獲取已存在的記錄器只需94納秒,而Log4J須要2234納秒,時間減小到了1/23。跟JUL相比的性能提升也是顯著的。
  • Commons Logging開銷更高
# 在使Commons Logging時爲了減小構建日誌信息的開銷,一般的作法是
if(log.isDebugEnabled()){
  log.debug("User name: " +
    user.getName() + " buy goods id :" + good.getId());
}

# 在Slf4j陣營,你只需這麼作:
log.debug("User name:{} ,buy goods id :{}", user.getName(),good.getId());

# 也就是說,Slf4j把構建日誌的開銷放在了它確認須要顯示這條日誌以後,減小內存和Cup的開銷,使用佔位符號,代碼也更爲簡潔
  • Logback文檔免費。Logback的全部文檔是全面免費提供的,不象Log4J那樣只提供部分免費文檔而須要用戶去購買付費文檔。

2、Slf4j用法

一、Slf4j與其它日誌組件的關係說明

  • Slf4j的設計思想比較簡潔,使用了Facade設計模式,Slf4j自己只提供了一個slf4j-api-version.jar包,這個jar中主要是日誌的抽象接口,jar中自己並無對抽象出來的接口作實現。
  • 對於不一樣的日誌實現方案(例如Logback,Log4j...),封裝出不一樣的橋接組件(例如logback-classic-version.jar,slf4j-log4j12-version.jar),這樣使用過程當中能夠靈活的選取本身項目裏的日誌實現。

二、Slf4j與其它日誌組件調用關係圖

slf4j-bind

三、Slf4j與其餘各類日誌組件的橋接說明

jar包名 說明
slf4j-log4j12-1.7.13.jar Log4j1.2版本的橋接器,你須要將Log4j.jar加入Classpath。
slf4j-jdk14-1.7.13.jar java.util.logging的橋接器,Jdk原生日誌框架。
slf4j-nop-1.7.13.jar NOP橋接器,默默丟棄一切日誌。
slf4j-simple-1.7.13.jar 一個簡單實現的橋接器,該實現輸出全部事件到System.err. 只有Info以及高於該級別的消息被打印,在小型應用中它也許是有用的。
slf4j-jcl-1.7.13.jar Jakarta Commons Logging 的橋接器. 這個橋接器將Slf4j全部日誌委派給Jcl。
logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar) Slf4j的原生實現,Logback直接實現了Slf4j的接口,所以使用Slf4j與Logback的結合使用也意味更小的內存與計算開銷
  • 具體的接入方式參見下圖
    slf4j-concrete-bindings1

3、Slf4j源碼分析

一、slf4j-api-version.jar中幾個核心類與接口

類與接口 用途
org.slf4j.LoggerFactory(class) 給調用方提供的建立Logger的工廠類,在編譯時綁定具體的日誌實現組件
org.slf4j.Logger(interface) 給調用方提供的日誌記錄抽象方法,例如debug(String msg),info(String msg)等方法
org.slf4j.ILoggerFactory(interface) 獲取的Logger的工廠接口,具體的日誌組件實現此接口
org.slf4j.helpers.NOPLogger(class) 對org.slf4j.Logger接口的一個沒有任何操做的實現,也是Slf4j的默認日誌實現
org.slf4j.impl.StaticLoggerBinder(class) 與具體的日誌實現組件實現的橋接類,具體的日誌實現組件須要定義org.slf4j.impl包,並在org.slf4j.impl包下提供此類,注意在slf4j-api-version.jar中不存在org.slf4j.impl.StaticLoggerBinder,在源碼包slf4j-api-version-source.jar中才存在此類

Slf4j調用過程源碼分析,只加入slf4j-api-version.jar,不加入任何實現包

示例代碼參見:https://github.com/chlsmile/slf4j-demo

pom核心配置以下

<dependencies>
    <!--只有slf4j-api依賴-->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.13</version>
    </dependency>
  </dependencies>

程序入口類以下

slf4j-0

源碼分析

  • 1)調用LoggerFactory的getLogger()方法建立Logger
    slf4j-1-getLogger

  • 2)調用LoggerFactory的getILoggerFactory方法來建立ILoggerFactory
    slf4j-2-getILoggerFactory

  • 3)調用LoggerFactory的performInitialization方法來進行初始化
    slf4j-3-performInitialization

  • 4)調用LoggerFactory的bind()方法
    slf4j-4-bind

  • 5)調用LoggerFactory的findPossibleStaticLoggerBinderPathSet()方法獲取StaticLoggerBinderPath集合
    slf4j-5-findPossibleStaticLoggerBinderPathSet

  • 6)調用LoggerFactory的reportMultipleBindingAmbiguity()方法,記錄綁定的StaticLoggerBinder信息
    slf4j-6-bind-1

  • 7)LoggerFactory的reportMultipleBindingAmbiguity()方法
    slf4j-9-bind-NoClassDefFoundError-report

  • 8)LoggerFactory的bind()方法找不到StaticLoggerBinder,拋出NoClassDefFoundError異常
    slf4j-8-bind-NoClassDefFoundError

  • 9)LoggerFactory的bind()方法捕獲NoClassDefFoundError異常,匹配到StaticLoggerBinder關鍵詞記錄信息到控制檯
    slf4j-9-bind-NoClassDefFoundError-report

  • 10)LoggerFactory的performInitialization()方法內部調用bind()方法結束
    slf4j-10-performInitialization-finished

  • 11)LoggerFactory的getLogger()方法內部getILoggerFactory()方法調用完成,建立出NOPLoggerFactory,而後由NOPLoggerFactory調用內部的getLogger()方法,建立出NOPLogger
    slf4j-11-getILoggerFactory-nop_fallback_initialization

    slf4j-12-pre-getLogger

    slf4j-13-NOPLoggerFactory-getLogger

    slf4j-14-NOPLogger-new

  • 12)App類內部的logger實際爲NOPLogger,調用logger.info()方法實際調用的是NOPLogger的info方法
    slf4j-15-App-logger-info

    slf4j-16-NOPLogger-info

二、Slf4j調用過程源碼分析

加入slf4j-api-version.jar,與Logback組件

Slf4j做爲門面採用Logback做爲實現或者採用其它上面提到過的組件做爲實現相似,這裏只分析採用Logback組件做爲實現

示例代碼參見:https://github.com/chlsmile/slf4j-logback-demo

pom核心配置以下

<dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.13</version>
    </dependency>
    <!--logback-classic依賴logback-core,會自動級聯引入-->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.3</version>
    </dependency>
  </dependencies>

程序入口類同上

源碼分析

  • 1)2)3)4)同上
  • 5)調用LoggerFactory的findPossibleStaticLoggerBinderPathSet()方法獲取StaticLoggerBinderPath集合
    slf4j-logabck-001
  • 6)調用LoggerFactory的bind()方法的staticLoggerBinderPathSet集合對象賦值
    slf4j-logabck-002-staticLoggerBinderPathSet
  • 7)在LoggerFactory的bind()方法中調用loback包下的StaticLoggerBinder建立單例對象
    slf4j-logabck-003-StaticLoggerBinder
  • 8)在LoggerFactory的bind()方法中調用reportActualBinding()記錄日誌加載信息
    slf4j-logabck-004-pre-reportActualBinding
    slf4j-logabck-005-reportActualBinding-detail
  • 9)LoggerFactory中INITIALIZATION_STATE的值爲SUCCESSFUL_INITIALIZATION,調用StaticLoggerBinder的單例對象獲取ILoggerFactory
    slf4j-logabck-006
    slf4j-logabck-007-StaticLoggerBinder-getLoggerFactory
  • 10)此時LoggerFactory中的getLogger()方法中獲取到的ILoggerFactory其實是logback jar下的LoggerContext
    slf4j-logabck-008
  • 11)此時LoggerFactory調用getLogger()方法獲取到的Logger其實是logback jar下的Logger
    slf4j-logabck-009
    slf4j-logabck-010

三、Slf4j調用過程源碼分析

加入slf4j-api-version.jar,同時加入多種日誌實現組件

在項目中若是用slf4j-api做爲日誌門面,有多個日誌實現組件同時存在,例如同時存在Logback,slf4j-log4j12,slf4j-jdk14,slf4j-jcl四種實現,則在項目實際運行中,Slf4j的綁定選擇綁定方式將有Jvm肯定,而且是隨機的,這樣會和預期不符,實際使用過程當中須要避免這種狀況。

示例代碼參見:https://github.com/chlsmile/slf4j-logback-log4j-demo

pom核心配置以下

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.25</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.3</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jdk14</artifactId>
      <version>1.7.25</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jcl</artifactId>
      <version>1.7.25</version>
    </dependency>
  </dependencies>

程序入口類同上

源碼分析

  • 基本步驟同上,這裏只追蹤主要不一樣點
  • 1)追蹤LoggerFactory的bind()方法內部調用findPossibleStaticLoggerBinderPathSet()方法後,從classpath下4個jar包內找到StaticLoggerBinder
    slf4j-multiple-bindings-1
  • 2)此時LoggerFactory的bind()方法內部調用reportMultipleBindingAmbiguity()方法,給出警告信息classpath下同時存在多個StaticLoggerBinder,JVM會隨機選擇一個StaticLoggerBinder
    slf4j-multiple-bindings-2

四、使用Slf4時如何橋接遺留的api

在實際環境中咱們常常會遇到不一樣的組件使用的日誌框架不一樣的狀況,例如Spring Framework使用的是日誌組件是Commons Logging,XSocket依賴的則是Java Util Logging。當咱們在同一項目中使用不一樣的組件時應該若是解決不一樣組件依賴的日誌組件不一致的狀況呢?如今咱們須要統一日誌方案,統一使用Slf4j,把他們的日誌輸出重定向到Slf4j,而後Slf4j又會根據綁定器把日誌交給具體的日誌實現工具。Slf4j帶有幾個橋接模塊,能夠重定向Log4j,JCL和java.util.logging中的Api到Slf4j。

遺留的api橋接方案

jar包名 做用
log4j-over-slf4j-version.jar 將Log4j重定向到Slf4j
jcl-over-slf4j-version.jar 將Commons Logging裏的Simple Logger重定向到slf4j
jul-to-slf4j-version.jar 將Java Util Logging重定向到Slf4j

橋接方式參見下圖

pic3

使用Slf4j橋接注意事項

  • 在使用Slf4j橋接時要注意避免造成死循環,在項目依賴的jar包中不要存在如下狀況。
多個日誌jar包造成死循環的條件 產生緣由
log4j-over-slf4j.jar和slf4j-log4j12.jar同時存在 因爲slf4j-log4j12.jar的存在會將全部日誌調用委託給log4j。但因爲同時因爲log4j-over-slf4j.jar的存在,會將全部對log4j api的調用委託給相應等值的slf4j,因此log4j-over-slf4j.jar和slf4j-log4j12.jar同時存在會造成死循環
jul-to-slf4j.jar和slf4j-jdk14.jar同時存在 因爲slf4j-jdk14.jar的存在會將全部日誌調用委託給jdk的log。但因爲同時jul-to-slf4j.jar的存在,會將全部對jul api的調用委託給相應等值的slf4j,因此jul-to-slf4j.jar和slf4j-jdk14.jar同時存在會造成死循環

五、遺留api橋接死循環源碼分析源碼

這裏以項目中集成log4j-over-slf4j與slf4j-log4j12爲例,其它組合造成死循環原理相相似。

示例代碼參見:https://github.com/chlsmile/slf4j-Infinite-loop-demo

程序入口類同上

源碼分析

基本步驟同上,調用鏈路LoggerFactory.getLogger()>LoggerFactory.getILoggerFactory()> LoggerFactory.performInitialization()>LoggerFactory.bind()

  • 1)LoggerFactory.bind()方法內部調用StaticLoggerBinder.getSingleton()獲取StaticLoggerBinder實例
    slf4j-Infinite-loop-1
  • 2)StaticLoggerBinder調用構造方法內部調用Log4jLoggerFactory構造方法建立ILoggerFactory
    slf4j-Infinite-loop-2
  • 3)Log4jLoggerFactory加載內部static代碼塊,校驗出classpath下存在org.apache.log4j.Log4jLoggerFactory,拋出異常
    slf4j-Infinite-loop-3

4、排除第三方包的日誌依賴

在實際使用過程當中,項目會根據須要引入一些第三方組件,例如經常使用的Spring,而Spring自己的日誌實現使用了Commons Logging,咱們又想使用Slf4j+Loback組合,這時候須要在項目中將Commons Logging排除掉,一般會用到如下3種方案,3種方案各有利弊,能夠根據項目的實際狀況選擇最適合本身項目的解決方案。

方案一:maven的exclusion

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <exclusions>
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
        </exclusion>
    </exclusions>
    <version>${springframework.version}</version>
</dependency>

這種方案優勢是exclusion是maven原生提供的,不足之處是若是有多個組件都依賴了commons-logging,則須要在不少處增長,使用起來不太方便

方案二:聲明commons-logging的scope爲provided

<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.1.1</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>1.8.0-beta2</version>
</dependency>

這種方案在調試代碼時仍是有可能致使IDE將commons-logging放置在classpath下,從而致使程序運行時出現異常

方案三:在maven私服中增長虛擬版本號

在maven私服中增長相似於99.0-does-not-exist這種虛擬的版本號

<dependency>    
    <groupId>commons-logging</groupId>    
    <artifactId>commons-logging</artifactId>    
    <version>99.0-does-not-exist</version>    
</dependency> 
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>1.8.0-beta2</version>
</dependency>

這種方案好處是聲明方式比較簡單,用IDE調試代碼時也不會出現問題,不足之處是99.0-does-not-exist這種版本是maven中央倉庫中是不存在的,須要發佈到本身的maven私服中。

總結

因爲歷史緣由JDK自身提供的Log組件出現的較晚,致使Jdk提供Log組件時第三方社區的日誌組件已經比較穩定成熟。通過多年的發展Slf4j+Logback與組合,Commons Logging與Log4j組合兩大陣營已經基本成爲了Java項目開發的標準,建議在新的項目開發中從這兩種方案中選擇適合本身項目的組合方案。

相關文章
相關標籤/搜索