1. 程式人生 > 其它 >SLF4J 快速入門 / 繫結原理

SLF4J 快速入門 / 繫結原理

官網: http://www.slf4j.org/
GitHub: https://github.com/qos-ch/slf4j

一、簡介

SLF4J(Simple Logging Façade for Java)日誌框架,是各種日誌框架的簡單門面(simple facade)或抽象介面,允許使用者部署時選擇具體的日誌實現。

相較於 JCL 有什麼優點:

  • 其在設計上簡單得多,因此也足夠健壯。
  • 靜態繫結非常簡單 但足夠有效,解決了困擾 JCL 的類載入器(class loader)問題
  • 引數化日誌的增強,解決了重要的日誌效能問題
  • 在 org.slf4j.Logger 介面中,Marker 物件的引入為更進階的日誌系統預留了空間;同時也允許切換回傳統的日誌系統

二、需求及包引入

要求及說明:

  • JDK版本要求:1.5+
  • 向後相容性:
    slf4j-api 自身目前是向後相容所有版本的,意味著可以從 1.0 升至任意更新版本。
    但根據slf4j-api版本不同,具體到繫結層上,則可能需要特定版本的繫結。例如 slf4j-api-1.5.6 需使用 slf4j-simple-1.5.6 而 slf4j-simple-1.4.2 將無法工作。
  • 包依賴整體邏輯,參考:Java 日誌框架概述(slf4j / log4j / JUL / Common-logging(JCL) / logback)
  • 所有繫結層/橋接層庫,均隨 SLF4J 一起釋出,可在此處尋找: https://github.com/qos-ch/slf4j

簡單示例:引入 SLF4j API 及其實現(以logback為例)

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

三、SLF4J API 簡單使用示例

  1. 宣告及獲取 LOGGER
    private static final Logger LOGGER = LoggerFactory.getLogger(DistrictController.class);
  2. 列印日誌
    LOGGER.debug("Attempted to do something in Obj {}", district);

四、更多相關知識

1. 日誌等級

分為5個等級,可見 org.slf4j.event

  • ERROR
  • WARN
  • INFO
  • DEBUG
  • TRACE

2. SLF4J 日誌介面設計

  1. 日誌列印介面
    slf4j 以"日誌等級"作為方法名,並至少接受一個 String 型別的訊息描述,如:
    debug(String msg)
    debug(String format, Object arg)
    debug(String msg, Throwable t)
    …
    
    至於為什麼不直接接受 Object ,參考: http://www.slf4j.org/faq.html#string_or_object
  2. 模板/引數化訊息
    SLFJ4J 為訊息提供了引數化(如下圖),以解決 debug("hello" + "world") 這種使用方式無論是否啟用該等級日誌都會進行字串連線"的問題,避免日誌開銷(據官方講30倍的開銷)

    訊息中引數佔位符(formatting anchor):{}
    例:logger.debug("Temperature set to {}. Old temperature was {}.", t, oldT);
    更多效能 / 轉義 / 匹配規則資訊,可參考: http://www.slf4j.org/faq.html#logging_performance

五、繫結實現原理

  1. 在第一次呼叫 LoggerFactory#getLogger 時,會嘗試呼叫 LoggerFactory#bind 進行日誌工廠的初始化。
  2. 根據版本不同,實現繫結的方式也不一致。但並非網上所謂的“編譯時繫結”這麼高深,關於這種說法後面會解釋。
    繫結這種術語其實本身就是slf4j官方自創的
    • 在 slf4j 1.8 版本之前:LoggerFactory#bind 基於 COC(Convention over Configuration)的思想,約定大於配置,單純呼叫 org.slf4j.impl.StaticLoggerBinder#getSingleton 來初始化。
      但實際上 slf4j-api 根本不包含此類,而是由各實現/繫結包(如 slf4j-log4j)來提供
      原始碼如下:

      import org.slf4j.impl.StaticLoggerBinder
      
      private final static void bind() {
      	try {
      		StaticLoggerBinder.getSingleton();
      		INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
      	} catch (NoClassDefFoundError ncde) {
      		INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
      		Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
      		Util.report("Defaulting to no-operation (NOP) logger implementation");
      		Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
      	}
      }
      

      這也是為什麼官方特別說明,放且僅放一個繫結,不要在類路徑上放置多個繫結,因為會衝突。(you simply drop one and only one binding of your choice onto the appropriate class path location. Do not place more than one binding on your class path. Here is a graphical illustration of the general idea)

    • 在 slf4j 1.8 之後:採用 Java SPI (Service Provider Interface) 機制

      private final static void bind() {
      		List<SLF4JServiceProvider> providersList = findServiceProviders();
      		if (providersList != null && !providersList.isEmpty()) {
      		   PROVIDER = providersList.get(0);
      		   PROVIDER.initialize();
      		   INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
      		}
      }
      private static List<SLF4JServiceProvider> findServiceProviders() {
      	ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
      	List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
      	for (SLF4JServiceProvider provider : serviceLoader) {
      		providerList.add(provider);
      	}
      	return providerList;
      }
      

      以 slf4j-log4j12 為例,基於 SPI 實現了 SLF4JServiceProvider

六、常見問題

  1. 網上為什麼說“編譯時繫結”
    答:雖然有些迷惑的說法,但也並不是毫無道理。原因如下:
    1. 官方的說法自身就很具誤導性,感覺是有意的:
      "In fact, each SLF4J binding is hardwired at compile time to use one and only one specific logging framework. For example, the slf4j-log4j12-1.7.28.jar binding is bound at compile time to use log4j”
    2. 雖有些不太準確,但 1.8 以前時的“靜態繫結”一定程度上也能解釋為編譯時繫結。
  2. 是否應該將類的 Logger 成員宣告為靜態?
    http://www.slf4j.org/faq.html#declared_static

七、相關踩坑

  1. 引用呼叫SLF4J API 執行報錯
    SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
    SLF4J: Defaulting to no-operation (NOP) logger implementation
    SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
    
    答:SLF4J只是介面層,未找到實現則預設的空實現(nop)【1.6 開始】,並列印警告
    可引入 SLF4J 實現/繫結層,如:
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-classic</artifactId>
    	<version>1.2.3</version>
    	<scop>test</scop>
    </dependency>
    
  2. 在多個引數情況下,若想列印 Throwable 堆疊資訊,需注意Throwable必須放在最後一個
    例:logger.error("錯誤訊息:{}",e.getMessage(),e);

參考:
SLF4J FAQ
Introduction to SLF4J
Slf4j列印異常的堆疊資訊

本文為博主原創文章,如需轉載請註明連結出處! 如有幫助到你,還請留言支援一下,謝謝:) 若有問題,也歡迎討論指正。