Java日誌框架:slf4j作用及其實現原理

五月的倉頡發表於2018-03-22

簡單回顧門面模式

slf4j是門面模式的典型應用,因此在講slf4j前,我們先簡單回顧一下門面模式,

門面模式,其核心為外部與一個子系統的通訊必須通過一個統一的外觀物件進行,使得子系統更易於使用。用一張圖來表示門面模式的結構為:

門面模式的核心為Facade即門面物件,門面物件核心為幾個點:

  • 知道所有子角色的功能和責任
  • 將客戶端發來的請求委派到子系統中,沒有實際業務邏輯
  • 不參與子系統內業務邏輯的實現

大致上來看,對門面模式的回顧到這裡就可以了,開始接下來對SLF4J的學習。

 

我們為什麼要使用slf4j

我們為什麼要使用slf4j,舉個例子:

我們自己的系統中使用了logback這個日誌系統
我們的系統使用了A.jar,A.jar中使用的日誌系統為log4j
我們的系統又使用了B.jar,B.jar中使用的日誌系統為slf4j-simple

這樣,我們的系統就不得不同時支援並維護logback、log4j、slf4j-simple三種日誌框架,非常不便。

解決這個問題的方式就是引入一個適配層,由適配層決定使用哪一種日誌系統,而呼叫端只需要做的事情就是列印日誌而不需要關心如何列印日誌,slf4j或者commons-logging就是這種適配層,slf4j是本文研究的物件。

從上面的描述,我們必須清楚地知道一點:slf4j只是一個日誌標準,並不是日誌系統的具體實現。理解這句話非常重要,slf4j只做兩件事情:

  • 提供日誌介面
  • 提供獲取具體日誌物件的方法

slf4j-simple、logback都是slf4j的具體實現,log4j並不直接實現slf4j,但是有專門的一層橋接slf4j-log4j12來實現slf4j。

為了更理解slf4j,我們先看例子,再讀原始碼,相信讀者朋友會對slf4j有更深刻的認識。

 

slf4j應用舉例

上面講了,slf4j的直接/間接實現有slf4j-simple、logback、slf4j-log4j12,我們先定義一個pom.xml,引入相關jar包:

 1 <!-- 原文:五月的倉頡http://www.cnblogs.com/xrq730/p/8619156.html -->
 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4       <modelVersion>4.0.0</modelVersion>
 5 
 6       <groupId>org.xrq.log</groupId>
 7       <artifactId>log-test</artifactId>
 8       <version>1.0.0</version>
 9       <packaging>jar</packaging>
10 
11       <name>log-test</name>
12       <url>http://maven.apache.org</url>
13 
14       <properties>
15         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16       </properties>
17 
18       <dependencies>
19         <dependency>
20             <groupId>junit</groupId>
21               <artifactId>junit</artifactId>
22               <version>4.11</version>
23               <scope>test</scope>
24         </dependency>
25         <dependency>
26             <groupId>org.slf4j</groupId>
27             <artifactId>slf4j-api</artifactId>
28             <version>1.7.25</version>
29         </dependency>
30         <dependency>
31             <groupId>ch.qos.logback</groupId>
32             <artifactId>logback-classic</artifactId>
33             <version>1.2.3</version>
34         </dependency>
35         <dependency>
36             <groupId>org.slf4j</groupId>
37             <artifactId>slf4j-simple</artifactId>
38             <version>1.7.25</version>
39         </dependency>
40         <dependency>
41             <groupId>log4j</groupId>
42             <artifactId>log4j</artifactId>
43             <version>1.2.17</version>
44         </dependency>
45         <dependency>
46             <groupId>org.slf4j</groupId>
47             <artifactId>slf4j-log4j12</artifactId>
48             <version>1.7.21</version>
49         </dependency>
50       </dependencies>
51 </project>

寫一段簡單的Java程式碼:

 1 @Test
 2 public void testSlf4j() {
 3     Logger logger = LoggerFactory.getLogger(Object.class);
 4     logger.error("123");
 5 }

接著我們首先把上面pom.xml的第30行~第49行註釋掉,即不引入任何slf4j的實現類,執行Test方法,我們看一下控制檯的輸出為:

看到沒有任何日誌的輸出,這驗證了我們的觀點:slf4j不提供日誌的具體實現,只有slf4j是無法列印日誌的

接著開啟logback-classic的註釋,執行Test方法,我們看一下控制檯的輸出為:

看到我們只要引入了一個slf4j的具體實現類,即可使用該日誌框架輸出日誌。

最後做一個測驗,我們把所有日誌開啟,引入logback-classic、slf4j-simple、log4j,執行Test方法,控制檯輸出為:

和上面的差別是,可以輸出日誌,但是會輸出一些告警日誌,提示我們同時引入了多個slf4j的實現,然後選擇其中的一個作為我們使用的日誌系統。

從例子我們可以得出一個重要的結論,即slf4j的作用:只要所有程式碼都使用門面物件slf4j,我們就不需要關心其具體實現,最終所有地方使用一種具體實現即可,更換、維護都非常方便

 

