03.03 SpringBoot統一日誌處理原理

閱讀推薦

SpringBoot作為日常開發利器,開箱即用,大量的star等已經成為節省開發的重要框架之一,但是各個框架的star中引入的日誌框架卻不盡相同,有的是log4j,有的是slf4j,這導致我們在引入多個框架的star的時候,往往會引入多個日誌框架,每一個日誌框架彼此效率不盡相同,那麼我們能不能做到在項目中僅引入一個統一的日誌框架呢?本篇我們就來探索SpringBoot如何實現統一日誌操作

為什麼需要日誌

首先我們需要明白,日誌的作用是什麼--即用來在程序運行過程中,將我們需要的信息打印出來,便於我們在調試中查找和觀察。在JAVA中存在很多常見的日誌框架,如JUL、JCL、Jboss-logging、log4j、logback、slf4j等,這麼多日誌框架,我們該如何選擇?

日誌門面與日誌實現

在日誌框架選型之前,我們先了解一個概念,什麼是日誌門面?日誌門面,不是具體的日誌解決方案,它只服務於各種各樣的日誌系統,允許最終用戶在部署其應用時使用其所希望的日誌實現來使用日誌功能。而日誌實現則是基於對應的日誌門面的規範來實現的具體日誌功能的框架,常見的日誌門面與日誌實現關係如下:

SpringBoot統一日誌處理原理

每一種日誌框架輸出信息的效率也不盡相同,而我們日常開發使用的框架中往往都會引入一個日誌框架來輔助輸出框架信息,然而框架之間由於歷史迭代原因及框架性能等問題,選擇的日誌框架也不一樣,常見的框架與默認選擇的日誌系統關係如下:

SpringBoot統一日誌處理原理

由於歷史迭代原因,JCL和jboss-logging日誌框架,基本已經很久沒有更新了,不太適合作為現在框架的主流選擇,那麼剩下的選擇中log4j、slf4j是使用最多的,然而由於log4j的輸出性能問題,log4j的作者選擇重新編寫了一個日誌門面--Slf4j,並且編寫了基於Slf4j的日誌實現--logback,其輸出信息的效率遠超log4j,解決了log4j遺留下的性能問題,所以在SpringBoot框架中,默認也選擇了Slf4j來作為默認日誌框架

slf4j的使用

現在,我們來看看slf4j的使用,引入maven依賴:

<code><dependency>
<groupid>org.slf4j/<groupid>
<artifactid>slf4j-api/<artifactid>
<version>1.7.28/<version>
/<dependency>
/<code>

按照slf4j官方的說法,,日誌記錄方法的調用,不應該來直接調用日誌的實現類,而是調用日誌抽象層裡面的實現方法,獲取通過日誌工廠創建的日誌實例,即可輸出對應的日誌:

<code>import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld { public static void main(String[] args) {
Logger logger =
LoggerFactory.getLogger(HelloWorld.class);
[圖片上傳中...(slf4j日誌輸出過程.png-6f5073-1583207284091-0)]
logger.info("Hello World");
}
}
/<code>

這裡我們注意到了一點,使用slf4j的輸出日誌的時候,我們也引入了logback這個基於slf4j日誌門面實現的具體日誌輸出框架,如果不指定具體的日誌輸出實現,將會找不到具體的日誌輸出實例,slf4j的日誌輸出過程如圖所示:

SpringBoot統一日誌處理原理

slf4j日誌輸出過程

從圖中可以看到,應用程序調用了slf4j的api接口以後,具體的實現則是由slf4j日誌門面找到對應的日誌的系統來實現日誌輸出

解決多框架日誌不統一問題

現在我們再回到日誌統一的問題上,前面已經瞭解了,開發常用的框架,如Springmybatis等使用的框架都是框架開發者自己選擇的,如果我們每個框架就引入一個日誌系統,並且最終需要打印日誌的時候,會出現使用n種日誌系統平臺,並且每一種的日誌打印的格式、內容和性能都需要手動控制,不僅讓項目變大,而且增大了項目複雜度,對性能也有很大的影響,那麼我們該如何讓所有的開源框架統一使用Slf4j來輸出呢?我們來看下slf4j官方給我們的方案,如圖所示:

SpringBoot統一日誌處理原理

sfl4j適配日誌

從圖中我們可以看出來,官方的方案是針對不同的日誌框架,開發了一套適配兼容的框架與之對應,使用這些兼容jar來替代原來的日誌框架即可,例如log4j日誌框架,與之對應的就是log4j-over-slf4j.jar,並且常見的日誌框架,slf4j團隊都實現了一套與之對應的基於slf4j的兼容框架,關係如下:

