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失敗,如果在類路徑上沒有找到系結,那么 SL??F4J 將默認為無操作實作
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
作者:京東物流 曹俊
來源:京東云開發者社區
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555740.html
標籤:其他
上一篇:Rust語言 - 介面設計的建議之受約束(Constrained)
下一篇:返回列表
