日誌那點事兒——slf4j源碼剖析

日誌那點事兒——slf4j源碼剖析

http://www.cnblogs.com/xing901022/p/4149524.html

前言:

  說到日誌,大多人都沒空去研究,頂多知道用logger.info或者warn打打消息。那麼commons-logging,slf4j,logback,log4j,logging又是什麼關係呢?其中一二,且聽我娓娓道來。

  手碼不易,轉載請註明_xingoo!

  涉及到的內容:日誌系統的關係、Slf4j下載、源文件jar包的使用、Slf4j源碼分析、JVM類加載機制淺談

  首先八卦一下這個日誌家族的成員,下面這張圖雖然沒有包含全部的內容,但是基本也涵蓋了日誌系統的基本內容,不管怎麼說,先記住下面這張圖:

  通過上面的圖,可以簡單的理清關係!

  commons-logging和slf4j都是日誌的接口,供用戶使用,而沒有提供實現!

  log4j,logback等等纔是日誌的真正實現。

  當我們調用接口時,接口的工廠會自動尋找恰當的實現,返回一個實現的實例給我服務。這些過程都是透明化的,用戶不需要進行任何操作!

  這裏有個小故事,當年Apache說服log4j以及其他的日誌來按照commons-logging的標準編寫,但是由於commons-logging的類加載有點問題,實現起來也不友好,因此log4j的作者就創作了slf4j,也因此而與commons-logging兩分天下。至於到底使用哪個,由用戶來決定吧。

  這樣,slf4j出現了,它通過簡單的實現就能找到符合自己接口的實現類,如果不是滿足自己標準的日誌,可以通過一些中間實現比如上面的slf4j-log4j12.jar來進行適配。

  如此強大的功能,是如何實現的呢?

  

  slf4j下載

 

  首先爲了查閱源碼,這裏先教大家如何使用開源的jar包!

  例如在官網:http://www.slf4j.org/download.html

  這裏提供給我們兩個版本,linux下的tar.gz壓縮包,和windows下的zip壓縮包。

  下載zip文件後解壓,可以找到提供給我們的使用工具包。一般來說,這種開源的項目會爲我們提供兩種jar包,就拿slf4j(有人叫他,撒拉風four接,很有意思的名字)slf4j.jar、slf4j-source.jar:

  這裏slf4j-api-xxx.jar就是它的核心包,而slf4j-api-xxx-source.jar是它的源碼包,裏面包含了未編譯的java文件。

 

  那麼如何使用呢?

  

  首先在eclipse中添加外部的jar包,引入api.jar

  添加jar包,然後編輯sourceattachment,可以點擊edit,也可以雙擊

  引入source文件,這樣,我們就是查看api.jar包中的class文件的源碼了!

 

  接下來進入正題,slf4j源碼的解讀!

 

  首先日誌的用法很簡單,通過工廠factory獲取log對象,然後打印消息就可以了!看一下效果,無圖無真相!

  main的代碼在這裏:

複製代碼
 1 package com.xingoo.test;
 2 
 3 import java.util.Date;
 4 
 5 import org.slf4j.Logger;
 6 import org.slf4j.LoggerFactory;
 7 
 8 public class LogTest {
 9     public static void main(String[] args) {
10         Logger logger = LoggerFactory.getLogger(LogTest.class);
11         logger.info("hello {}",new Date());
12     }
13 }
複製代碼

  這裏也可以看到Slf4j的一個很重要的特性,佔位符!—— {} 可以任意的拼接字符串,自動的填入字符串中!用法用戶可以自己去嘗試,這裏就不再贅述了。

  爲了便於理解下面的代碼,推薦先了解一下facade外觀模式,因爲Slf4j就是利用外觀模式,提供對外的接口!

  可以參考之前整理的設計模式的帖子:設計模式

 

  首先,直接用LoggerFactory的靜態工廠獲取一個Logger對象,我們先看下getLogger方法!

1  public static Logger getLogger(Class clazz) {
2     return getLogger(clazz.getName());
3   }

  這裏把傳入的類,提取出名字,再填寫到getLogger靜態方法中!這裏博友們可能有一個疑問,爲什麼要獲取類的名字,而根據名字來獲取對象呢。因爲每個類使用的日誌處理實現可能不同,iLoggerFactory中也是根據名字來判斷一個類的實現方式的。

  public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
  }

  在getLogger方法中,通過getLoggerFactory獲取工廠,然後獲取日誌對象!看來一切的迷霧都在getILoggerFactory()中!

複製代碼
 1   public static ILoggerFactory getILoggerFactory() {
 2     if (INITIALIZATION_STATE == UNINITIALIZED) {
 3       INITIALIZATION_STATE = ONGOING_INITIALIZATION;
 4       performInitialization();
 5     }
 6     switch (INITIALIZATION_STATE) {
 7       case SUCCESSFUL_INITIALIZATION:
 8         return StaticLoggerBinder.getSingleton().getLoggerFactory();
 9       case NOP_FALLBACK_INITIALIZATION:
10         return NOP_FALLBACK_FACTORY;
11       case FAILED_INITIALIZATION:
12         throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
13       case ONGOING_INITIALIZATION:
14         // support re-entrant behavior.
15         // See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
16         return TEMP_FACTORY;
17     }
18     throw new IllegalStateException("Unreachable code");
19   }
20 }
複製代碼

  這個方法稍微複雜一點,總結起來:

  第2行~第5行:判斷是否進行初始化,如果沒有初始化,則修改狀態,進入performIntialization初始化!

  第6行~第17行:對狀態進行測試,如果初始化成功,則通過StaticLoggerBinder獲取日誌工廠!

  那麼下面就看一下Slf4j如何進行初始化,又是如何獲取日誌工廠的!

複製代碼
  private final static void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
      versionSanityCheck();
    }
  }
複製代碼

  在初始化中,先bind(),在修改狀態,進行版本檢查!先看一下版本檢查的內容:

複製代碼
  private final static void versionSanityCheck() {
    try {
      String requested = StaticLoggerBinder.REQUESTED_API_VERSION;

      boolean match = false;
      for (int i = 0; i < API_COMPATIBILITY_LIST.length; i++) {
        if (requested.startsWith(API_COMPATIBILITY_LIST[i])) {
          match = true;
        }
      }
      if (!match) {
        Util.report("The requested version " + requested
                + " by your slf4j binding is not compatible with "
                + Arrays.asList(API_COMPATIBILITY_LIST).toString());
        Util.report("See " + VERSION_MISMATCH + " for further details.");
      }
    } catch (java.lang.NoSuchFieldError nsfe) {
      // given our large user base and SLF4J's commitment to backward
      // compatibility, we cannot cry here. Only for implementations
      // which willingly declare a REQUESTED_API_VERSION field do we
      // emit compatibility warnings.
    } catch (Throwable e) {
      // we should never reach here
      Util.report("Unexpected problem occured during version sanity check", e);
    }
  }
複製代碼

  這裏獲取JDK的版本,並與Slf4j支持的版本進行比較,如果大版本相同則通過,如果不相同,那麼進行失敗提示!

  最關鍵的要看bind是如何實現的!

複製代碼
 1 private final static void bind() {
 2     try {
 3       Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
 4       reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
 5       // the next line does the binding
 6       StaticLoggerBinder.getSingleton();
 7       INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
 8       reportActualBinding(staticLoggerBinderPathSet);
 9       fixSubstitutedLoggers();
10     } catch (NoClassDefFoundError ncde) {
11       String msg = ncde.getMessage();
12       if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
13         INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
14         Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
15         Util.report("Defaulting to no-operation (NOP) logger implementation");
16         Util.report("See " + NO_STATICLOGGERBINDER_URL
17                 + " for further details.");
18       } else {
19         failedBinding(ncde);
20         throw ncde;
21       }
22     } catch (java.lang.NoSuchMethodError nsme) {
23       String msg = nsme.getMessage();
24       if (msg != null && msg.indexOf("org.slf4j.impl.StaticLoggerBinder.getSingleton()") != -1) {
25         INITIALIZATION_STATE = FAILED_INITIALIZATION;
26         Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
27         Util.report("Your binding is version 1.5.5 or earlier.");
28         Util.report("Upgrade your binding to version 1.6.x.");
29       }
30       throw nsme;
31     } catch (Exception e) {
32       failedBinding(e);
33       throw new IllegalStateException("Unexpected initialization failure", e);
34     }
35   }
複製代碼

  第2行~第10行:初始化!首先獲取實現日誌的加載路徑,查看路徑是否合法,再初始化StaticLoggerBinder的對象,尋找合適的實現方式使用。

  第10行~第22行:如果找不到指定的類,就會報錯!

  第22行~第31行:如果找不到指定的方法,就會報錯!

  第31行~第34行:報錯!

  通關查看代碼,可以理解,這個方法的主要功能就是尋找實現類,如果找不到或者指定的方法不存在,都會報錯提示!

  那麼如何查找實現類呢?這就要看findPossibleStaticLoggerBinderPathSet方法了!

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

  這裏就是slf4j的源碼精華之處!

  第1行:它定義了一個字符串,這個字符串是實現類的class地址。然後通過類加載加載指定的文件!

  第6行:創建一個Set,因爲有可能有多個實現。

  第8行~第9行:獲取LoggerFactory的類加載器!

  第11行~第13行:如果獲取不到類加載器則說明是系統加載器,那麼在系統路徑下獲取該資源文件

  第13行~第15行:獲取到了類加載器,則用該類加載器加載指定的資源文件。

  第17行~第20行:解析類加載器的地址。

  第24行:返回加載器地址的集合。

 

  這裏不瞭解類加載器的原理的可能會不大明白!

 

  在JVM中,最後的文件都是Class文件,也就是字節碼文件,因此需要把該文件加載到JVM中才能運行。而加載的過程,只會執行靜態代碼塊。

  類加載器分爲三種:BootStrapClassLoader加載器,ExtensionClassLoader標準擴展加載器,SystemClassLoader系統類加載器。

  每一種加載器加載指定的class文件會得到不同的類,因此爲了能夠使用,這裏必須要保證LoggerFactory的類加載器與StaticLoggerBinder的類加載是相同的。

  爲了避免不同的加載器加載後會出現不一致的問題,JVM採用了一種父類委託機制的實現方式,也就是說,用戶加載器會首先委託父類系統加載器,系統加載器再尋找父類——標準擴展加載器來加載類,而標準擴展加載器又會委託它的父類引導類加載器來加載,引導類加載器是屬於最高級別的類加載器,它是沒有父類加載器的。這裏可以通過一個簡單的圖來表示他們的關係:

  而用戶在運行期,也是獲取不到引導類加載器的,因此當一個類獲取它的類加載器,得到的對象時null,就說明它是由引導類加載器加載的。引導類加載器是負責加載系統目錄下的文件,因此源碼中使用getSystemresource來獲取資源文件。

  這個地方雖然有點繞,但是理解起來還應該算簡單吧!

  如果沒有理解加載器的機制,那麼推薦看一下《深入理解JVM》,或者推薦的帖子:類加載機制

 

  總結Slf4j工作原理

 

  上面的內容說多不多,說少也不少!

  你需要了解:JVM類加載機制、設計模式——外觀模式,Eclipse jar包使用,然後就是慢慢的閱讀源碼。

 

  簡單的說下它的原理,就是通過工廠類,提供一個用戶的接口!用戶可以通過這個外觀接口,直接使用API實現日誌的記錄。而後面的具體實現由Slf4j來尋找加載.尋找的過程,就是通過類加載加載那個叫org/slf4j/impl/StaticLoggerBinder.class的文件,只要實現了這個文件的日誌實現系統,都可以作爲一種實現方式。如果找到很多種方式,那麼就尋找一種默認的方式。

  這就是日誌接口的工作方式,簡單高效,關鍵是完全解耦!不需要日誌實現部分提供任何的修改配置,只需要符合接口的標準就可以加載進來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章