文章目錄
- 前言
- 一、認識slf4j
-
- 1.1、slf4j概述
- 1.2、第三方jar包
- 1.3、切換日志架構詳略圖
- 1.4、相關注意點
- 二、實際應用
-
- 2.1、配合自身簡單日志實作(slf4j-simple)
- 2.2、配置logback日志實作
- 2.3、配置Log4j日志實作(需擴充卡)
- 2.4、配置JUL日志實作(需擴充卡)
- 2.4、添加slf4j-nop依賴(日志開關)
- 三、原理分析
-
- 3.1、初始綁定日志實作原理
- 四、橋接舊的日志實作架構
-
- 介紹橋接器
- 4.1、log4j-over-slf4j橋接器使用
-
- 解決過程
- 原理分析
- 4.2、jul、jcl橋接器
- 三個slf4j日志實作架構與橋接器不能同時使用
- 總結
- 參考資料
前言
本篇部落客要介紹現如今主流的日志門面技術
slf4j
,
Springboot
中推薦使用該日志門面技術。其他日志架構内容可見日志專欄。
所有部落格檔案目錄索引(包含日志架構系列學習):部落格目錄索引(持續更新)
一、認識slf4j
1.1、slf4j概述
官網:http://www.slf4j.org/
使用者手冊:http://www.slf4j.org/manual.html
slf4j
(Simple Logging Facade For Java):為所有的日志架構提供了一套标準、規範的API架構,主要是提供了接口,具體的實作交由對應的日志架構,例如
Log4j
、
Logback
、
Log4j2
等。其自己本身也提供了簡單的日志實作(
slf4j-simple
)。
現如今對于一般的Java項目而言,日志架構會選擇
slf4j-api
作為門面,配置上具體的實作架構,中間使用橋接器來完成橋接。
介紹其中兩個類:日志執行個體
Logger
以及
LogFactory.getLogger()
(工廠)擷取日志執行個體。
1.2、第三方jar包
想要使用
slf4j
日志門面,需要使用第三方
jar
包,下面為
pom.xml
的對應坐标:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
若是想要之後切換日志架構,最好先引入指定版本的
slf4j-api
(盡管之後引入的對應的
slf4j
日志實作架構中有對應
api
依賴),來進行統一的API管理。
1.3、切換日志架構詳略圖
我們去到slf4j官網的使用者手冊網頁即可檢視到下圖:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yY4ImMzYGO3UWZwcDOzITN0EjYxgTM0AjMjRTZkZ2N48CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
application
下面的
SLF4J API
表示slf4j的日志門面,包含三種情況:
- ①若是隻導入
日志門面沒有導入對應的日志實作架構,那麼日志功能将會是預設關閉的,不會進行日志輸出的。slf4j
- ②藍色圖裡
、Logback
、slf4j-simple
出來的比較晚就遵循了slf4j-nop
的slf4j
規範,也就是說隻要導入對應的實作就預設實作了對應的接口,來實作開發。API
- ③對于中間兩個日志實作架構
(log4j
)、slf4j-log4j12
(JUL
)由于出現的比slf4j-jdk14
早,是以就沒有遵循slf4j
的接口規範,是以無法進行直接綁定,中間需要加一個适配層(slf4j
),通過對應的擴充卡來适配具體的日志實作架構,其對應的擴充卡其實就間接的實作了Adaptation layer
的接口規範。slf4j-api
注意:在圖中對于
logback
需要引入兩個
jar
包,不過在
maven
中有一個傳遞的思想,當配置
logback-classic
時就會預設傳遞
core
資訊,是以我們隻需要引入
logback-classic
的
jar
包即可。
1.4、相關注意點
在使用slf4j日志門面的過程中,若是引入了兩個日志實作架構會報以下錯誤,并會預設實作第一個引入的日志實作:
- 這裡是同時配置
以及simple
情況logback
注意:以
pom.xml
中配置順序有關!!!
二、實際應用
2.1、配合自身簡單日志實作(slf4j-simple)
若想使用自身的日志實作架構,需要引入第三方
jar
包
slf4j-simple
(slf4j自帶實作類):
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
</dependency>
- 其中該坐标包含了對應的
的依賴,可以不用手動導入slf4j-api
。slf4j-api
-
04、slf4j(日志門面)前言一、認識slf4j二、實際應用三、原理分析四、橋接舊的日志實作架構總結參考資料
測試程式:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//擷取Logger執行個體
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、列印日志記錄
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符輸出
String name = "changlu";
int age = 20;
LOGGER.info("報錯,name:{},age:{}",name,age);
//3、列印堆棧資訊
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("報錯",e);
}
}
}
- 預設日志等級為
,能夠實作占位符輸出,并且可以在日志等級方法中傳入異常執行個體,來列印對應的日志資訊。INFO
注意點:若是我們隻使用日志門面而沒有導入指定的日志實作架構,調用
Logger
執行個體并調用日志方法會出現以下錯誤:
2.2、配置logback日志實作
引入
logback-classic
的jar包,其中包含有
slf4j-api
以及
logback-core
的依賴,是以隻需要引入該依賴即可:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
測試程式:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//擷取Logger執行個體
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、列印日志記錄
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符輸出
String name = "changlu";
int age = 20;
LOGGER.info("報錯,name:{},age:{}",name,age);
//3、列印堆棧資訊
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("報錯",e);
}
}
}
2.3、配置Log4j日志實作(需擴充卡)
①首先添加日志架構實作依賴
之前在1.3中介紹,對于
Log4j
、
JUL
這些比較早出現的日志實作架構需要有對應的适配層,在這裡我們引入對應的擴充卡
slf4j-log412
的依賴坐标:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
-
坐标中實際就包含了slf4j-log4j12
以及Log4j
依賴,是以我們添加該坐标即可。slf4j-api
②添加
log4j.properties
配置檔案
# rootLogger日志等級為trace,輸出到螢幕上
log4j.rootLogger = trace,console
# console
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern= [%-5p]%r %l %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
測試程式:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//擷取Logger執行個體
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、列印日志記錄
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符輸出
String name = "changlu";
int age = 20;
LOGGER.info("報錯,name:{},age:{}",name,age);
//3、列印堆棧資訊
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("報錯",e);
}
}
}
2.4、配置JUL日志實作(需擴充卡)
對于
slf4j
日志門面實作
JUL
日志架構需要使用是擴充卡來實作
slf4
j的日志接口,我們直接添加對應擴充卡依賴如下:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.5.6</version>
</dependency>
-
是我們JUL
自帶的日志架構,是以不需要額外引入jar包,引入jdk
坐标,其中就包含了slf4j-jdk14
的依賴,是以我們隻需要引入一個坐标即可。slf4j-api
測試程式:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
//擷取Logger執行個體
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
System.out.println(LOGGER.getName());//xyz.changlu.LogTest
//1、列印日志記錄
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
//2、占位符輸出
String name = "changlu";
int age = 20;
LOGGER.info("報錯,name:{},age:{}",name,age);
//3、列印堆棧資訊
try {
int i = 5/0;
}catch (Exception e){
LOGGER.error("報錯",e);
}
}
}
2.4、添加slf4j-nop依賴(日志開關)
當添加了
slf4j-nop
坐标後,其相當于一個日志開關,導入實作以後就不會使用任何實作架構:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.27</version>
</dependency>
測試程式:
public class LogTest {
//擷取Logger執行個體
public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
LOGGER.error("error");
LOGGER.warn("warn");
LOGGER.info("info");
LOGGER.debug("debug");
LOGGER.trace("trace");
}
}
- 預設就關閉了
的日志架構使用。slf4j
三、原理分析
3.1、初始綁定日志實作原理
在
slf4j-api
中我們通常使用下面的方法來擷取
logger
執行個體:
獲得的執行個體實際上跟我們導入
jar
包有關,那麼它是如何進行初始配置的呢?看下面源碼:
public final class LoggerFactory {
public static Logger getLogger(Class<?> clazz) {
//1、調用一個重載方法,傳入類名
Logger logger = getLogger(clazz.getName());//見2
if (DETECT_LOGGER_NAME_MISMATCH) {
Class<?> autoComputedCallingClass = Util.getCallingClass();
if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
autoComputedCallingClass.getName()));
Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
}
}
return logger;
}
//2、根據類名來擷取logger執行個體
public static Logger getLogger(String name) {
//擷取ILoggerFactory接口的實作類(接口方法是getLogger())
ILoggerFactory iLoggerFactory = getILoggerFactory();//見3
return iLoggerFactory.getLogger(name);
}
//3、擷取ILoggerFactory的執行個體
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
//執行初始化方法
performInitialization();//見4
}
}
}
...
}
//4、執行初始化操作
private final static void performInitialization() {
bind();//見5
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}
//5、綁定操作
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) {
//下面兩行比較關鍵,這行是查找可能的靜态日志執行器路徑使用Set來接收
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();//見6
//檢視set中是否超過1個路徑,若是則進行視窗輸出提示資訊
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);//見7
}
//
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
....
}
//6、這裡是查找有關org/slf4j/impl/StaticLoggerBinder.class路徑,都放置到set中傳回
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
//從類加載器中進行查找是否有org/slf4j/impl/StaticLoggerBinder.class路徑
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
//這裡依舊是根據查找到的路徑繼續往下延伸查找并添加到Set中去
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}
}
52行的查找 org/slf4j/impl/StaticLoggerBinder.class
路徑操作存放到Set中傳回。
看到上面方法6中是不是有個疑惑,查找
org/slf4j/impl/StaticLoggerBinder.class
這個相關路徑有什麼用,與我們要加載對應的日志實作有什麼關系呢?
我們在本次源碼過程中引入兩個日志實作架構
slf4j-log4j12
、
slf4j-jdk14
(這兩個都是
slf4j
為較早出現的日志設定的擴充卡),引入jar包之後,我們嘗試搜尋一下
StaticLoggerBinder
這個類:
好家夥原來
slf4j
實作的相關擴充卡的名稱都叫
StaticLoggerBinder
啊,我們繼續看下去,看下jdk14的吧(就是JUL):
該工廠類中的執行個體
loggerFactory
是擷取了一個JDK14的工廠類,那麼我們繼續看向擴充卡中内容:
重寫了
getLogger()
方法,其中執行個體化了
JUL
的
logger
執行個體,調用有參構造傳入并且建立了一個
JDK14
的擴充卡,我們再看下這個擴充卡中都做了些什麼:
好家夥其中包含了各個日志等級的方法,其中都包含了
JUL
的日志操作。
53行中調用的 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
來報告多個綁定日志實作架構
public final class LoggerFactory {
//7、來進行報告含有多個日志架構的路徑
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
//該方法判斷是否set中數量>1
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {//見8
//若是超過1個的話就會報該問題(就是我們之前1.4中的注意點報錯)
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) {
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}
//8、判斷set中的容量大小是否>1
private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
return binderPathSet.size() > 1;
}
}
該方法就是來檢測是否有多個日志實作架構導入,若是有則報出提示資訊。
四、橋接舊的日志實作架構
介紹橋接器
直接舉場景來說明:對于一些老項目直接使用的是
Log4j
或
JUL
的日志實作架構,并沒有使用到日志門面來進行管理日志架構,當項目需要疊代更新時,我們想把原先的日志實作架構切換為
logback
,此時會出現一個問題,若是我們直接将對應的日志jar包更改為
logback
,那麼項目中會出現大量報錯,因為原先引入的包是
import org.apache.log4j.Logger;
,此時就會出現問題,我們需要重新修改大量的代碼,需要耗費大量的時間與精力。
解決方案:在
slf4j
中可以使用橋接器進而讓我們不用修改一行代碼實作日志架構的切換。在slf4j中附帶了幾個橋接木塊,這些子產品對于
log4j
、
JCL
和
JUL
的
API
調用重定向(其實就是全限定名與原來的完全相同)。
下圖包含了對應的解決方案:
- 可用
替代log4j-over-slf4j.jar
Log4j
4.1、log4j-over-slf4j橋接器使用
解決過程
問題描述
模拟場景:老項目直接使用的是
org.apache.log4j.Logger
,現今項目疊代更新,需要使用
Logback
日志架構。
我們首先将
log4j
jar包移除,之後引入
logback-classic
依賴坐标,此時就會出現下方情況:
解決方案:使用橋接器 log4j-over-slf4j
引入坐标依賴:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
- 不用修改任何代碼即可替換日志架構。
原理分析
- 從導入的包來看,其中的全限定類名與原本
的一毛一樣,接着看是如何達到無縫銜接的。Log4j
同樣是下方的擷取
logger
執行個體方法:
import org.apache.log4j.Logger;
public class LogTest {
public static final Logger LOGGER = Logger.getLogger(LogTest.class);
}
檢視源碼:
//即引入的log4j-over-slf4j坐标
package org.apache.log4j;
public class Logger extends Category {
//1、
public static Logger getLogger(Class clazz) {
return getLogger(clazz.getName());//見2
}
//2、
public static Logger getLogger(String name) {
return Log4jLoggerFactory.getLogger(name);//見3
}
//4、有參構造
protected Logger(String name) {
super(name);//調用的是Category的有參構造 去5
}
}
//log4j的工廠類
class Log4jLoggerFactory {
//3、擷取logger執行個體
public static Logger getLogger(String name) {
Logger instance = (Logger)log4jLoggers.get(name);
if (instance != null) {
return instance;
} else {
//重要的點來了:注意看這個方法
Logger newInstance = new Logger(name);//回到上面的Logger類中的4方法
Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
}
//Loger類的父類
public class Category {
protected Logger slf4jLogger;
//5、有參構造
Category(String name) {
this.name = name;
//注意這個方法,LoggerFactory.getLogger()擷取的是slf4j的對應Logger
this.slf4jLogger = LoggerFactory.getLogger(name);
if (this.slf4jLogger instanceof LocationAwareLogger) {
this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
}
}
}
- 簡單來說就是
的開發者提供了一個與slf4j
的全限定類名相同的一個包,其中的方法名稱與Log4j
的都相同,在Log4j
方法中實際擷取到了getLogger()
對應的slf4j
執行個體。Logger
如下圖:
log4j
是灰色表示的是移除掉,使用
log4j-over-slf4j
4.2、jul、jcl橋接器
當使用單獨的日志實作架構想要替換成如
logback
日志架構時可使用對應的橋接器來進行替代:
<!-- jul -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
替換了之後别忘了引入對應要替換的日志實作架構。
三個slf4j日志實作架構與橋接器不能同時使用
以下的
jar
包不能同時出現:
-
(橋接器)和log4j-over-slf4j.jar
slf4j-log4j12.jar
-
和jcl-over-slf4j.jar
slf4j-jcl.jar
-
和jul-to-slf4j.jar
slf4j-jdk14.jar
為什麼不能同時出現呢?我們借第一組進行檢視:
①首先看下相應的依賴導入
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
-
坐标依賴log4j-over-slf4j
-
坐标依賴,注意該包的全限定類名與slf4j-log4j12
的權限定類名一緻。log4j
-
04、slf4j(日志門面)前言一、認識slf4j二、實際應用三、原理分析四、橋接舊的日志實作架構總結參考資料
-
②我們運作下程式檢視一下
import org.apache.log4j.Logger;
public class LogTest {
//擷取Logger執行個體
public static final Logger LOGGER = Logger.getLogger(LogTest.class);
public static void main(String[] args) {
LOGGER.error("error");
}
}
為什麼會出現這種情況呢,看下面原理圖一下子懂了:
- 其實說白了就是
與log4j
的權限定包名都是相同的,當在log4j-over-slf4j
的擴充卡中若是找到slf4j-log4j12
中的log4j-over-slf4j
時此時就會出現無限死循環,也就導緻棧溢出了。log4j
前後引入jar包位置改變不會報錯
不過我發現了一個問題:如果
pom.xml
中的坐标前後位置,兩個
jar
包都導入了也不會報錯
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.27</version>
</dependency>
- 先引入
再引入slf4j-log4j12
運作就不會報錯,可能是在log4j-over-slf4j
的擴充卡中找slf4j-log4j12
時,Logger
中pom.xml
首先被加載到了是以就不會報錯了。log4j
說明:盡管這樣引入不會報錯,我們也一定不要這樣子引入橋接器與
slf4j
的日志實作架構,這樣也沒必要。
總結
1、對于
slf4j
切換日志架構我們實際上就隻需要引入
slf4j
提供的各個日志實作依賴即可(對應坐标其中包含了
slf-api
以及對應實作架構依賴)。
2、對于一開始就沒有進行使用日志門面而隻是單單使用日志架構的項目,若是想要不修改代碼進行切換日志架構,我們就要考慮使用
slf4j
橋接器來進行日志架構切換。
3、
slf4j
提供的日志架構實作盡量不要與橋接器同時使用,否則極有可能會報錯!
參考資料
[1] 視訊:2020年Java進階教程,全面學習多種java日志架構
我是長路,感謝你的耐心閱讀,如有問題請指出,我會聽取建議并進行修正。
歡迎關注我的公衆号:長路Java,其中會包含軟體安裝等其他一些資料,包含一些視訊教程以及學習路徑分享。
程式設計學習qq群:891507813 我們可以一起探讨學習
注明:轉載可,需要附帶上文章連結