1 SLF4J介紹
SLF4J即Simple Logging Facade for Java,它提供了Java中所有日志框架的簡單外觀或抽象。因此,它使用戶能夠使用單個依賴項處理任何日志框架,例如:Log4j,Logback和JUL(java.util.logging)。通過在類路徑中插入適當(dāng)?shù)?jar 文件(綁定),可以在部署時插入所需的日志框架。如果要更換日志框架,僅僅替換依賴的slf4j bindings。比如,從java.util.logging替換為log4j,僅僅需要用slf4j-log4j12-1.7.28.jar替換slf4j-jdk14-1.7.28.jar。
2 SLF4J源碼分析
我們通過代碼入手,層層加碼,直觀感受SLF4J打印日志,并跟蹤代碼追本溯源。主要了解,SLF4J是如何作為門面和其他日志框架進(jìn)行解耦。
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 執(zhí)行一個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 將默認(rèn)為無操作實現(xiàn)
2.1.3 跟蹤源碼
點開方法getLogger(),可以直觀看到LoggerFactory使用靜態(tài)工廠創(chuàng)建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();
}
}
進(jìn)一步分析綁定方法findPossibleStaticLoggerBinderPathSet(),可以發(fā)現(xiàn)在當(dāng)前ClassPath下查詢了所有該路徑的資源“org/slf4j/impl/StaticLoggerBinder.class”,這里可能沒有加載到任何文件,也可能綁定多個,對沒有綁定和綁定多個的場景進(jìn)行了友好提示。這里通過路徑加載資源的目的主要用來對加載的各種異常場景提示。
再往下代碼StaticLoggerBinder.getSingleton()才是實際的綁定,并且獲取StaticLoggerBinder的實例。這里如果反編譯,你會發(fā)現(xiàn)根本沒有這個類StaticLoggerBinder。
如果沒有加載到文件,正如上邊demo執(zhí)行的結(jié)果一樣,命中NoSuchMethodError異常,并打印沒有綁定場景的提示信息。
方法findPossibleStaticLoggerBinderPathSet()的源碼如下,可以發(fā)現(xiàn)類加載器通過路徑獲取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 執(zhí)行demo
可以看到正常的打印日志信息,并且沒有任何異常
2.2.2 跟蹤源碼
這個時候如果再點擊進(jìn)入方法StaticLoggerBinder.getSingleton(),發(fā)現(xiàn)類StaticLoggerBinder是由包logback-classic提供的,并且實現(xiàn)了SLF4J中的接口LoggerFactoryBinder。StaticLoggerBinder的創(chuàng)建用到了單例模式,該類主要目的返回一個創(chuàng)建Logger的工廠。這里實際返回了ch.qos.logback.classic.LoggerContext的實例,再由該實例創(chuàng)建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 執(zhí)行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 執(zhí)行demo
根據(jù)日志打印結(jié)果,可以看到實際綁定的是org.apache.logging.slf4j.Log4jLoggerFactory;但是沒有正常打印出日志,需要進(jìn)行l(wèi)og4j2的日志配置。說明實際綁定的是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,發(fā)現(xiàn)壓根沒有這個類org.slf4j.impl.StaticLoggerBinder,他怎么會編譯成功呢?猜想是不是打包的時候把這個類排除掉了呢?通過git下載源碼發(fā)現(xiàn)slf4j源碼其實是有這個文件的,org/slf4j/impl/StaticLoggerBinder.class;這里使用了一個小技巧,打包的時候把實現(xiàn)類排除掉了,雖然不太優(yōu)雅,但是思路很巧妙。
2.5.2 slf4j-api-2.0.0版本引入SPI(Service Provider Interface)
該版本通過使用SPI方式進(jìn)行實現(xiàn)類的加載,感覺比之前的實現(xiàn)方式優(yōu)雅了很多。橋接包只需要在這個位置:META-INF/services/,定義一個文件org.slf4j.spi.SLF4JServiceProvider(命名為SLFJ4提供的接口名),并且文件中指定實現(xiàn)類。只要引入這個橋接包,就可以適配到對應(yīng)實現(xiàn)的日志框架。
以下是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官方已經(jīng)實現(xiàn)綁定的日志框架
slf4j已經(jīng)提供了常用日志框架的橋接包,以及詳細(xì)的文檔描述,使用起來非常簡單。
下圖是SLF4J官網(wǎng)中提供的,表示了各種日志實現(xiàn)框架和SLF4J的關(guān)系:
2.7 總結(jié)
- SLF4J API旨在一次綁定一個且僅一個底層日志框架。而且引入SLF4J后,不管是否可以加載到StaticLoggerBinder,或者加載到多個StaticLoggerBinder,都進(jìn)行友好提示,用戶體驗上考慮都很周到。如果類路徑上存在多個綁定,SLF4J 將發(fā)出警告,列出這些綁定的位置。當(dāng)類路徑上有多個綁定可用時,應(yīng)該選擇一個希望使用的綁定,然后刪除其他綁定。
- 單純看SLF4J源碼,其實整體設(shè)計實現(xiàn)上都很簡單明確,定位非常清楚,就是做好門面。
- 鑒于 SLF4J 接口及其部署模型的簡單性,新日志框架的開發(fā)人員應(yīng)該會發(fā)現(xiàn)編寫 SLF4J 綁定非常容易。
- 對于目前比較主流的日志框架都通過實現(xiàn)適配進(jìn)行兼容支持。只要用戶選擇了SLF4J,就可以確保以后變更日志框架的自由。
3 SLF4J設(shè)計模式的使用
在slf4j中用到了一些經(jīng)典的設(shè)計模式,比如門面模式、單例模式、靜態(tài)工廠模式等,我們來分析以下幾種設(shè)計模式。
3.1 門面模式(Facade Pattern)
1)解釋
門面模式,也叫外觀模式,要求一個子系統(tǒng)的外部與其內(nèi)部的通信必須通過一個統(tǒng)一的對象進(jìn)行。門面模式提供一個高層次的接口,使得子系統(tǒng)更易于使用。使用了門面模式,使客戶端調(diào)用變得更加簡單。
Slf4j制定了log日志的使用標(biāo)準(zhǔn),提供了高層次的接口, 我們編碼過程只需要依賴接口Logger和工廠類 LoggerFactory就可以實現(xiàn)日志的打印,完全不用關(guān)心日志內(nèi)部的實現(xiàn)細(xì)節(jié)是logback實現(xiàn)的方式,還是log4j的實現(xiàn)方式。
2)圖解
Logger logger = LoggerFactory.getLogger(HelloSlf4j.class);
logger.info("Hello World info");
3)優(yōu)點
解耦,減少系統(tǒng)的相互依賴。所有的依賴都是對門面對象的依賴,與子系統(tǒng)無關(guān),業(yè)務(wù)層的開發(fā)不需要關(guān)心底層日志框架的實現(xiàn)及細(xì)節(jié),在編碼的時候也不需要考慮日后更換框架所帶來的成本。
接口和實現(xiàn)分離,屏蔽了底層的實現(xiàn)細(xì)節(jié),面向接口編程。
3.2 單例模式(Singleton Pattern)
1)解釋
單例模式,確保一個類僅有一個實例,并提供一個訪問它的全局訪問點。
在SLF4J的適配包中都需要實現(xiàn)類StaticLoggerBinder,而類StaticLoggerBinder的實現(xiàn)就用了單例模式,而且是最簡單的實現(xiàn)方法,在靜態(tài)初始化器中直接new StaticLoggerBinder(),提供全局訪問方法獲取該實例。
2)UML圖
3)優(yōu)點
在單例模式中,活動的單例只有一個實例,對單例類的所有實例化得到的都是相同的一個實例。這樣就防止其它對象對自己的實例化,確保所有的對象都訪問一個實例
單例模式具有一定的伸縮性,類自己來控制實例化進(jìn)程,類就在改變實例化進(jìn)程上有相應(yīng)的伸縮性。
提供了對唯一實例的受控訪問。
在內(nèi)存里只有一個實例,減少了內(nèi)存的開銷,提高系統(tǒng)的性能。
4 啟示
- 盡管SLF4J整體代碼短小但很精煉,可見門面模式運用好的威力。門面模式也為我們提供了對于多版本的實現(xiàn)如何統(tǒng)一定義接口以及兼容提供了參考。
- SLF4J定義和實現(xiàn)方案對用戶都很友好,同時又提供了各種橋接包,進(jìn)行完善的文檔指導(dǎo)使用??傊黜椨脩趔w驗都很棒,這也許也是SLF4J目前最受歡迎的原因之一吧。
- 我們要多思考面向接口編程的思想,降低代碼耦合度,提高代碼擴(kuò)展性。
- 使用SPI的方式,優(yōu)雅的加載擴(kuò)展實現(xiàn)。
- 好產(chǎn)品是設(shè)計出來的,更是優(yōu)化迭代出來的。
5 參考資料
- slf4j官網(wǎng):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
作者:京東物流 曹俊文章來源:http://www.zghlxwxcb.cn/news/detail-493301.html
來源:京東云開發(fā)者社區(qū)文章來源地址http://www.zghlxwxcb.cn/news/detail-493301.html
到了這里,關(guān)于SLF4J門面日志框架源碼探索的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!