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自動生成的流程圖有點問題):
通常環境變量和配置文件都是空的,關鍵步驟在於第三步中的“找到日誌實現服務”。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中):
org.apache.commons.logging.impl.Log4JLogger
org.apache.commons.logging.impl.Jdk14Logger
org.apache.commons.logging.impl.Jdk13LumberjackLogger
org.apache.commons.logging.impl.SimpleLog
可以看到如果你引入了log4j的jar包,優先也將使用log4j,那麼爲什麼要用SLF4代替Commons Logging呢?大致原因有:
- SLF4J是編譯時綁定到具體的日誌框架,性能優於採用運行時搜尋的方式的commons-logging。但其代價便是如果你需要使用某一種日誌實現,那麼你必須手動引入該日誌實現的SLF4J橋接包(見本文末尾)。
- SLF4J提供了更好的日誌記錄方式,帶來下這幾方面的好處:
- 更好的可讀性;
- 不需要使用logger.isDebugEnabled()來解決日誌因爲字符拼接產生的性能問題。
- 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提供的所有橋接方案,開發者須針對引入的日誌實現來選擇對應的橋接包。