JAVA SpringBoot統一日志處理原理詳解

框架 日志
Spring JCL
SpringBoot Sfl4j–>logback
Hibernate3 Slf4j
Struts2 LoggerFactory(com.opensymphony.xwork2.util.logging.LoggerFactory)

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

slf4j的使用

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

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.28</version>
</dependency>

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

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");
 }
}

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

slf4j日志輸出過程

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

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

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

sfl4j適配日志

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

日志框架 slf4j兼容框架
log4j log4j-over-slf4j
commons logging jcl-over-slf4j
java.util.logging jui-to-slf4j

SpringBoot如何處理日志關系

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

SpringBoot處理日志關系

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

日志兼容包的包名關系

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

<dependency>        
    <groupId>org.springframework</groupId>            
    <artifactId>spring‐core</artifactId>            
    <exclusions>            
        <exclusion>                
        <groupId>commons‐logging</groupId>            
        <artifactId>commons‐logging</artifactId>     
        </exclusion>                
    </exclusions>            
</dependency> 

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

slf4j的橋接原理

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

1、將系統中其他日志框架先排除出去;

2、用中間包來替換原有的日志框架;

3、我們導入slf4j其他的實現

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

//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();
    ...  
 }

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

//設置默認的查找日志實例的StaticLoggerBinder路徑
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
private static Set findPossibleStaticLoggerBinderPathSet() {
  .......
  paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
  ......
}

這個時候我們就該思考一個問題,如果我們同時存在瞭多個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的橋接過程,使得我們可以靈活的使用本地日志或者分佈式日志呢?首先我們先看看我們需要實現的需求:

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

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

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

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

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: