日誌組件是開發中最經常使用到的組件,但也是最容易被忽視的一個組件,我本身就遇到過不少次因爲Log4j報錯致使的應用沒法啓動的問題,如下作一個梳理,參考和借鑑了一些前輩的經驗,並加入了一些本身的理解,相對容易看懂一些~html
目前常見的Java日誌框架和facades(中文彷佛不太好翻譯)有一下幾種:java
一、log4j數據庫
二、logbackapache
三、SLF4Jsegmentfault
四、commons-loggingapi
五、j.u.l (即java.util.logging)bash
1-3是同一個做者(Ceki)所寫。4被不少開源項目所用,5是Java原生庫(如下用j.u.l簡寫來代替),可是在Java 1.4中才被引入。這麼多日誌庫,瞭解他們的優劣和關係才能找到一款更加適配本身項目的框架。app
以下圖,common-logging與slf4j同屬於日誌的門面 (facade),下層能夠對接具體的日誌框架層。框架
common-logging:開發者可使用它兼容j.u.l和log4j,至關於一箇中間層,須要注意的是common-logging對j.u.l和log4j的配置兼容性並無理想中那麼好,更糟糕的是,在common-logging發佈初期,使用common-logging可能會遇到類加載問題,致使maven
NoClassDefFoundError的錯誤出現;
slf4j:可以更加靈活的對接多個底層框架;
級別順序爲:DEBUG < INFO < WARN < ERROR < FATAL (制定當前日誌的重要程度);
log4j的級別規則:只輸出級別不低於設定級別的日誌信息,例:loggers級別爲INFO,則INFO、WARN、ERROR、FATAL級別的日誌信息都會輸出,而級別比INFO低的DEBUG則不會輸出;
Appender容許經過配置將日誌輸出到不一樣的地方,如控制檯(Console)、文件(Files)等,能夠根據天數或者文件大小產生新的文件,能夠以流的形式發送到其餘地方;
經常使用配置類:
org.apache.log4j.ConsoleAppender(控制檯)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(天天產生一個日誌文件)
org.apache.log4j.RollingFileAppender(文件大小到達指定尺寸的時候產生一個新的文件)org.apache.log4j.WriterAppender(將日誌信息以流格式發送到任意指定的地方)
用戶能夠根據需求格式化日誌輸出,log4j能夠在Appenders後面附加Layouts來完成;
Layouts提供四種日誌輸出格式:根據HTML樣式、自由指定樣式、包含日誌級別與信息的樣式、包含日誌時間/線程/類別等信息的樣式;
org.apache.log4j.HTMLLayout(以HTML表格形式佈局)
org.apache.log4j.PatternLayout(能夠靈活地指定佈局模式)
org.apache.log4j.SimpleLayout(包含日誌信息的級別和信息字符串)
org.apache.log4j.TTCCLayout(包含日誌產生的時間、線程、類別等信息)
在這三個類中都經過Logger.getLogger(XXX.class);獲取logger:
logger組織結構以下,父子關係經過 「.」 實現:
Loggers是被命名的實體,Logger的名稱是大小寫敏感的,而且遵循層次命名規則。命名爲「com.mobile」的logger是命名爲「com.mobile.log」的logger的父親。同理,命名爲「java」的logger是命名爲「java.util」的logger的父親,是命名爲「java.util.Vector」的祖先。
root logger位於整個logger繼承體系的最頂端,相比於普通logger它有兩個特別之處:
1)、root logger老是存在。
2)、root logger不能經過名稱獲取。
能夠經過調用Logger類的靜態方法getRootLogger獲取root logger對象。其它普通logger的實例能夠經過Logger類的另外一個靜態方法getLogger獲取。getLogger方法接受一個參數做爲logger的名字。
logger節點的繼承關係體如今level上,能夠參見下面幾個例子:
Example 1:這個例子中,只有root logger被分配了一個level值Proot,Proot會被其餘的子logger繼承:x、x.y、x.y.z
Example 2:這個例子中,全部的logger都被分配了一個level值,就不須要繼承level了
Example 3:這個例子中,root、x和x.y.z三個logger分別被分配了Proot、Px和Pxyz三個level值,x.y這個logger從它的父親那裏繼承level值;
Example 4:這個例子中,root和x兩個logger分別被分配了Proot和Px這兩個level值。x.y和x.y.z兩個logger則從離本身最近的祖先x繼承level值;
logger的additivity表示:子logger是否繼承父logger的輸出源 (Appender),即默認狀況下子logger會繼承父logger的Appender (子logger會在父logger的Appender裏輸出),當手動設置additivity flag爲false,子logger只會在本身的Appender裏輸出,不會在父Appender裏輸出。
以下圖展現:
slf4j的使用有兩種方式,一種是混合綁定(concrete-bindings), 另外一種是橋接遺產(bridging-legacy).
concrete-bindings模式指在新項目中即開發者直接使用sl4j的api來打印日誌, 而底層綁定任意一種日誌框架,如logback, log4j, j.u.l等.混合綁定根據實現原理,基本上有兩種形式, 分別爲有適配器(adapter)的綁定和無適配器的綁定.
有適配器的混合綁定是指底層沒有實現slf4j的接口,而是經過適配器直接調用底層日誌框架的Logger, 無適配器的綁定不須要調用其它日誌框架的Logger, 其自己就實現了slf4j的所有接口.
幾個混合綁定的包分別是:
slf4j-log4j12-1.7.21.jar(適配器, 綁定log4j, Logger由log4j-1.2.17.jar提供)
slf4j-jdk14-1.7.21.jar(適配器, 綁定l.u.l, Logger由JVM runtime, 即j.u.l庫提供)
logback-classic-1.0.13.jar(無適配器, slf4j的一個native實現)
slf4j-simple-1.7.21.jar(無適配器,slf4j的簡單實現, 僅打印INFO及更高級別的消息, 全部輸出所有重定向到System.err, 適合小應用)
以上幾種綁定能夠無縫切換, 不須要改動內部代碼. 不管哪一種綁定,均依賴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則不依賴其它庫.
以上四種綁定的示例圖以下:
關於適配器,正常使用slf4j從LoggerFactory.getLogger獲取logger開始,在getLogger內部會先經過StaticLoggerBinder獲取ILoggerFactory,StaticLoggerBinder則是存在具體的適配器包中的,我瞭解的一種實現是經過在適配器中的StaticLoggerBinder來綁定,舉個例子,引用這四個slf4j-api.jar, log4j-core-2.3.jar, log4j-api-2.3.jar, log4j-slf4j-impl.jar(將slf4j轉發到log4j2):
下面來分析兩個典型綁定log4j (有適配器) 和logback (無適配器) 的用法.
<!--pom.xml-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12.version}</version>
</dependency>複製代碼
注意: 添加上述適配器綁定配置後會自動拉下來兩個依賴庫, 分別是slf4j-api-1.7.21.jar和log4j-1.2.17.jar
基本邏輯: 用戶層 <- 中間層 <- 底層基礎日誌框架層
<!--pom.xml-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</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爲中間層, 將上層舊日誌框架的消息轉發到底層綁定的新日誌框架上。
舉例說明上述facade的使用, 以便理解。假如我有一個已完成的使用了舊日誌框架commons-loggings的項目,如今想把它替換成log4j以得到更多更好的特性.
項目的maven舊配置以下:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons-logging.version}</version>
</dependency>複製代碼
項目代碼:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class LogTest {
private static Log logger = LogFactory.getLog(LogTest.class);
public static void main(String[] args) throws InterruptedException {
logger.info("hello,world");
}
}複製代碼
項目打印的基於commons-logging的日誌顯示在console上,具體以下:
十一月 20, 2017 5:52:00 下午 LogTest 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和混合綁定中相同的jar包slf4j-log412.jar便可.
改造後的maven配置:
<!--facade-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${jcl-over-slf4j.version}</version>
</dependency>
<!--binding-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12.version}</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=log-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輸出變爲:
2017-11-20 19:26:15 INFO LogTest:11 - hello,world複製代碼
同時在當前目錄生成了一個日誌文件:
% cat log-testing.log
INFO 2017-11-20 19:26:15,341 0 LogTest [main] hello,world複製代碼
可見, 基於facade的日誌框架橋接已經生效, 咱們再不改動代碼的前提下,讓commons-logging日誌框架具備了log4j12的所有特性.
配置文件方式內容比較多,用獲得時候能夠詳細查閱一下相關文檔,還有log4j2相比log4j來說,性能、代碼可讀性、支持日誌參數化打印等方面都表現了很高的優越性。
下圖同一個顏色的兩行表示不可共存的包,不要在工程中引入會造成循環的這兩個包(可能不全,歡迎補充~):
當log4j.xml中Appender:additivity設置爲true 且 appender-ref配置了對應的appender 時,會出現重複打印的問題,光說很差理解,舉個例子:
log4j.xml配置以下:
testlog.java測試類中:
結果是:在root.log和logtest.log裏面分別打印了相同的日誌進去
當<logger name="logTest" additivity="false">additivity置爲false:root.log就沒打日誌了
1)、SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: See www.slf4j.org/codes.html#… for further details.
報出錯誤的地方主要是slf4j的jar包,而故障碼中「Failed to load 'org.slf4j.impl.StaticLoggerBinder'的意思則是「加載類文件org.slf4j.impl.StaticLoggerBinder時失敗」」,
Apache官方給出的解決方案是:
This error is reported when the org.slf4j.impl.StaticLoggerBinder class could not be loaded into memory. This happens when no appropriate SLF4J binding could be found on the class path. Placing one (and only one) of slf4j-nop.jar, slf4j-simple.jar, slf4j-log4j12.jar, slf4j-jdk14.jar or logback-classic.jar on the class path should solve the problem.
翻譯來就是:這個錯誤當org.slf4j.impl.StaticLoggerBinder找不到的時候會報出,當類路徑中沒有找到合適的slf4j綁定時就會發生。能夠放置以下jar包中的一個且有且一個jar包便可:slf4j-nop.jar、slf4j-simple.jar、slf4j-log4j12.jar、slf4j-jdk14.jar 或者 logback-classic.jar。
2)、multiple bindings were found on the class path.
slf4j是一個日誌門面框架,它只能同時綁定有且一個底層日誌框架,若是綁定了多個,slf4j就會有這個提示,而且會列出這些綁定的具體位置,當有多個綁定的時候,選擇你想去綁定的那個,而後刪掉其餘的,好比:你綁定了
我測試了一下,同時在slf4j-api.jar綁定了兩個適配器:
真實的綁定是slf4j-log4j12.jar這個包裏面的實現:
因此當綁定了兩個包的時候,最後選擇了哪一個包裏的實現方式,應該是按照classpath裏面的順序,這個順序應該與classLoader的加載規則有關。
其餘具體的信息可查閱:www.slf4j.org/codes.html#… for an explanation