title: 【Java深刻學習系列】三. 那些年咱們用過的日誌框架
date: 2016-10-16 15:32:50html
目前常見的Java日誌框架和facades(中文彷佛不太好翻譯)有一下幾種:java
其中,①-③爲同一個做者(Ceki)所寫。④被不少開源項目所用,⑤是Java原生庫(如下用j.u.l簡寫來代替),可是在Java 1.4中才被引入。數據庫
這麼多的日誌庫,咱們該如何選擇呢,我認爲,這並不是一道非此即彼的選擇題,可是在瞭解它們的歷史淵源和優劣以及相互關係的基礎上才能更好地適配本身的項目。apache
下面我將上述這些框架串起來說一下,若有疏漏請見諒。segmentfault
在上古時期,Java打日誌依賴System.out.println()
, System.err.println()
或者e.printStackTrace()
。Debug日誌被寫入STDOUT
流,錯誤日誌被寫入STDERR
流。api
這種方式目前小腳本中也依然使用普遍。可是在生產環境或大的項目中,Debug日誌一般被重定向到/dev/null中: >/dev/null
, 錯誤日誌被重定向到本地文件中: 2>stderr.log
。看起來很完美,是嗎?實則否則,這樣打日誌有一個很是大的缺陷:沒法可定製化。bash
具體來說,沒有一個相似開關的東東來切換是否打印Debug日誌,當咱們定位問題時須要輸出Debug日誌到文件去查看,而不是到/dev/null
裏,是嗎?日誌沒法定製化,咱們只能硬編碼到代碼裏,不須要時再註釋掉相關代碼,從新編譯。app
還有一些缺陷,好比:沒法更細粒度地輸出日誌,換句話說,缺乏當前成熟的日誌框架常見的LOG LEVEL控制。框架
而Java自己也沒有提供相應的Library,在這樣惡劣的境況下,Log4j勇敢地站了出來,拯救勞苦大衆。jvm
Log4j能夠說是一個里程碑式的框架,它提出的一些基本理念,深深地影響了後來者,直至今天,這些理念也依然在被普遍使用:
咱們來看下維基百科對Logger
的定義:
A Logger is an object that allows the application to log without regard to where the output is sent/stored. The application logs a message by passing an object or an object and an exception with an optional severity level to the logger object under a given a name/identifier.
Logger是一個容許應用記錄日誌的對象,開發者沒必要需考慮輸出位置。應用可將具體須要打印的信息經過一個Object傳遞。每一個Logger互相獨立,經過名字或標識符來區分。
每一個appender可獨立配置記錄日誌的設備,能夠是文件、數據庫、消息系統等。
每一個打印日誌均可以單獨制定日誌級別。外部經過配置文件來控制輸出級別,不一樣的輸出級別打印不一樣的日誌信息。
J.U.L
姍姍來遲後來,Sun公司開始意識到JDK須要一個記錄日誌的特性。受Log4j的啓發,Sun在Java1.4版本中引入了一個新的API, 叫java.util.logging
, 可是,j.u.l
功能遠不如Log4j完善,若是開發者要使用它,就意味着須要本身寫Appenders
(Sun稱它爲Handlers
),並且,只有兩個Handlers
可被使用:Console
和File
,這就意味着,開發者只能將日誌寫入Console和文件。
如前面所述,j.u.l
在Java 1.4才被引入,在這以前,並無官方的日誌庫供開發者使用。因而便有了不少日誌相關的"輪子"。我想這應該是當前會有如此多日誌框架的一個很重要的緣由。
回顧歷史,一方面,在Java 1.4以前,第三方日誌庫已經被普遍使用了,佔得了先機。另外一方面,j.u.l
在被引入時性能和可用性都不好,直到1.5甚至之後纔有了顯著提高。
因爲項目的日誌打印必然依賴以上兩個框架中至少一個,不管是j.u.l
仍是log4j
,開發者必須去兩個都配置。這時候,Apache的commons-logging
出現了。本質上來說,commons-logging
並不是一個日誌打印框架,而是一個API bridge, 它起到一個鏈接和溝通的做用,開發者可使用它來兼容logging frameworks(j.u.l
和log4j
)。有了它,第三方庫就可使用commons-logging
來作一箇中間層,去靈活選擇j.u.l
或者log4j
,而沒必要強加依賴。
然而commons-logging
對j.u.l
和log4j
的配置問題兼容得並很差,更糟糕的是,使用commons-logging
可能會遇到類加載問題,致使NoClassDefFoundError
的錯誤出現。
最終,log4j
的創始人Ceki發起了另外一個項目,這即是大名鼎鼎的SLF4j
日誌框架,該框架能夠當作是log4j
的升級版。須要說明的是,log4j 2.0已經被加入Apache基金會,過去幾年已經被大幅改善,社區活躍度也很是高,藉助開源社區的力量,log4j 2.0目前被加入愈來愈多得現代化特性,必定程度上,甚至超越了log4j
的升級版logback
(稍後介紹),關於log4j 2.0的新特性,請參見這篇文章:THE NEW LOG4J 2.0
據slf4j
的做者Ceki說,首先,slf4j是不只僅是一個logging framework, 並且一個logging facdes, 藉助slf4j
的log4j adapter, 開發者從slf4j切換到log4j不須要額外改動一行代碼,只須要從CLASS_PATH中排除掉slf4j-log4j12.jar。若是想從log4j
遷移到logback
, 在CLASS_PATH添加slf4j-log4j12.jar, 並將log4j.properties轉換爲logback.xml便可,這裏有一個在線工具能夠自動完成轉換: logback.xml translator。
slf4j提供了很大的靈活度,開發者能夠藉助它去靈活選擇底層的日誌框架。好比,當下更多的開發者比較傾向於使用log4j的升級版logback,由於它具備較log4j更多更好的特性:
須要說明的是,logback是slf4j接口的一套具體實現,又是同一個做者,於是保證了其和log4j相近的使用方式,也具備slf4j的所有特性。
此外,對於一些大型框架及服務的開發者,須要考慮客戶端用戶的體驗。好比jstorm, 你不能只考慮本身的喜愛,或許有人偏好使用slf4j
開發jstorm topology, 而另外一些人喜歡用logback。這種狀況下,你應該使用slf4j
,把最終logging framework的選擇權留給用戶。
最後,除了slf4j
比j.u.l
或者log4j
更好用,還有一個選擇slf4j
的現實緣由:Java圈的很是多開發者更鐘情於slf4j
做爲他們的logging API, 隨大流有時候能少不少沒必要要的麻煩。
slf4j除了包含該log4j的所有特性外,還提供了parameterized logging特性。這個特性很是有用,它容許開發者在打印日誌時藉助{}
來實現參數化打印:
logger.debug("The attribute value is {}", fooIns.getAttribute());
logback複用了slf4j的API,這意味着使用logback其實是在使用slf4j的API,不難看出,logback一樣支持parameterized logging特性。
以上日誌框架,有些是爲了解決現有框架的不足,有些是功能的擴展升級,有些是從頭至尾從新寫的,根據各自出現前後次序,能夠將它們放在同一時間線上:
注意箭頭僅表明時間走向,分支不具備fork的含義。
slf4j的使用有兩種方式,一種是混合綁定(concrete-bindings), 另外一種是橋接遺產(bridging-legacy).
concrete-bindings模式指在新項目中 即開發者直接使用sl4j的api來打印日誌, 而底層綁定任意一種日誌框架,如logback, log4j, j.u.l等.
混合綁定根據實現原理,基本上有兩種形式, 分別爲有適配器(adapter)的綁定和無適配器的綁定.
有適配器的混合綁定是指底層沒有實現slf4j的接口,而是經過適配器直接調用底層日誌框架的Logger, 無適配器的綁定不須要調用其它日誌框架的Logger, 其自己就實現了slf4j的所有接口.
幾個混合綁定的包分別是:
以上幾種綁定能夠無縫切換, 不須要改動內部代碼. 不管哪一種綁定,均依賴slf4j-api.jar.
此外, 適配器綁定須要一種具體的日誌框架, 如log4j綁定slf4j-log4j12-1.7.21.jar依賴log4j.jar, j.u.l綁定slf4j-jdk14-1.7.21.jar依賴j.u.l(java runtime提供); 無適配器的直接實現, logback-classic依賴logback-core提供底層功能, slf4j-simple則不依賴其它庫.
以上四種綁定的示例圖以下:
下面來分析兩個典型綁定log4j和logback的用法.
①log4j適配器綁定(slf4j-log4j12)
配置:
<!--pom.xml--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency>
注意: 添加上述適配器綁定配置後會自動拉下來兩個依賴庫, 分別是slf4j-api-1.7.21.jar和log4j-1.2.17.jar
基本邏輯: 用戶層 <- 中間層 <- 底層基礎日誌框架層
org.slf4j.impl.Log4jLoggerFactory <- StaticLoggerBinder.getSingleton().getLoggerFactory()<- org.sl4j.impl.StaticLoggerBinder.getSingleton().getLoggerFactory() <- 具體的日誌框庫的Logger
其中org.slf4j.impl.Log4jLoggerFactory在應用層調用, StaticLoggerBinder在中間層實現, 獲取具體的日誌框庫的Logger
綁定實例圖以下:
應用層(slf4j-api-1.7.21.jar)
用戶調用org.slf4j.impl.Log4jLoggerFactory.getLogger獲取底層具體的Logger:
// Foo.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Foo { private final static Logger logger = LoggerFactory.getLogger(CommuteNaviInfoParser.class); public static void main() { logger.info("info:{}..", "hello, sl4j"); } }
適配層(slf4j-log4j12-1.7.21.jar)
由應用層org.slf4j.impl.Log4jLoggerFactory.getLogger內部建立適配層的StaticLoggerBinder:
public static Logger getLogger(Class<?> clazz) { return StaticLoggerBinder.getSingleton().getLoggerFactory(); }
接下來直接由StaticLoggerBinder獲取具體的Logger:
private StaticLoggerBinder() { loggerFactory = new Log4jLoggerFactory(); } public Log4jLoggerFactory() { // force log4j to initialize org.apache.log4j.LogManager.getRootLogger(); }
注意, 各個StaticLoggerBinder均在適配層實現, 放在org.slf4j.impl中.
② slf4j綁定到logback-classic上
配置:
<!--pom.xml--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.7</version> </dependency>
注意: 添加上述適配器綁定配置後會自動拉下來兩個依賴庫, 分別是slf4j-api-1.7.21.jar和logback-core-1.0.13.jar
logback-classic沒有適配器層, 而是在logback-classic-1.0.13.jar的ch.qos.logback.classic.Logger
直接實現了slf4j的org.slf4j.Logger
, 並強依賴ch.qos.logback.core
中的大量基礎類:
import org.slf4j.LoggerFactory; import org.slf4j.Marker; import org.slf4j.spi.LocationAwareLogger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.classic.util.LoggerNameUtil; import ch.qos.logback.core.Appender; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.spi.AppenderAttachable; import ch.qos.logback.core.spi.AppenderAttachableImpl; import ch.qos.logback.core.spi.FilterReply; public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {}
綁定示例圖:
橋接遺產用法主要針對歷史遺留項目, 不管是用log4j寫的, j.c.l寫的,仍是j.u.l寫的, 均可以在不改動代碼的狀況下具備另一種日誌框架的能力.
好比,你的項目使用java提供的原生日誌庫j.u.l寫的, 使用slf4j的bridging-legacy模式,即可在不改動一行代碼的狀況下瞬間具備log4j的所有特性.
說得更直白一些,就是你的項目代碼多是5年前寫的, 當時因爲沒得選擇, 用了一個比較垃圾的日誌框架, 有各類缺陷和問題, 如不能按天存儲, 不能控制大小, 支持的appender不多, 沒法存入數據庫等. 你很想對這個已完工並在線上運行的項目進行改造, 顯然, 直接改代碼, 把舊的日誌框架替換掉是不現實的, 由於頗有可能引入不可預期的bug.
那麼,如何在不修改代碼的前提下, 替換掉舊的日誌框架,引入更優秀且成熟的日誌框架如如log4j和logback呢? slf4j的bridging-legacy模式即是爲了解決這個痛點.
slf4j以slf4j-api爲中間層, 將上層舊日誌框架的消息轉發到底層綁定的新日誌框架上.
基於不一樣的底層框架,以SLF4J做爲中轉層,有以下幾種組合用法:
上述facade將slf4j-api.jar綁定到底層基礎日誌庫j.u.l(jvm runtime)上. slf4j-api和底層日誌庫的Logger經過適配器鏈接.
舉例說明上述facade的使用, 以便於你們理解.
假如我有一個已完成的使用了舊日誌框架commons-loggings的項目,如今想把它替換成log4j以得到更多更好的特性.
項目的maven舊配置以下:
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
項目代碼:
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Created by xialeizhou on 16/9/20. */ public class MainTest { private static Log logger = LogFactory.getLog(MainTest.class); public static void main(String[] args) throws InterruptedException { logger.info("hello,world"); } }
項目打印的基於commons-logging的日誌顯示在console上,具體以下:
十月 23, 2016 6:52:00 下午 MainTest main 信息: hello,world
下面咱們對項目改造, 將commongs-logging框架的日誌轉發到log4j上. 改造很簡單, 咱們將commongs-logging依賴刪除, 替換爲相應的facade(此處爲jcl-over-slf4j.jar), 並在facade下面掛一個5.1的混合綁定便可.
具體來說, 將commons-logging.jar
替換成jcl-over-slf4j.jar
, 並加入適配器slf4j-log412.jar(注意, 加入slf4j-log412.jar後會自動pull下來另外兩個jar包), 因此實際最終只需添加facadejcl-over-slf4j.jar
和5.1節混合綁定中相同的jar包slf4j-log412.jar
便可.
改造後的maven配置:
<!--facade--> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.21</version> </dependency> <!--binding--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency>
如今, 咱們的舊項目在沒有改一行代碼的狀況下具備了log4j的所有特性, 下面進行測試.
在resources/
下新建一個log4j.properties
文件, 對commongs-logging庫的日誌輸出進行定製化:
# Root logger option log4j.rootLogger=INFO, stdout, fout # Redirect log messages to console log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.Threshold = INFO log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n # add a FileAppender to the logger fout log4j.appender.fout=org.apache.log4j.FileAppender # create a log file log4j.appender.fout.File=royce-testing.log log4j.appender.fout.layout=org.apache.log4j.PatternLayout # use a more detailed message pattern log4j.appender.fout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
從新編譯運行, console輸出變爲:
2016-10-23 19:26:15 INFO MainTest:11 - hello,world
同時在當前目錄生成了一個日誌文件:
% cat royce-testing.log INFO 2016-10-23 19:26:15,341 0 MainTest [main] hello,world
可見, 基於facade的日誌框架橋接已經生效, 咱們再不改動代碼的前提下,讓commons-logging日誌框架具備了log4j12的所有特性.