天天看點

Java常用日志架構介紹

目錄

  • Java日志概述
  • Java常用日志架構類别
  • Java常用日志架構曆史
  • java常用日志架構關系
  • Commons Logging與Slf4j實作機制對比
    • Commons Logging實作機制
    • Slf4j實作機制
  • 項目中選擇日志架構選擇
  • Slf4j用法
    • Slf4j與其它日志元件的關系說明
    • Slf4j與其它日志元件調用關系圖
    • Slf4j與其他各種日志元件的橋接說明
  • Slf4j源碼分析
    • slf4j-api-version.jar中幾個核心類與接口
    • Slf4j調用過程源碼分析,隻加入slf4j-api-version.jar,不加入任何實作包
      • 示例代碼
      • pom核心配置如下
      • 程式入口類如下
      • 源碼追蹤分析
    • Slf4j調用過程源碼分析,加入slf4j-api-version.jar,與Logback元件
      • 程式入口類同上
    • Slf4j調用過程源碼分析,加入slf4j-api-version.jar,同時加入多種日志實作元件
  • 使用Slf4時如何橋接遺留的api
    • 遺留的api橋接方案
    • 橋接方式參見下圖
    • 使用Slf4j橋接注意事項
    • 遺留api橋接死循環源碼分析源碼
  • 排除掉項目中依賴的第三方包的日志依賴
    • 方案一 采用maven的exclusion方案
    • 方案二 在maven聲明commons-logging的scope為provided
    • 方案三 在maven私服中增加類似于99.0-does-not-exist這種虛拟的版本号
  • 總結
  • 參考連結

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

  • 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以來的官方日志實作。
看了上面的介紹是否會覺得比較混亂,這些日志架構之間有什麼異同,都是由誰在維護,在項目中應該如何選擇日志架構,應該如何使用? 下文會逐一介紹。

  • 1996年早期,歐洲安全電子市場項目組決定編寫它自己的程式跟蹤API(Tracing API)。經過不斷的完善,這個API終于成為一個十分受歡迎的Java日志軟體包,即Log4j。後來Log4j成為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 Gülcü不适應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的發展趨勢更好:

    Java常用日志架構介紹
  • 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是通過動态查找機制,在程式運作時,使用自己的ClassLoader尋找和載入本地具體的實作。詳細政策可以檢視commons-logging-*.jar包中的org.apache.commons.logging.impl.LogFactoryImpl.java檔案。由于Osgi不同的插件使用獨立的ClassLoader,Osgi的這種機制保證了插件互相獨立, 其機制限制了Commons Logging在Osgi中的正常使用。

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那樣隻提供部分免費文檔而需要使用者去購買付費文檔。

  • Slf4j的設計思想比較簡潔,使用了Facade設計模式,Slf4j本身隻提供了一個slf4j-api-version.jar包,這個jar中主要是日志的抽象接口,jar中本身并沒有對抽象出來的接口做實作。
  • 對于不同的日志實作方案(例如Logback,Log4j...),封裝出不同的橋接元件(例如logback-classic-version.jar,slf4j-log4j12-version.jar),這樣使用過程中可以靈活的選取自己項目裡的日志實作。

Java常用日志架構介紹

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的結合使用也意味更小的記憶體與計算開銷
  • 具體的接入方式參見下圖
    Java常用日志架構介紹

類與接口 用途
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中才存在此類

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

Java常用日志架構介紹

  • 1)調用LoggerFactory的getLogger()方法建立Logger
    Java常用日志架構介紹
  • 2)調用LoggerFactory的getILoggerFactory方法來建立ILoggerFactory
    Java常用日志架構介紹
  • 3)調用LoggerFactory的performInitialization方法來進行初始化
    Java常用日志架構介紹
  • 4)調用LoggerFactory的bind()方法
    Java常用日志架構介紹
  • 5)調用LoggerFactory的findPossibleStaticLoggerBinderPathSet()方法擷取StaticLoggerBinderPath集合
    Java常用日志架構介紹
  • 6)調用LoggerFactory的reportMultipleBindingAmbiguity()方法,記錄綁定的StaticLoggerBinder資訊
    Java常用日志架構介紹
  • 7)LoggerFactory的reportMultipleBindingAmbiguity()方法
    Java常用日志架構介紹
  • 8)LoggerFactory的bind()方法找不到StaticLoggerBinder,抛出NoClassDefFoundError異常
    Java常用日志架構介紹
  • 9)LoggerFactory的bind()方法捕獲NoClassDefFoundError異常,比對到StaticLoggerBinder關鍵詞記錄資訊到控制台
    Java常用日志架構介紹
  • 10)LoggerFactory的performInitialization()方法内部調用bind()方法結束
    Java常用日志架構介紹
  • 11)LoggerFactory的getLogger()方法内部getILoggerFactory()方法調用完成,建立出NOPLoggerFactory,然後由NOPLoggerFactory調用内部的getLogger()方法,建立出NOPLogger
    Java常用日志架構介紹
    Java常用日志架構介紹
    Java常用日志架構介紹
    Java常用日志架構介紹
  • 12)App類内部的logger實際為NOPLogger,調用logger.info()方法實際調用的是NOPLogger的info方法
    Java常用日志架構介紹
    Java常用日志架構介紹

