天天看點

SLF4J源碼分析

介紹

官網:​​http://www.slf4j.org/​​

github:​​https://github.com/qos-ch/slf4j​​

SLF4J(Simple Logging Facade for Java),它為Java的日志系統提供了一套統一的接口(門面),即:作為各種日志架構(java.util.logging,logback,log4j)的抽象。

通過引入SLF4J,可以使項目與logging具體的實作分離,在提供了一緻的接口的同時,提供了靈活選擇logging實作的能力。(引入SLF4J的庫/應用意味着僅添加一個強制性依賴項slf4j-api.jar)

1、為什麼要設計出一個日志接口的抽象層?

我們都知道,日志對于一個系統來說非常重要。同樣,我們在開發出了一個庫時,也需要列印一些調試或者運作日志,而我們系統往往會引入大量的第三方庫。這是,就會遇到一個問題:假設我們系統使用的是Log4j日志架構,引入了RMQ庫使用的是Logback架構,這時系統就出現了兩個日志架構,維護起來非常麻煩。

解決這個問題的方法是引入一個适配層。例如:

如果我們都是通過SLF4J這種統一的接口,那麼RMQ庫在釋出時就無需帶着具體日志架構的實作,這樣我們系統引入RMQ後,仍然使用的是我們系統中引入的日志實作了,這樣就友善了維護。

slf4j隻做兩件事情:

  • 提供日志接口
  • 提供擷取具體日志對象的方法

說明:這種抽象的思想,在軟體開發中很常見。

2、SLF4J和JCL差別:

在SLF4J之前,Apache Common Logging(即Jakarta Commons Logging,簡稱JCL)也提供了類似的功能(即:統一的日志接口)。它與SLF4J的差別在于:

  • JCL即提供了統一的接口,也提供了一套預設的實作;SLF4J則隻提供了接口層
  • JCL采用運作時綁定,通過Classloader體系加載相應的logging實作;SLF4J采用了靜态綁定
  • SLF4J在接口易用性上更有優勢,大大減少了不必要的日志拼接:
  • JCL為了避免無效的字元串拼接,一般需要通過if判斷:
  • SLF4J則提供了占位符"{}",隻在必要的情況下才會進行日志字元串處理和拼接:
//JCL
if (log.isInfoEnabled()){
  log.info("testid:"+id+",cont:"+JSON.toJSONString(jsonstr));
}

//slf4j
log.info("testid:{},cont:{}",id,JSON.toJSONString(jsonstr));      

推薦使用slf4j中占位符原因主要有兩點:

  • 當設定的日志級别高于某條代碼中的日志級别時,使用占位符可以免掉字元串拼接操作;
  • 占位符底層使用的是StringBuilder進行的拼接,性能比“+”要好;

注:在SLF4J和JCL中,推薦使用前者。

3、SLF4J使用:

SLF4J的使用非常簡單:

  • 引入​​SLF4J依賴​​ (slf4j-api.jar)
  • 引入一種SLF4J的實作,比如:logback、log4j...

然後:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public MyClass {
  Logger logger = LoggerFactory.getLogger(MyClass.class);

  puhblic void method() {
    logger.info("hello world...");
  }
}      

注:從1.6.0開始,如果在類路徑上未找到綁定,則SLF4J将預設為無操作實作;

下圖從SLF4J官網中找到的一個圖,表示了各種實作類和SLF4J的關系:

SLF4J源碼分析

總之,SLF4J接口及其各種擴充卡非常簡單,不依賴任何類加載器,是以SLF4J不會遇到類加載器問題或Commons Logging(JCL)所觀察到的記憶體洩漏。實際上,每個SLF4J綁定在編譯時都進行了硬連線,以使用一個且僅一個特定的日志記錄架構。

靜态綁定原理

和Apache Common Logging不同,SLF4j采用了靜态綁定來确定具體日志庫。靜态綁定就是:

  • 每一個具體的日志庫定義一個包名和類名都相同的類: org.slf4j.impl.StaticLoggerBinder,這個類的功能就是調用具體的日志庫,該類存放在Adaptation layer(适配層)或者native implementation of slf4j-api(實作包)的jar包中;(該類在slf4j-api打成jar包時被mvn移除)
  • SLF4j的使用者隻要把具體日志庫對應的Adaptation layer或者native implementation of slf4j-api的jar包放入classpath中,SLF4j便會裝載(load)對應版本的org.slf4j.impl.StaticLoggerBinder,進而調用具體的日志庫;
  • slf4j-api.jar中通過classLoader.getResources("org/slf4j/impl/StaticLoggerBinder.class")來加載classpath中具體的日志庫中的StaticLoggerBinder類;
SLF4J源碼分析

SLF4J相比JCL的一大優勢是采用了靜态綁定,避免了在OSGI等場景中通過classloader動态綁定造成的困擾。

1、1.7.25版本的slf4j-api.jar靜态綁定過程分析:

1.1)源碼分析:

在demo中可知,使用SLF4J的LoggerFactory.getLogger(Class<?>)方法擷取一個Logger對象,這個過程完成了和具體日志實作類的綁定。通過slf4j-api.jar源碼,SLF4J是調用bind()方法實作的綁定。

1)bind()方法:

  1. 調用findPossibleStaticLoggerBinderPathSet()方法擷取classpath上所有的org/slf4j/impl/StaticLoggerBinder.class,用來報告(沒有找到也不會報錯);
  2. 執行StaticLoggerBinder.getSingleton()實作靜态綁定,如果沒有日志實作架構,則抛出異常;
  3. 執行reportActualBinding()方法
SLF4J源碼分析