日誌框架slf4j兼容框架log4jlog4j-over-slf4jcommons loggingjcl-over-slf4jjava.util.loggingjui-to-slf4j

SpringBoot如何處理日誌關係

在使用SpringBoot的時候,我們會發現官方默認使用的是spring‐boot‐starter‐logging這個starter來引入日誌系統的,我們展開該依賴的依賴圖,如下:

SpringBoot統一日誌處理原理

SpringBoot處理日誌關係

可以看到spring‐boot‐starter‐logging這個starter中,引入了四個日誌實例的依賴,分別是logback和我們前面提到的日誌兼容jar的依賴,並且最終引入了slf4j的日誌門面的依賴,實現了統一日誌處理。但是為什麼兼容jar引入後就能解決日誌輸出的問題呢?難道兼容包有什麼神奇的黑科技嗎?其實不然,我們隨便展開其中的幾個兼容日誌jar的包名,如圖:

SpringBoot統一日誌處理原理

日誌兼容包的包名關係

原來這些日誌兼容包的包名與原來的日誌框架的包名完全一樣,並且完全按照slf4j的方式實現了一套和以前一樣的API,這樣依賴這些日誌框架的開源框架在運行的時候查找對應包名下的class也不會報錯,但熟悉java類加載機制的都知道,兩個jar的包名以及使用的class都一樣的話,加載會出現異常,我們進入spring‐boot‐starter‐logging的pom依賴中一探究竟,最後在maven依賴中發現了端倪,如Spring框架使用的是commons-logging,而在spring-boot-starter-logging中,將spring的日誌依賴排除,如下:

<code><dependency>        
<groupid>org.springframework/<groupid>
<artifactid>spring‐core/<artifactid>
<exclusions>
<exclusion>
<groupid>commons‐logging/<groupid>
<artifactid>commons‐logging/<artifactid>
/<exclusion>
/<exclusions>
/<dependency>
/<code>

這樣spring框架在運行時使用的時候,使用的就是兼容jar中的日誌實例了,SpringBoot成功的完成了一次日誌系統統一的偷天換日操作。

slf4j的橋接原理

通過查看SpringBoot的日誌處理,我們可以大致總結如下幾步操作:

1、將系統中其他日誌框架先排除出去;2、用中間包來替換原有的日誌框架;3、我們導入slf4j其他的實現

通過以上的操作,即可完成日誌系統的統一,但是我們開始有了新的疑惑,slf4j是怎麼做到的自動查找對應的實現日誌,並且完成了日誌的正常打印操作的呢?這個就要涉及到slf4j的橋接原理,我們先來看看slf4j源碼中關於日誌調用相關的代碼:

<code>//slf4j日誌調用過程相關的代碼
//根據名稱獲取日誌實例
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
//獲取日誌實例工廠並且完成日誌實例的查找與初始化操作
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
//查找實現類
performInitialization();
}
...
return StaticLoggerBinder.getSingleton().getLoggerFactory();
...
}
/<code>

可以看到整個過程中是通過StaticLoggerBinder.getSingleton() 來進行初始化日誌工廠操作,而StaticLoggerBinder這個類是從哪來的呢?我們發現StaticLoggerBinder類並不存在於slf4j的jar中,而是通過查找org/slf4j/impl/StaticLoggerBinder.class類的路徑來發現具體的實現類,代碼如下:

<code>//設置默認的查找日誌實例的StaticLoggerBinder路徑
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

private static Set findPossibleStaticLoggerBinderPathSet() {
.......
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
......
}

/<code>

這個時候我們就該思考一個問題,如果我們同時存在了多個StaticLoggerBinder 時會加載哪一個呢?熟悉java類加載機制可知,類加載器會按照一定的順序逐個掃描jar包目錄並且加載出來,所以先被類加載器掃描的StaticLoggerBinder會優先被加載,具體的加載順序如下:

1.$java_home/lib 目錄下的java核心api

2.$java_home/lib/ext 目錄下的java擴展jar包

3.java -classpath/-Djava.class.path所指的目錄下的類與jar包

4.$CATALINA_HOME/common目錄下按照文件夾的順序從上往下依次加載

5.$CATALINA_HOME/server目錄下按照文件夾的順序從上往下依次加載

6.$CATALINA_BASE/shared目錄下按照文件夾的順序從上往下依次加載

7.項目/WEB-INF/classes下的class文件

8.項目/WEB-INF/lib下的jar文件

根據slf4j橋接原理改造logger

我們都知道平時使用slf4j輸出日誌的時候往往獲取Logger實例來進行日誌打印,但是Logger僅僅支持本地日誌,不支持分佈式環境的日誌,而在slfj中有LogBean實例,可以支持分佈式日誌,包含了鏈路相關信息,那麼我們是否可以改造slf4j的橋接過程,使得我們可以靈活的使用本地日誌或者分佈式日誌呢?首先我們先看看我們需要實現的需求:

  1. logger和logbean結合,統一日誌入口
  2. logbean降低代碼侵入性
  3. 無縫替換第三方框架中的日誌,根據需求加入到分佈式日誌中

想要實現這個功能,有以下兩個思路實現:

1.我們通過自定義appender,基於logback的appender進行擴展,可以實現分別輸出本地日誌以及分佈式日誌,但是缺陷在於appender擴展性不高,很多參數信息獲取不到,例如上下文信息等

2.我們通過實現Logger接口,用來將Logger和LogBean聚合在一起,從而實現LogBean集成到Logger中,同樣此種方式的缺陷在於對於第三方框架日誌,我們無能為力,無法直接替換使用,並且在使用的時候需要使用自定義的LogFactory

第一種思路我們可以看出來,侷限性太高,靈活度不夠,接下來我們嘗試使用第二種方案,實現聚合LoggerLogBean,對外公開統一的api進行日誌輸出使用:

<code>public class CustomLogger implements LocationAwareLogger {
private Logger logger;
//提供getLogger方法獲取logger
public static LoggerFacade getLogger(Class clazz) {
LoggerFacade loggerFacade = new LoggerFacade();
loggerFacade.logger = LoggerFactory.getLogger(clazz);
return loggerFacade;
}

...
//打印本地日誌的同時 輸出到logbean中
@Override
public void warn(String msg) {
logger.warn(msg);
appendExtra(msg, Level.WARN);
}

......

public void appendExtra(String str, Level level) {
String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
//獲取上下文,通過上下文判斷,如果存在則獲取分佈式環境的LogBean實例
ThreadContext threadContext = ContextContainer.retrieveServiceContext();
if (threadContext != null) {
LogBean logBean = threadContext.getLogBean();
if (logBean != null) {
logBean.getInner().getExtra().add(date + " " + level.toString() + " " + simpleName(getName()) + " -" +
" " + str);
}
}
}
}
/<code>

接下來我們可以替換slf4j的實現,修改為我們自定義的CustomerLogger,內部調用logback的日誌本地輸出,而通過前面橋接原理可以知道,slf4j具體橋接獲取實例的過程是通過LoggerFactory來獲取,那麼我們來嘗試修改LoggerFactory的代碼實現替換為CustomerLogger實例:

<code>public class CustomLoggerFactory implements ILoggerFactory {

private static CustomLoggerFactory customLoggerFactory;

public static CustomLoggerFactory getInstance(LoggerContext loggerContext) {
if (customLoggerFactory == null) {
customLoggerFactory = new CustomLoggerFactory(loggerContext);
}
return customLoggerFactory;
}
//logback的LoggerFactory實現

private LoggerContext loggerContext;

public CustomLoggerFactory(LoggerContext loggerContext) {
this.loggerContext = loggerContext;
}
//返回CustomLogger
@Override
public Logger getLogger(String name) {
ch.qos.logback.classic.Logger logger = loggerContext.getLogger(name);
return CustomLogger.getLogger(logger);
}

public LoggerContext getLoggerContext() {
return loggerContext;
}

@Override
public ILoggerFactory getLoggerFactory() {
if (!initialized) {
return defaultLoggerContext;
}

if (contextSelectorBinder.getContextSelector() == null) {
throw new IllegalStateException(
"contextSelector cannot be null. See also " + NULL_CS_URL);
}
LoggerContext loggerContext = contextSelectorBinder.getContextSelector().getLoggerContext();
return CustomLoggerFactory.getInstance(loggerContext);
}
}
/<code>

由以上替換後,項目中通過LoggerFactory獲取的到logger對象 就替換成了CustomLogger對象了,從而實現了降低侵入,將LoggerLogBean整合的效果

結語

Hi~ o( ̄▽ ̄)ブ ,整理了約100G的面試、學習資料,但是呢篇幅有限。若你有此需求,那便可免費分享下載,在簡信發送“面試”或 點擊此鏈接獲取資源下載方式下載吧。

SpringBoot統一日誌處理原理

網盤上百G資源


SpringBoot統一日誌處理原理

java面試題詳解


SpringBoot統一日誌處理原理

java視頻及資料


分享到:


相關文章: