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以來的官方日志實現。
看了上面的介紹是否會覺得比較混亂,這些日志框架之間有什么異同,都是由誰在維護,在項目中應該如何選擇日志框架,應該如何使用? 下文會逐一介紹。
Java常用日志框架歷史
-
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的發展趨勢更好: -
Apache眼看有被Logback反超的勢頭,於2012-07重寫了Log4j 1.x,成立了新的項目Log4j 2, Log4j 2具有Logback的所有特性。
java常用日志框架關系
-
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那樣只提供部分免費文檔而需要用戶去購買付費文檔。
Slf4j用法
Slf4j與其它日志組件的關系說明
- Slf4j的設計思想比較簡潔,使用了Facade設計模式,Slf4j本身只提供了一個slf4j-api-version.jar包,這個jar中主要是日志的抽象接口,jar中本身並沒有對抽象出來的接口做實現。
- 對於不同的日志實現方案(例如Logback,Log4j...),封裝出不同的橋接組件(例如logback-classic-version.jar,slf4j-log4j12-version.jar),這樣使用過程中可以靈活的選取自己項目里的日志實現。
Slf4j與其它日志組件調用關系圖
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源碼分析
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,不加入任何實現包
示例代碼
pom核心配置如下
<dependencies>
<!--只有slf4j-api依賴-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.13</version>
</dependency>
</dependencies>
程序入口類如下
源碼追蹤分析
-
1)調用LoggerFactory的getLogger()方法創建Logger
-
2)調用LoggerFactory的getILoggerFactory方法來創建ILoggerFactory
-
3)調用LoggerFactory的performInitialization方法來進行初始化
-
4)調用LoggerFactory的bind()方法
-
5)調用LoggerFactory的findPossibleStaticLoggerBinderPathSet()方法獲取StaticLoggerBinderPath集合
-
6)調用LoggerFactory的reportMultipleBindingAmbiguity()方法,記錄綁定的StaticLoggerBinder信息
-
7)LoggerFactory的reportMultipleBindingAmbiguity()方法
-
8)LoggerFactory的bind()方法找不到StaticLoggerBinder,拋出NoClassDefFoundError異常
-
9)LoggerFactory的bind()方法捕獲NoClassDefFoundError異常,匹配到StaticLoggerBinder關鍵詞記錄信息到控制台
-
10)LoggerFactory的performInitialization()方法內部調用bind()方法結束
-
11)LoggerFactory的getLogger()方法內部getILoggerFactory()方法調用完成,創建出NOPLoggerFactory,然后由NOPLoggerFactory調用內部的getLogger()方法,創建出NOPLogger
-
12)App類內部的logger實際為NOPLogger,調用logger.info()方法實際調用的是NOPLogger的info方法
Slf4j調用過程源碼分析,加入slf4j-api-version.jar,與Logback組件
Slf4j作為門面采用Logback作為實現或者采用其它上面提到過的組件作為實現類似,這里只分析采用Logback組件作為實現
示例代碼
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集合
-
6)調用LoggerFactory的bind()方法的staticLoggerBinderPathSet集合對象賦值
-
7)在LoggerFactory的bind()方法中調用loback包下的StaticLoggerBinder創建單例對象
-
8)在LoggerFactory的bind()方法中調用reportActualBinding()記錄日志加載信息
-
9)LoggerFactory中INITIALIZATION_STATE的值為SUCCESSFUL_INITIALIZATION,調用StaticLoggerBinder的單例對象獲取ILoggerFactory
-
10)此時LoggerFactory中的getLogger()方法中獲取到的ILoggerFactory實際上是logback jar下的LoggerContext
-
11)此時LoggerFactory調用getLogger()方法獲取到的Logger實際上是logback jar下的Logger
Slf4j調用過程源碼分析,加入slf4j-api-version.jar,同時加入多種日志實現組件
在項目中如果用slf4j-api作為日志門面,有多個日志實現組件同時存在,例如同時存在Logback,slf4j-log4j12,slf4j-jdk14,slf4j-jcl四種實現,則在項目實際運行中,Slf4j的綁定選擇綁定方式將有Jvm確定,並且是隨機的,這樣會和預期不符,實際使用過程中需要避免這種情況。
示例代碼
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
-
2)此時LoggerFactory的bind()方法內部調用reportMultipleBindingAmbiguity()方法,給出警告信息classpath下同時存在多個StaticLoggerBinder,JVM會隨機選擇一個StaticLoggerBinder
使用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 |
橋接方式參見下圖
使用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為例,其它組合形成死循環原理相類似。
示例代碼
程序入口類同上
源碼追蹤分析
基本步驟同上,調用鏈路LoggerFactory.getLogger()>LoggerFactory.getILoggerFactory()> LoggerFactory.performInitialization()>LoggerFactory.bind()
-
1)LoggerFactory.bind()方法內部調用StaticLoggerBinder.getSingleton()獲取StaticLoggerBinder實例
-
2)StaticLoggerBinder調用構造方法內部調用Log4jLoggerFactory構造方法創建ILoggerFactory
-
3)Log4jLoggerFactory加載內部static代碼塊,校驗出classpath下存在org.apache.log4j.Log4jLoggerFactory,拋出異常
排除掉項目中依賴的第三方包的日志依賴
在實際使用過程中,項目會根據需要引入一些第三方組件,例如常用的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,則需要在很多處增加
,使用起來不太方便
方案二 在maven聲明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私服中增加類似於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項目開發的標准,建議在新的項目開發中從這兩種方案中選擇適合自己項目的組合方案。