Slf4j作為門面采用Logback作為實作或者采用其它上面提到過的元件作為實作類似,這裡隻分析采用Logback元件作為實作

<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)同上
  • Java常用日志架構介紹
  • 6)調用LoggerFactory的bind()方法的staticLoggerBinderPathSet集合對象指派
    Java常用日志架構介紹
  • 7)在LoggerFactory的bind()方法中調用loback包下的StaticLoggerBinder建立單例對象
    Java常用日志架構介紹
  • 8)在LoggerFactory的bind()方法中調用reportActualBinding()記錄日志加載資訊
    Java常用日志架構介紹
    Java常用日志架構介紹
  • 9)LoggerFactory中INITIALIZATION_STATE的值為SUCCESSFUL_INITIALIZATION,調用StaticLoggerBinder的單例對象擷取ILoggerFactory
    Java常用日志架構介紹
    Java常用日志架構介紹
  • 10)此時LoggerFactory中的getLogger()方法中擷取到的ILoggerFactory實際上是logback jar下的LoggerContext
    Java常用日志架構介紹
  • 11)此時LoggerFactory調用getLogger()方法擷取到的Logger實際上是logback jar下的Logger
    Java常用日志架構介紹
    Java常用日志架構介紹

在項目中如果用slf4j-api作為日志門面,有多個日志實作元件同時存在,例如同時存在Logback,slf4j-log4j12,slf4j-jdk14,slf4j-jcl四種實作,則在項目實際運作中,Slf4j的綁定選擇綁定方式将有Jvm确定,并且是随機的,這樣會和預期不符,實際使用過程中需要避免這種情況。

<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
    Java常用日志架構介紹
  • 2)此時LoggerFactory的bind()方法内部調用reportMultipleBindingAmbiguity()方法,給出警告資訊classpath下同時存在多個StaticLoggerBinder,JVM會随機選擇一個StaticLoggerBinder
    Java常用日志架構介紹

在實際環境中我們經常會遇到不同的元件使用的日志架構不同的情況,例如Spring Framework使用的是日志元件是Commons Logging,XSocket依賴的則是Java Util Logging。當我們在同一項目中使用不同的元件時應該如果解決不同元件依賴的日志元件不一緻的情況呢?現在我們需要統一日志方案,統一使用Slf4j,把他們的日志輸出重定向到Slf4j,然後Slf4j又會根據綁定器把日志交給具體的日志實作工具。Slf4j帶有幾個橋接子產品,可以重定向Log4j,JCL和java.util.logging中的Api到Slf4j。

作用
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

Java常用日志架構介紹

  • 在使用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同時存在會形成死循環

這裡以項目中內建log4j-over-slf4j與slf4j-log4j12為例,其它組合形成死循環原理相類似。

基本步驟同上,調用鍊路LoggerFactory.getLogger()>LoggerFactory.getILoggerFactory()> LoggerFactory.performInitialization()>LoggerFactory.bind()
  • 1)LoggerFactory.bind()方法内部調用StaticLoggerBinder.getSingleton()擷取StaticLoggerBinder執行個體
    Java常用日志架構介紹
  • 2)StaticLoggerBinder調用構造方法内部調用Log4jLoggerFactory構造方法建立ILoggerFactory
    Java常用日志架構介紹
  • 3)Log4jLoggerFactory加載内部static代碼塊,校驗出classpath下存在org.apache.log4j.Log4jLoggerFactory,抛出異常
    Java常用日志架構介紹

在實際使用過程中,項目會根據需要引入一些第三方元件,例如常用的Spring,而Spring本身的日志實作使用了Commons Logging,我們又想使用Slf4j+Loback組合,這時候需要在項目中将Commons Logging排除掉,通常會用到以下3種方案,3種方案各有利弊,可以根據項目的實際情況選擇最适合自己項目的解決方案。

<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,則需要在很多處增加,使用起來不太友善

<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下,進而導緻程式運作時出現異常

<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項目開發的标準,建議在新的項目開發中從這兩種方案中選擇适合自己項目的組合方案。

  • Slf4j官網
  • Slf4j使用手冊1
  • Slf4j使用手冊2
  • Logback官網
  • Commons Logging官網

繼續閱讀