slf4j實現原理

上面看了slf4j的示例,下面研究一下slf4j的實現,我們只關注重點程式碼。

slf4j的用法就是常年不變的一句"Logger logger = LoggerFactory.getLogger(Object.class);",可見這裡就是通過LoggerFactory去拿slf4j提供的一個Logger介面的具體實現而已,LoggerFactory的getLogger的方法實現為:

 1 public static Logger getLogger(Class<?> clazz) {
 2     Logger logger = getLogger(clazz.getName());
 3     if (DETECT_LOGGER_NAME_MISMATCH) {
 4         Class<?> autoComputedCallingClass = Util.getCallingClass();
 5         if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
 6             Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
 7                             autoComputedCallingClass.getName()));
 8             Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
 9         }
10     }
11     return logger;
12 }

從第2行開始跟程式碼,一直跟到LoggerFactory的bind()方法:

 1 private final static void bind() {
 2     try {
 3         Set<URL> staticLoggerBinderPathSet = null;
 4         // skip check under android, see also
 5         // http://jira.qos.ch/browse/SLF4J-328
 6         if (!isAndroid()) {
 7             staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
 8             reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
 9         }
10         // the next line does the binding
11         StaticLoggerBinder.getSingleton();
12         INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
13         reportActualBinding(staticLoggerBinderPathSet);
14         fixSubstituteLoggers();
15         replayEvents();
16         // release all resources in SUBST_FACTORY
17         SUBST_FACTORY.clear();
18     } catch (NoClassDefFoundError ncde) {
19         String msg = ncde.getMessage();
20         if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
21             INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
22             Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
23             Util.report("Defaulting to no-operation (NOP) logger implementation");
24             Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
25         } else {
26             failedBinding(ncde);
27             throw ncde;
28         }
29     } catch (java.lang.NoSuchMethodError nsme) {
30         String msg = nsme.getMessage();
31         if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
32             INITIALIZATION_STATE = FAILED_INITIALIZATION;
33             Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
34             Util.report("Your binding is version 1.5.5 or earlier.");
35             Util.report("Upgrade your binding to version 1.6.x.");
36         }
37         throw nsme;
38     } catch (Exception e) {
39         failedBinding(e);
40         throw new IllegalStateException("Unexpected initialization failure", e);
41     }
42 }

這個地方第7行是一個關鍵,看一下程式碼:

 1 static Set<URL> findPossibleStaticLoggerBinderPathSet() {
 2     // use Set instead of list in order to deal with bug #138
 3     // LinkedHashSet appropriate here because it preserves insertion order
 4     // during iteration
 5     Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
 6     try {
 7         ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
 8         Enumeration<URL> paths;
 9         if (loggerFactoryClassLoader == null) {
10             paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
11         } else {
12             paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
13         }
14         while (paths.hasMoreElements()) {
15             URL path = paths.nextElement();
16             staticLoggerBinderPathSet.add(path);
17         }
18     } catch (IOException ioe) {
19         Util.report("Error getting resources from path", ioe);
20     }
21     return staticLoggerBinderPathSet;
22 }

這個地方重點其實就是第12行的程式碼,getLogger的時候會去classpath下找STATIC_LOGGER_BINDER_PATH,STATIC_LOGGER_BINDER_PATH值為"org/slf4j/impl/StaticLoggerBinder.class",即所有slf4j的實現,在提供的jar包路徑下,一定是有"org/slf4j/impl/StaticLoggerBinder.class"存在的,我們可以看一下:

我們不能避免在系統中同時引入多個slf4j的實現,所以接收的地方是一個Set。大家應該注意到,上部分在演示同時引入logback、slf4j-simple、log4j的時候會有警告:

這就是因為有三個"org/slf4j/impl/StaticLoggerBinder.class"存在的原因,此時reportMultipleBindingAmbiguity方法控制檯輸出語句:

1 private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
2     if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
3         Util.report("Class path contains multiple SLF4J bindings.");
4         for (URL path : binderPathSet) {
5             Util.report("Found binding in [" + path + "]");
6         }
7         Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
8     }
9 }

那網友朋友可能會問,同時存在三個"org/slf4j/impl/StaticLoggerBinder.class"怎麼辦?首先確定的是這不會導致啟動報錯,其次在這種情況下編譯期間,編譯器會選擇其中一個StaticLoggerBinder.class進行繫結,這個地方sfl4j也在reportActualBinding方法中報告了繫結的是哪個日誌框架:

1 private static void reportActualBinding(Set<URL> binderPathSet) {
2     // binderPathSet can be null under Android
3     if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
4         Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
5     }
6 }

對照上面的截圖,看最後一行,確實是"Actual binding is of type..."這句。

最後StaticLoggerBinder就比較簡單了,不同的StaticLoggerBinder其getLoggerFactory實現不同,拿到ILoggerFactory之後呼叫一下getLogger即拿到了具體的Logger,可以使用Logger進行日誌輸出。

相關文章