spring集成SLF4J時的問題及延展

Spring Framework 所使用的日誌接口一直都是 commons-logging,Apache Commons Logging是一個通用的日誌接口,與slf4j簡單日誌門面類似。如果它搜索到應用添加了log4j的引用,那麼將直接使用log4j,如果你想用現在越來越流行的SLF4J來接管日誌接口,則需要使用SLF4J提供的 jcl-over-slf4j 把 commons-logging API 轉接到 SLF4J API 上。

問題

一般的教程在介紹maven配置時除了讓添加 slf4j-api 和 jcl-over-slf4j(不可缺少) 的jar包外,還會提醒移除spring中對 commons-logging 的依賴(如下)。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>${spring-version}</version>
    <scope>runtime</scope>
    <exclusions>
    <exclusion>
        <artifactId>commons-logging</artifactId>
        <groupId>commons-logging</groupId>
    </exclusion>
    </exclusions>
</dependency>

注意到jcl-over-slf4j幾乎複製了commons-logging的所有方法、包路徑,因此移除了commons-logging自然就全由jcl-over-slf4j接管。然而諸位有沒發現就算你不移除commons-logging依賴,日誌依然是由jcl-over-slf4j接管的,因此依然可以識別到SLF4J,這到底是如何做到的呢?

分析

Apache Commons Logging (原名 Jakarta Commons Logging,JCL) 只提供 log 接口,具體的實現則在運行時動態尋找,如果找不到外部實現那麼將使用自帶的實現類。

我們先從調用開始說起,JCL的調用如下:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Log log = LogFactory.getLog(Main.class.getName());

getLog()方法的實現如下:

 public static Log getLog(Class clazz) {
     return (getFactory().getInstance(clazz));
 }

也就是說JCL先通過getFactory()搜索工廠實現類,然後使用它來獲取Log實例。getFactory()方法很長,我大致說下加載的過程(markdown自動生成的流程圖有點問題):

Created with Raphaël 2.1.2開始尋找Factory找不到Factory緩存找不到環境變量org.apache.commons.logging.LogFactory設置的類找不到日誌實現服務(service)找不到commons-logging.properties文件中org.apache.commons.logging.LogFactory設置的類使用自帶的LogFactoryImpl類緩存Factory尋找完成返回nullyesnoyesnoyesnoyesnoyesno

通常環境變量和配置文件都是空的,關鍵步驟在於第三步中的“找到日誌實現服務”。JDK中關於該步驟的註解如下:

嘗試使用JDK1.3定義的服務加載機制來搜尋服務,它需要讀取META-INF/services目錄下的文件,該文件的內容指定了實現所需接口的類。

這其實就是Java SPI(Service Provider Interface)的約定。一個服務通常指的是已知的接口或者抽象類,服務提供方就是對這個接口或者抽象類的實現。按照SPI制定的標準提供方需要在資源路徑META-INF/services目錄下放置一個文本文件,文件的命名爲該服務接口的全限定名,內容爲實現類的全限定名。

好,這時候我們打開jcl-over-slf4j.jar文件,可以看到在META-INF/services目錄下有一個org.apache.commons.logging.LogFactory文件,內容爲org.apache.commons.logging.impl.SLF4JLogFactory,該實現類就在本jar包類。通過這種SPI約定的加載方式,SLF4J就接管了Apache Commons Logging,然後只需要引入SLF4J針對log4j或logback提供的橋接包,就可以讓spring表面上使用的是Apache Commons Logging,實際上用的卻是你提供的日誌框架。

延展

如果你不想接入SLF4J,那麼最後使用的自帶LogFactoryImpl類將會依次按照以下順序查找,若找到則直接加載該實現類(實現類也是自帶的commons-logging.jar中):

  1. org.apache.commons.logging.impl.Log4JLogger
  2. org.apache.commons.logging.impl.Jdk14Logger
  3. org.apache.commons.logging.impl.Jdk13LumberjackLogger
  4. org.apache.commons.logging.impl.SimpleLog

可以看到如果你引入了log4j的jar包,優先也將使用log4j,那麼爲什麼要用SLF4代替Commons Logging呢?大致原因有:

  1. SLF4J是編譯時綁定到具體的日誌框架,性能優於採用運行時搜尋的方式的commons-logging。但其代價便是如果你需要使用某一種日誌實現,那麼你必須手動引入該日誌實現的SLF4J橋接包(見本文末尾)。
  2. SLF4J提供了更好的日誌記錄方式,帶來下這幾方面的好處:
    1. 更好的可讀性;
    2. 不需要使用logger.isDebugEnabled()來解決日誌因爲字符拼接產生的性能問題。
  3. Apache Common Logging使用的類加載機制無法在OSGI環境下工作。

SLF4和Commons Logging其實都是對不同日誌框架提供的一個門面封裝,可以在部署的時候不修改任何配置即可接入一種日誌實現方案。也就是說上述的方法其實就是雙層門面,一層套一層。

此外,在不用SLF4J時,Apache Common Logging默認的Log4JLogger使用的1.x版本的Log4J,若要使用2.x版本需要加入log4j提供的Apache Commons Logging 橋接器log4j-jcl.jar,其原理也是在META-INF/services目錄下配置了自己的實現類org.apache.logging.log4j.jcl.LogFactoryImpl

最後附上一張SLF4J提供的所有橋接方案,開發者須針對引入的日誌實現來選擇對應的橋接包。
SLF4J橋接方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章