點贊再看,養成習慣,公衆號搜一搜【一角錢技術】關注更多原創技術文章。本文 GitHub org_hejianhui/JavaStudy 已收錄,有個人系列文章。html
對於一個應用程序來講日誌記錄是必不可少的一部分。線上問題追蹤,基於日誌的業務邏輯統計分析等都離不日誌。java領域存在多種日誌框架,目前經常使用的日誌框架包括Log4j 1,Log4j 2,Commons Logging,Slf4j,Logback,Jul。可是在咱們的系統裏面到底該怎麼使用日誌框架?還在爲弄不清commons-logging.jar、log4j.jar、sl4j-api.jar等日誌框架之間複雜的關係而感到煩惱嗎?還在爲如何統一系統的日誌輸出而感到不知所措嘛?好比,要更改Spring的日誌輸出爲Log4j 2,殊不知該引哪些jar包,只知道去百度一下所謂的博客,照着人家複製,卻沒法弄懂其中的原理?本文將弄懂其中的原理,只要你靜下心看本文,你就能爲所欲爲更改你係統裏的日誌框架,統一日誌輸出!java
看到這麼多日誌框架是否會以爲比較混亂,這些日誌框架之間有什麼異同,都是由誰在維護,在項目中應該如何選擇日誌框架,應該如何使用? 不要急,咱們先把這些術語概念先有個印象。讓咱們先了解一它們的發展歷史。git
大神Ceki github
早年,你工做的時候,在日誌裏使用了log4j框架來輸出,因而你代碼是這麼寫的spring
import org.apache.log4j.Logger;
//省略...
Logger logger = Logger.getLogger(Test.class);
logger.trace("trace");
//省略...
複製代碼
可是,歲月流逝,sun公司對於log4j的出現心裏隱隱表示嫉妒。因而在jdk1.4版本後,增長了一個包爲java.util.logging
,簡稱爲jul,用以對抗log4j。因而,你的領導要你把日誌框架改成jul,這時候你只能一行行的將log4j的api改成jul的api,以下所示:apache
import java.util.logging.Logger;
//省略...
Logger loggger = Logger.getLogger(Test.class.getName());
logger.finest("finest");
//省略...
複製代碼
能夠看出,api徹底是不一樣的。那有沒有辦法,將這些api抽象出接口,這樣之後調用的時候,就調用這些接口就行了呢?設計模式
這個時候jcl(Jakarta Commons Logging)出現了,說jcl可能你們有點陌生,講commons-logging-xx.jar組件,你們總有印象吧。JCL 只提供 log 接口,具體的實現則在運行時動態尋找。這樣一來組件開發者只須要針對 JCL 接口開發,而調用組件的應用程序則能夠在運行時搭配本身喜愛的日誌實踐工具。JCL能夠實現的集成方案以下圖所示 jcl默認的配置:若是能找到Log4j 則默認使用log4j 實現,若是沒有則使用jul(jdk自帶的) 實現,再沒有則使用jcl內部提供的SimpleLog 實現。api
因而,你在代碼裏變成這麼寫了數組
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
//省略...
Log log =LogFactory.getLog(Test.class);
log.trace('trace');
//省略...
複製代碼
至於這個Log具體的實現類,JCL會在ClassLoader中進行查找。這麼作,有三個缺點:安全
因而log4j的做者(Ceki)以爲jcl很差用,本身又寫了一個新的接口api,那麼就是slf4j。
咱們在代碼中須要寫日誌,變成下面這麼寫
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//省略...
Logger logger = LoggerFactory.getLogger(Test.class);
//省略...
logger.info("info");
複製代碼
在代碼中,並不會出現具體日誌框架的api。程序根據classpath中的橋接器類型,和日誌框架類型,判斷出logger.info應該以什麼框架輸出!注意了,若是classpath中不當心引了兩個橋接器,那會直接報錯的!
所以,在阿里的開發手冊上纔有這麼一條
強制:應用中不可直接使用日誌系統(log4j、logback)中的 API ,而應依賴使用日誌框架 SLF4J 中的 API 。使用門面模式的日誌框架,有利於維護和各個類的日誌處理方式的統一。
如圖所示,應用調了sl4j-api,即日誌門面接口。日誌門面接口自己一般並無實際的日誌輸出能力,它底層仍是須要去調用具體的日誌框架API的,也就是實際上它須要跟具體的日誌框架結合使用。因爲具體日誌框架比較多,並且互相也大都不兼容,日誌門面接口要想實現與任意日誌框架結合可能須要對應的橋接器,上圖紅框中的組件便是對應的各類橋接器!
jar包名 | 說明 |
---|---|
slf4j-log4j12-1.7.30.jar | Log4j1.2版本的橋接器,你須要將Log4j.jar加入Classpath。 |
slf4j-jdk14-1.7.30.jar | java.util.logging的橋接器,Jdk原生日誌框架。 |
slf4j-nop-1.7.30.jar | NOP橋接器,默默丟棄一切日誌。 |
slf4j-simple-1.7.30.jar | 一個簡單實現的橋接器,該實現輸出全部事件到System.err. 只有Info以及高於該級別的消息被打印,在小型應用中它也許是有用的。 |
slf4j-jcl-1.7.30.jar | Jakarta Commons Logging 的橋接器. 這個橋接器將Slf4j全部日誌委派給Jcl。 |
logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar) | Slf4j的原生實現,Logback直接實現了Slf4j的接口,所以使用Slf4j與Logback的結合使用也意味更小的內存與計算開銷 |
具體的介入方式參考下圖
類與接口 | 用途 |
---|---|
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-api依賴-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
複製代碼
package com.niuh;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
final static Logger logger = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
logger.info("Hello World");
}
}
複製代碼
Slf4j做爲門面採用Logback做爲實現或者採用其它上面提到過的組件做爲實現相似,這裏只分析採用Logback組件做爲實現
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!--logback-classic依賴logback-core,會自動級聯引入-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
複製代碼
同上
在項目中若是用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.30</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.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jcl</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>
複製代碼
同上
在實際環境中咱們常常會遇到不一樣的組件使用的日誌框架不一樣的狀況,例如Spring Framework使用的是日誌組件是Commons Logging,XSocket依賴的則是Java Util Logging。當咱們在同一項目中使用不一樣的組件時應該若是解決不一樣組件依賴的日誌組件不一致的狀況呢?如今咱們須要統一日誌方案,統一使用Slf4j,把他們的日誌輸出重定向到Slf4j,而後Slf4j又會根據綁定器把日誌交給具體的日誌實現工具。Slf4j帶有幾個橋接模塊,能夠重定向Log4j,JCL和java.util.logging中的Api到Slf4j。
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橋接時要注意避免造成死循環,在項目依賴的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同時存在會造成死循環 |
前面介紹過門面型的日誌框架主要就兩個JCL(Commons Logging)和Slf4j,咱們來簡單瞭解下它們的區別:
JCL(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中進行綁定。
一個項目,一個模塊用log4j,另外一個模塊用slf4j+log4j2,如何統一輸出?
其實在某些中小型公司,這種狀況很常見。我曾經見過某公司的項目,由於研發不懂底層的日誌原理,日誌文件裏頭既有log4j.properties,又有log4j2.xml,各類API混用,慘不忍睹!
還有人用着jul的API,而後拿着log4j.properties,跑來問我,爲何配置不生效!簡直是一言難盡!
OK,回到咱們的問題,如何統一輸出!OK,這裏就要用上slf4j的適配器,slf4j提供了各類各樣的適配器,用來將某種日誌框架委託給slf4j。其最明顯的集成工做方式有以下: 進行選擇填空,將咱們的案例裏的條件填入,根據題意應該選log4j-over-slf4j適配器,因而就變成下面這張圖
就能夠實現日誌統一爲log4j2來輸出!
PS: 根據適配器工做原理的不一樣,被適配的日誌框架並非必定要刪除!以上圖爲例,log4j這個日誌框架刪不刪均可以,你只要能保證log4j的加載順序在log4j-over-slf4j後便可。由於log4j-over-slf4j這個適配器的工做原理是,內部提供了和log4j如出一轍的api接口,所以你在程序中調用log4j的api的時候,你必須想辦法讓其走適配器的api。若是你刪了log4j這個框架,那你程序裏確定是走log4j-over-slf4j這個組件裏的api。若是不刪log4j,只要保證其在classpth裏的順序比log4j前便可!
如何讓Spring以log4j2的形式輸出?
Spring默認使用的是jcl輸出日誌,因爲你此時並無引入Log4j的日誌框架,jcl會以jul作爲日誌框架。此時集成圖以下 而你的應用中,採用了slf4j+log4j-core,即log4j2進行日誌記錄,那麼此時集成圖以下 那咱們如今須要讓Spring以log4j2的形式輸出?怎麼辦?
OK,第一種方案,走jcl-over-slf4j適配器,此時集成圖就變成下面這樣了 在這種方案下,spring框架中遇到日誌輸出的語句,就會如上圖紅線流程同樣,最終以log4J2的形式輸出!
OK,有第二種方案麼?
有,走jul-to-slf4j適配器,此時集成圖以下
PS: 這種狀況下,記得在代碼中執行
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
複製代碼
這樣jul-to-slf4j適配器才能正常工做,詳情能夠查詢該適配器工做原理。
假設,咱們在應用中調用了sl4j-api,可是呢,你引了四個jar包,slf4j-api-xx.jar,slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar,因而你就會出現以下尷尬的場面
如上圖所示,在這種狀況下,你調用了slf4j-api,就會陷入死循環中!slf4j-api去調了slf4j-log4j12,slf4j-log4j12又去調用了log4j,log4j去調用了log4j-over-slf4j。最終,log4j-over-slf4j又調了slf4j-api,陷入死循環!
構建spring4項目,採用java+註解的方式快速構建,pom中只引入spring-context包
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.21.RELEASE</version>
</dependency>
</dependencies>
複製代碼
運行下面的代碼,能夠看到有日誌輸出
public class MainClass {
public static void main(String[] args) {
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
}
}
複製代碼
找到打印日誌的地方,debug模式下,查看輸出日誌的Log是什麼log 能夠看出是jdk14Logger,這個在JCL中說過,這個指的是JUL,也就是說在默認spring日誌體系下,採用的是JUL,
接下來,咱們按照以前的方法引入log4j,debug運行上面的程序,再次查看日誌類型
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
複製代碼
額,此次在增長log4j jar包和配置文件的狀況下,spring4有使用了log4j,這麼像JCL呢,木錯,讓咱們在idea中打開spring4的日誌依賴結構: common-logging 這不就是JCL使用到的包嗎,能夠看出,Spring4使用的是原生的JCL,因此在有log4j的時候使用log4j打印日誌,沒有的時候使用JUL打印日誌。
依賴結構圖:
大致結構沒變,只是原來common-logging ,換成了spring-jcl,看名字就知道是spring自造的包,jcl,更是標註了,它使用的是JCL日誌體系。
咱們仍是經過看源碼來驗證,咱們只用debug找到spring內部一個Log,看看他的產生方式和類型。此次我給你們找了AbstractApplicationContext裏面找到產生Log的地方
進入這個方法的getLog()中,一直深刻,找到LogAdapter中的createLog()方法 能夠看出來Spring5中對日誌的生產,不在像原生JCL中那樣使用一個數組,而後進行循環產生,這裏用到的是Switch case,這個關鍵字段logApi又是在哪一部分賦值的呢?以下所示: 咱們看到是在靜態代碼塊中賦的值,爲了驗證,咱們準備用其中提到的log4j2驗證(注意:log4j不行,由於這裏的switch沒有log4j選項),首先咱們準備log4j2.xml的配置文件
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
複製代碼
而後準備pom的依賴
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.10.0</version>
</dependency>
</dependencies>
複製代碼
運行下面的代碼
public class MainClass {
public static void main(String[] args) {
AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
}
}
複製代碼
結果有日誌打印出來了 因此,在Spring5中,依然使用的是JCL,可是不是原生的,是通過改造的JCL,默認使用的是JUL,而原生JCL中默認使用的是log4j。
若是是在一個新的項目中建議使用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的開銷,使用佔位符號,代碼也更爲簡潔
複製代碼
文章持續更新,能夠公衆號搜一搜「 一角錢技術 」第一時間閱讀, 本文 GitHub org_hejianhui/JavaStudy 已經收錄,歡迎 Star。