我們通過代碼入手,層層加碼,直觀感受SLF4J列印日誌,並跟蹤代碼追本溯源。主要瞭解,SLF4J是如何作為門面和其他日誌框架進行解耦。 ...
1 SLF4J介紹
SLF4J即Simple Logging Facade for Java,它提供了Java中所有日誌框架的簡單外觀或抽象。因此,它使用戶能夠使用單個依賴項處理任何日誌框架,例如:Log4j,Logback和JUL(java.util.logging)。通過在類路徑中插入適當的 jar 文件(綁定),可以在部署時插入所需的日誌框架。如果要更換日誌框架,僅僅替換依賴的slf4j bindings。比如,從java.util.logging替換為log4j,僅僅需要用slf4j-log4j12-1.7.28.jar替換slf4j-jdk14-1.7.28.jar。
2 SLF4J源碼分析
我們通過代碼入手,層層加碼,直觀感受SLF4J列印日誌,並跟蹤代碼追本溯源。主要瞭解,SLF4J是如何作為門面和其他日誌框架進行解耦。
2.1 pom只引用依賴slf4j-api,版本是1.7.30
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
2.1.1 執行一個Demo
public class HelloSlf4j {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
logger.info("Hello World info");
}
}
2.1.2 日誌提示信息
綁定org.slf4j.impl.StaticLoggerBinder失敗。如果在類路徑上沒有找到綁定,那麼 SLF4J 將預設為無操作實現
2.1.3 跟蹤源碼
點開方法getLogger(),可以直觀看到LoggerFactory使用靜態工廠創建Logger。通過以下方法,逐步點擊,報錯也很容易找到,可以在bind()方法看到列印的異常日誌信息。
org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)
org.slf4j.LoggerFactory#getLogger(java.lang.String)
org.slf4j.LoggerFactory#getILoggerFactory
org.slf4j.LoggerFactory#performInitialization
org.slf4j.LoggerFactory#bind
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
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.");
} else {
failedBinding(ncde);
throw ncde;
}
} catch (java.lang.NoSuchMethodError nsme) {
String msg = nsme.getMessage();
if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
INITIALIZATION_STATE = FAILED_INITIALIZATION;
Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
Util.report("Your binding is version 1.5.5 or earlier.");
Util.report("Upgrade your binding to version 1.6.x.");
}
throw nsme;
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
} finally {
postBindCleanUp();
}
}
進一步分析綁定方法findPossibleStaticLoggerBinderPathSet(),可以發現在當前ClassPath下查詢了所有該路徑的資源“org/slf4j/impl/StaticLoggerBinder.class”,這裡可能沒有載入到任何文件,也可能綁定多個,對沒有綁定和綁定多個的場景進行了友好提示。這裡通過路徑載入資源的目的主要用來對載入的各種異常場景提示。
再往下代碼StaticLoggerBinder.getSingleton()才是實際的綁定,並且獲取StaticLoggerBinder的實例。這裡如果反編譯,你會發現根本沒有這個類StaticLoggerBinder。
如果沒有載入到文件,正如上邊demo執行的結果一樣,命中NoSuchMethodError異常,並列印沒有綁定場景的提示信息。
方法findPossibleStaticLoggerBinderPathSet()的源碼如下,可以發現類載入器通過路徑獲取URL資源。
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
2.2 pom引用依賴logback-classic
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
2.2.1 執行demo
可以看到正常的列印日誌信息,並且沒有任何異常
2.2.2 跟蹤源碼
這個時候如果再點擊進入方法StaticLoggerBinder.getSingleton(),發現類StaticLoggerBinder是由包logback-classic提供的,並且實現了SLF4J中的介面LoggerFactoryBinder。StaticLoggerBinder的創建用到了單例模式,該類主要目的返回一個創建Logger的工廠。這裡實際返回了ch.qos.logback.classic.LoggerContext的實例,再由該實例創建ch.qos.logback.classic.Logger。
UML類圖如下:
2.3 pom再引入log4j-slf4j-impl
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.9.1</version>
</dependency>
2.3.1 執行demo
列印日誌如下,提示綁定了兩個StaticLoggerBinder.class,但最終實際綁定的是ch.qos.logback.classic.util.ContextSelectorStaticBinder。這裡邊也驗證了一旦一個類被載入之後,全局限定名相同的類就無法被載入了。這裡Jar包被載入的順序直接決定了類載入的順序。
SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
18:19:43.521 [main] INFO com.cj.HelloSlf4j - Hello World info
2.4 log4j-slf4j-impl和logback-classic的引入位置變換
如果Pom文件先引入log4j-slf4j-impl,再引入logback-classic
2.4.1 執行demo
根據日誌列印結果,可以看到實際綁定的是org.apache.logging.slf4j.Log4jLoggerFactory;但是沒有正常列印出日誌,需要進行log4j2的日誌配置。說明實際綁定的是og4j-slf4j-impl包中的org/slf4j/impl/StaticLoggerBinder.class文件;這裡也驗證瞭如果有引入了多個橋接包,實際綁定的是先載入到的文件;
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/D:/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.9.1/log4j-slf4j-impl-2.9.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/D:/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.
2.5 類載入方式的變化
2.5.1 slf4j-api-1.7.30版本的打包技巧
反編譯看slf4j-api-1.7.30-sources.jar,發現壓根沒有這個類org.slf4j.impl.StaticLoggerBinder,他怎麼會編譯成功呢?猜想是不是打包的時候把這個類排除掉了呢?通過git下載源碼發現slf4j源碼其實是有這個文件的,org/slf4j/impl/StaticLoggerBinder.class;這裡使用了一個小技巧,打包的時候把實現類排除掉了,雖然不太優雅,但是思路很巧妙。
2.5.2 slf4j-api-2.0.0版本引入SPI(Service Provider Interface)
該版本通過使用SPI方式進行實現類的載入,感覺比之前的實現方式優雅了很多。橋接包只需要在這個位置:META-INF/services/,定義一個文件org.slf4j.spi.SLF4JServiceProvider(命名為SLFJ4提供的介面名),並且文件中指定實現類。只要引入這個橋接包,就可以適配到對應實現的日誌框架。
以下是SPI方式載入的源碼
private static List<SLF4JServiceProvider> findServiceProviders() {
ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
List<SLF4JServiceProvider> providerList = new ArrayList();
Iterator var2 = serviceLoader.iterator();
while(var2.hasNext()) {
SLF4JServiceProvider provider = (SLF4JServiceProvider)var2.next();
providerList.add(provider);
}
return providerList;
}
2.5.3 類載入方式對比
2.6 SLF4J官方已經實現綁定的日誌框架
slf4j已經提供了常用日誌框架的橋接包,以及詳細的文檔描述,使用起來非常簡單。
下圖是SLF4J官網中提供的,表示了各種日誌實現框架和SLF4J的關係:
2.7 總結
- SLF4J API旨在一次綁定一個且僅一個底層日誌框架。而且引入SLF4J後,不管是否可以載入到StaticLoggerBinder,或者載入到多個StaticLoggerBinder,都進行友好提示,用戶體驗上考慮都很周到。如果類路徑上存在多個綁定,SLF4J 將發出警告,列出這些綁定的位置。當類路徑上有多個綁定可用時,應該選擇一個希望使用的綁定,然後刪除其他綁定。
- 單純看SLF4J源碼,其實整體設計實現上都很簡單明確,定位非常清楚,就是做好門面。
- 鑒於 SLF4J 介面及其部署模型的簡單性,新日誌框架的開發人員應該會發現編寫 SLF4J 綁定非常容易。
- 對於目前比較主流的日誌框架都通過實現適配進行相容支持。只要用戶選擇了SLF4J,就可以確保以後變更日誌框架的自由。
3 SLF4J設計模式的使用
在slf4j中用到了一些經典的設計模式,比如門面模式、單例模式、靜態工廠模式等,我們來分析以下幾種設計模式。
3.1 門面模式(Facade Pattern)
1)解釋
門面模式,也叫外觀模式,要求一個子系統的外部與其內部的通信必須通過一個統一的對象進行。門面模式提供一個高層次的介面,使得子系統更易於使用。使用了門面模式,使客戶端調用變得更加簡單。
Slf4j制定了log日誌的使用標準,提供了高層次的介面, 我們編碼過程只需要依賴介面Logger和工廠類 LoggerFactory就可以實現日誌的列印,完全不用關心日誌內部的實現細節是logback實現的方式,還是log4j的實現方式。
2)圖解
Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
logger.info("Hello World info");
3)優點
解耦,減少系統的相互依賴。所有的依賴都是對門面對象的依賴,與子系統無關,業務層的開發不需要關心底層日誌框架的實現及細節,在編碼的時候也不需要考慮日後更換框架所帶來的成本。
介面和實現分離,屏蔽了底層的實現細節,面向介面編程。
3.2 單例模式(Singleton Pattern)
1)解釋
單例模式,確保一個類僅有一個實例,並提供一個訪問它的全局訪問點。
在SLF4J的適配包中都需要實現類StaticLoggerBinder,而類StaticLoggerBinder的實現就用了單例模式,而且是最簡單的實現方法,在靜態初始化器中直接new StaticLoggerBinder(),提供全局訪問方法獲取該實例。
2)UML圖
3)優點
在單例模式中,活動的單例只有一個實例,對單例類的所有實例化得到的都是相同的一個實例。這樣就防止其它對象對自己的實例化,確保所有的對象都訪問一個實例
單例模式具有一定的伸縮性,類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。
提供了對唯一實例的受控訪問。
在記憶體里只有一個實例,減少了記憶體的開銷,提高系統的性能。
4 啟示
- 儘管SLF4J整體代碼短小但很精煉,可見門面模式運用好的威力。門面模式也為我們提供了對於多版本的實現如何統一定義介面以及相容提供了參考。
- SLF4J定義和實現方案對用戶都很友好,同時又提供了各種橋接包,進行完善的文檔指導使用。總之各項用戶體驗都很棒,這也許也是SLF4J目前最受歡迎的原因之一吧。
- 我們要多思考面向介面編程的思想,降低代碼耦合度,提高代碼擴展性。
- 使用SPI的方式,優雅的載入擴展實現。
- 好產品是設計出來的,更是優化迭代出來的。
5 參考資料
- slf4j官網:https://www.slf4j.org/manual.html
類載入: - https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html
- https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html
- https://www.ibm.com/docs/en/sdk-java-technology/7.1?topic=cl-parent-delegation-model-1
作者:京東物流 曹俊
來源:京東雲開發者社區