2)findPossibleStaticLoggerBinderPathSet()方法:

通過jdk提供的ClassLoader.getStstemResources()方法擷取指定資源的URI。

SLF4J源碼分析

可以發現,在slf4j-api.jar包中根本沒有org.slf4j.impl.StaticLoggerBinder 這個類,是以,如果沒有具體的日志實作庫,那麼在執行到StaticLoggerBinder.getSingleton()方法時就會抛出NoClassDeffoundException

SLF4J源碼分析

3)日志實作庫:

slf4j-log4j12庫中的org.slf4j.impl.StaticLoggerBinder

SLF4J源碼分析

1.2)疑問:

通過slf4j源碼,LoggerFactory.java檔案有一行import org.slf4j.impl.StaticLoggerBinder; 但是上面我們發現在slf4j-api.jar中居然沒有該org.slf4j.impl.StaticLoggerBinder類,也就是說slf4j-api這個工程是無法編譯通過的,又是如何打成slf4j-api.jar的呢?

寫一個工程A,類似sfl4j-api,然後把StaticLoggerBinder類删掉,工程雖然報錯,但是可以通過mvn install打包成功;

寫一個工程B,引入A.jar,然後調用其中方法,會發現報錯:Unresolved compilation problem: 從A.jar包中檢視相應的LoggerFatory類,居然是這樣的:

SLF4J源碼分析

可以發現:雖然上面可以用mvn打包成功,但是由于A工程是一個編譯有問題的工程,反編譯位元組碼檔案可以看到方法全都抛出異常,這說明在打包時,LoggerFactory類生成的位元組碼檔案是不完整的,帶有錯誤的。

通過slf4j-api源碼可以發現,其實在slf4j-api工程中是有org.slf4j.impl.StaticLoggerBinder.java類的,隻是在mvn打包的時候通過ant插件,将org.slf4j.impl.StaticLoggerBinder.class移除掉了。騙過了jdk,使得LoggerFactory.class是一個完整的,可以校驗通過的位元組碼檔案。

SLF4J源碼分析

1.3)總結:

先來明确一下 Java 的綁定(Binding)的概念,Java 本身隻支援靜态(static)綁定與運作時(runtime)綁定,直到與 JDK 1.6 版本一起釋出的 JSR269 才能進行編譯時綁定,編譯時綁定最有代表的是lomok 在編譯過程中修改位元組碼。

1.7.25版本的SFL4j 的 logger 執行個體是 new 出來的(通過StaticLoggerBinder單例),綁定 LogContext 的 StaticLoggerBinder(中介類) 是寫死的,編譯時并沒有處理任何邏輯,也談不上什麼編譯時綁定,而且翻遍了 SLF4j 文檔也沒有找到任何有關編譯時綁定的材料,官方隻提到了 “static binding”, 是以,SLF4j使用的是 Convention over Configuration(CoC)– 慣例優于配置原則,不管是什麼日志架構,隻加載org.slf4j.impl.StaticLoggerBinder。這完美契合了軟體設計的 KISS(Keep It Simple, Stupid)原則。

而 Commons-logging 魔法(magic)一樣的動态加載雖然設計很高大上,在應用領域卻直接被打臉,低效率、與 OSGi 共同使用所導緻的 ClassLoader 問題更是火上澆油,是以員外與大家共勉,寫代碼切勿炫技。

參考:

​​https://www.jianshu.com/p/b562b7ff499f​​

2、1.8版本的slf4j-api.jar靜态綁定過程分析:

SLF4J 1.8中最大的改進就是摒棄了之前的hard code的代碼綁定(要求具體實作日志架構中必須要有一個org.slf4j.impl.StaticLoggerBinder.java),而是使用了更加優雅、耦合更松的SPI方式進行服務發現。我們看看1.8版本slf4j-api中對日志綁定的改進:

  • 提供了org.slf4j.spi.SLF4JServiceProvider服務接口用于SPI綁定
  • 改進了org.slf4j.LoggerFactory.bind()的實作,采用SPI方式進行SLF4JServiceProvider服務發現和綁定
  • 不再支援1.8版本以前的按照約定的類型StaticXxxBinder約定類名進行綁定的方式

由此可見,1.8版本和之前的版本是不相容的(​​http://www.slf4j.org/codes.html#version_mismatch​​)。而且1.8往上的版本都是beta,沒有一個是stable/release的。

SLF4J源碼分析

說明:(官網)

從用戶端的角度來看,slf4j-api的所有版本都是相容的。隻需要確定綁定的版本與slf4j-api.jar的版本比對即可。在初始化時,如果SLF4J懷疑可能存在sfl4j-api與綁定版本不比對的問題,它将發出有關可疑不比對的警告。

1.1)源碼分析:

1)bind方法:

SLF4J源碼分析

2)findServiceProviders()方法:

SLF4J源碼分析

3)日志實作庫:

slf4j-api:1.8.0-beta-2版本,對應的logback-classic版本為logback-classic:1.3.0-alpha4。為了相容1.8的SLF4J,logback-classic提供了SPI服務配置檔案,如下圖。這樣,在啟動階段,SLF4J就可以通過ServiceLoader找到logback-classic并進行注冊了。

同時,最新版的logback也去掉了org.slf.impl包,徹底摒棄了老版本SLF4J的支援。

同樣,在slf4j-log4j12-1.8版本中,也是去掉了org.slf.impl包,提供了SPI服務配置檔案:

SLF4J源碼分析

總結:

slf4j-api1.8版本整個流程和1.7的基本一緻,除了采用了更優雅的服務發現機制,在其他方面,SLF4J 1.8與之前版本差别很小。

參考: