天天看點

深入源碼之SLF4J

commons logging+log4j一直是java日志的經典組合,以至于很多伺服器都使用了類似的配置,像websphere、以前的tomcat都使用commons logging作為日志輸出架構,而據說jboss則直接commons logging和log4j一起使用了(這個估計是為了解決commons logging中經常在這類伺服器上遇到的classloader問題)。然而log4j的開發團隊對commons logging貌似不滿意(可以從log4j manual中看出一些端倪),因而log4j團隊開發了自己的日志門面架構slf4j(simple logging façade for java),貌似他們對自己開發的log4j的性能也不滿意,然後又弄出了個logback,關鍵執行語句的性能要比log4j快10倍以上(官網資料,我本人還沒有仔細看過logback的代碼,更沒有測試過,不知道具體性能能提高多少),這是後話,等過幾年看logback代碼後再仔細讨論。

以我個人了解,slf4j的出現是為了解決commons logging存在的兩個問題:

1.    commons logging存在的classloader問題,即當伺服器本身引入commons logging時,如果在伺服器中載入commons logging包,則該包中的類由伺服器的classloader加載,進而在加載log4j中的logger類時會出現classnotfoundexception,因為伺服器中的classloader沒法找到web app下的jar包。對于父classloader優先的類加載機制來說,目前的一個解決方案是使用commons-logging-api-1.1.1.jar包,該包的log實作類隻包含jdk14logger、simplelog、nooplog三個類,因而在加載這幾個類時會使用web app中的commons logging包,進而解決classnotfoundexception的問題,然而這種方式對那些實作child classloader first的伺服器來說,由webclassloader父classloader加載的類在使用日志時會存在問題,因為它們的log接口由加載自身類的classloader加載,而log4jlogger類卻由webclassloader加載。具體關于commons logging中存在的問題我會在另外一篇文章中詳細說明。

2.    在使用commons logging時,我們經常會看到以下方法的寫法:

if (logger.isdebugenabled()) {

    logger.info("loading xml bean definitions from " + encodedresource.getresource());

}

存在isdebugenabled()的判斷邏輯是為了在避免多餘的字元串拼接,即如果不存在isdebugenabled()判斷,即使目前日志級别為error時,在遇到logger.info()調用時,它還會先拼接日志消息的字元串,然後進入該方法内,才發現這個日志語句不用列印。而這種多餘的拼接不僅浪費了多餘的cpu操作,而且會增加gc的負擔。slf4j則提供以下的方式來解決這個問題:

logger.info("loading xml bean definitions from {}", encodedresource.getresource());

類似commons logging,slf4j在使用時通過loggerfactory得到命名的logger執行個體,然後通過該logger執行個體調用相應的方法列印日志:

final logger logger = loggerfactory.getlogger("levin.logging.slf4j");

logger.info("using slf4j, current time is {}", new date());

然而不同于commons logging的動态綁定機制,slf4j則采用了一種靜态綁定的機制,即每個支援slf4j的logging架構必須存在一個繼承自loggerfactorybinder接口的staticloggerbinder類:

public interface loggerfactorybinder {

 public iloggerfactory getloggerfactory();

   public string getloggerfactoryclassstr();

loggerfactory調用staticloggerbinder類中的getloggerfactory()方法傳回相應的iloggerfactory執行個體:

public interface iloggerfactory {

 public logger getlogger(string name);

最後通過iloggerfactory執行個體擷取logger執行個體:

public interface logger {

   public string getname();

   ...(trace)

   public boolean isdebugenabled();

   public void debug(string msg);

   public void debug(string format, object arg);

   public void debug(string format, object arg1, object arg2);

   public void debug(string format, object... arguments);

   public void debug(string msg, throwable t);

   public boolean isdebugenabled(marker marker);

   public void debug(marker marker, string msg);

   public void debug(marker marker, string format, object arg);

   public void debug(marker marker, string format, object arg1, object arg2);

   public void debug(marker marker, string format, object... arguments);

   public void debug(marker marker, string msg, throwable t);

   ...(info)

   ...(warn)

   ...(error)

也正是因為這個設計,slf4j在classpath下隻支援一個橋接包(slf4j-simple-<version>.jar、slf4j-log4j12-<version>.jar、slf4j-jdk14-<version>.jar、logback-classic-<version>.jar等)。如果在classpath下存在多個橋接包,則具體用哪個就要看這幾個橋接包的加載順序了,實際中會使用先加載的橋接包。同時slf4j會列印使用哪個橋接包,哪些橋接包沒有使用。這種靜态綁定的設計比commons logging在可擴充性上具有更加靈活的機制,對“可插拔”的支援也更加高效。如果要支援一個新的logging架構,commons logging需要通過在屬性配置檔案、或虛拟機屬性中配置支援這個新的logging架構的實作類(實作log接口);而slf4j則隻需要編寫一個五個相應的類:

1.    實作logger接口的類

2.    實作iloggerfactory接口的類

3.    實作loggerfactorybinder接口的類staticloggerbinder類(必須使用staticloggerbinder類名),并且存在一個靜态的getsingleton()方法。

4.    實作markerfactorybinder類的staticmarkerbinder類(必須使用staticmarkerbinder類名),可選。一般也會存在一個靜态的singleton字段,不過也是可選的。

5.    實作staticmdcbinder類,可選。一般也會存在一個靜态的singleton字段,也可選。

slf4j的類設計也相對比較簡單(也感覺有點零散):

深入源碼之SLF4J

由于采用了靜态綁定的方式,而不是像commons logging中的動态綁定,slf4j中loggerfactory的實作要比commons logging中logfactory的實作要簡單的多。即loggerfactory調用getiloggerfactory()方法,該方法會初始化loggerfactory,即通過在bind()方法中加載classpath中的staticloggerbinder類,并根據加載結果設定目前loggerfactory的初始化狀态,進而在getiloggerfactory()方法中通過目前loggerfactory的狀态判斷傳回的iloggerfactory執行個體。簡單的示意圖如下:

深入源碼之SLF4J

bind()方法的主要源碼如下:

 private final static void bind() {

    try {

      ...

      //實作綁定

      staticloggerbinder.getsingleton();

      initialization_state = successful_initialization;

    } catch (noclassdeffounderror ncde) {

      string msg = ncde.getmessage();

      //判斷是否是因為沒有找到staticloggerbinder類引起的異常

      //此時,使用noploggerfactory類傳回給getiloggerfactory(),不列印任何日志

      if (messagecontainsorgslf4jimplstaticloggerbinder(msg)) {

        initialization_state = nop_fallback_initialization;

        ...

      } else {

        // initialization_state = failed_initialization

        failedbinding(ncde);

        throw ncde;

      }

    } catch (java.lang.nosuchmethoderror nsme) {

      string msg = nsme.getmessage();

      if (msg != null && msg.indexof("org.slf4j.impl.staticloggerbinder.getsingleton()") != -1) {

        initialization_state = failed_initialization;

      throw nsme;

    } catch (exception e) {

      //initialization_state = failed_initialization;

      failedbinding(e);

      throw new illegalstateexception("unexpected initialization failure", e);

    }

 }

即bind()方法使用調用staticloggerbinder.getsingleton()方法來實作綁定,如果該方法調用成功,則将初始化狀态設定為successful_initialization,如果因為沒有找到staticloggerbinder類而引起的異常,則将狀态設定為nop_fallback_initialization,否則将狀态設定為failed_initialization,并抛出異常。如果在目前classpath下存在多個橋接jar包,在實作綁定前後會記錄存在哪些可使用的橋接jar包,綁定了那個iloggerfactory類。

在bind()傳回後,performinitialization()方法會再做一些版本檢查,即staticloggerbinder可以定義一個靜态的requested_api_version字段,表示該staticloggerbinder支援的slf4j版本,如果該版本不在loggerfactory定義的相容版本清單中(api_compatibility_list),slf4j會列印警告資訊,并列出目前loggerfactory相容的版本清單。而後在getiloggerfactory()方法中會根據目前loggerfactory的初始化狀态來決定傳回的iloggerfactory執行個體:

 public static iloggerfactory getiloggerfactory() {

    if (initialization_state == uninitialized) {

      initialization_state = ongoing_initialization;

      performinitialization();

    switch (initialization_state) {

      case successful_initialization:

        return staticloggerbinder.getsingleton().getloggerfactory();

      case nop_fallback_initialization:

        return nop_fallback_factory;

      case failed_initialization:

        throw new illegalstateexception(unsuccessful_init_msg);

      case ongoing_initialization:

        // support re-entrant behavior.

        // see also http://bugzilla.slf4j.org/show_bug.cgi?id=106

        return temp_factory;

    throw new illegalstateexception("unreachable code");

當loggerfactory成功初始化,則傳回綁定的staticloggerbinder中的iloggerfactory執行個體;如果為nop_fallback_initialization(沒有找到橋接jar),則傳回noploggerfactory,它傳回一個單例的noplogger執行個體,該類不會列印任何日志;如果初始化狀态為failed_initialization,抛出illegalstateexception異常;如果初始化狀态為ongoing_initialization,則傳回substituteloggerfactory類執行個體,該狀态發生在一個線程正在初始化loggerfactory,而另一個線程已經開始請求擷取iloggerfactory執行個體,substituteloggerfactory會記錄目前請求的logger名稱,然後傳回noplogger執行個體。所有這些在loggerfactory初始化時被忽略的logger name會在loggerfactory初始化成功以後被report出來(在system.err流中列印出來)。

      slf4j實作了一個簡單的日志系統:slf4j-simple-<version>.jar。要實作一個相容slf4j的日志系統,基本的需要三個類:

1.    staticloggerbinder類,實作loggerfactorybinder接口。它實作單例模式,存在getsingleton()靜态方法,存在requested_api_verion靜态字段,不用final避免編譯器的優化(将值直接寫入源碼中,而不使用該字段)。傳回的iloggerfactory執行個體也一直使用同一個執行個體(simpleloggerfactory)。

public class staticloggerbinder implements loggerfactorybinder {

 private static final staticloggerbinder singleton = new staticloggerbinder();

 public static final staticloggerbinder getsingleton() {

    return singleton;

 // to avoid constant folding by the compiler, this field must *not* be final

 public static string requested_api_version = "1.6.99"; // !final

 private static final string loggerfactoryclassstr = simpleloggerfactory.class.getname();

 private final iloggerfactory loggerfactory;

 private staticloggerbinder() {

    loggerfactory = new simpleloggerfactory();

 public iloggerfactory getloggerfactory() {

    return loggerfactory;

 public string getloggerfactoryclassstr() {

    return loggerfactoryclassstr;

 }  

2.    實作iloggerfactory接口的simpleloggerfactory。它有一個loggermap字段緩存所有之前建立的simplelogger執行個體,以logger name為key,實作每個相同名字的logger執行個體隻需要建立一次。

public class simpleloggerfactory implements iloggerfactory {

 final static simpleloggerfactory instance = new simpleloggerfactory();

 map loggermap;

 public simpleloggerfactory() {

    loggermap = new hashmap();

 public logger getlogger(string name) {

    logger slogger = null;

    // protect against concurrent access of the loggermap

    synchronized (this) {

      slogger = (logger) loggermap.get(name);

      if (slogger == null) {

        slogger = new simplelogger(name);

        loggermap.put(name, slogger);

    return slogger;

3.    simplelogger類,實作logger接口。simplelogger繼承自markerignoringbase類,該基類忽略所有存在marker參數的日志列印方法。simplelogger将日志級别分成五個級别:trace、debug、info、warn、error,這些級别對應的int值一次增大。simplelogger還支援對simplelogger.properties配置檔案的解析,它支援的key值有:

org.slf4j.simplelogger.defaultloglevel

org.slf4j.simplelogger.showdatetime

org.slf4j.simplelogger.datetimeformat

org.slf4j.simplelogger.showthreadname

org.slf4j.simplelogger.showlogname

org.slf4j.simplelogger.showshortlogname

org.slf4j.simplelogger.logfile

org.slf4j.simplelogger.levelinbrackets

org.slf4j.simplelogger.warnlevelstring(warn提示字元,預設“warn”)

同時simplelogger還支援為特定的logger name字首(以”.”作為分隔符)指定level:

org.slf4j.simplelogger.log.<lognameprefix>

并且所有這些key都可以定義在系統屬性中。

simplelogger類的實作主要分成兩步:初始化和列印日志:

a.    初始化

加載配置檔案,使用加載的配置檔案初始化類字段,即對應以上simplelogger.properties支援的key;儲存目前logger name;計算目前logger實際的level,即如果沒有為該logger name(或其以“.”分隔的字首)配置特定的level,則使用預設配置的level,否則,使用具體的日志,并儲存計算出的level值,如果沒有找到level配置,使用預設值info。

b.    列印日志

對使用format字元串的日志列印方法,調用formatandlog()方法,其内部先調用messageformatter.arrayformat()方法,然後調用log()方法實作列印資訊。log()方法的實作隻是根據解析出來的配置資訊,判斷哪些資訊需要列印,則列印這些資訊,實作比較簡單,不再贅述。

并不是所有logging系統支援這些功能,對它們支援最全面的當屬logback架構了,因而這些類将會在介紹logback架構時一起讨論。在slf4j-simple-<version>.jar,staticmarkerbinder傳回basicmarkerfactory執行個體,而staticmdcbinder傳回nopmdcadapter執行個體。

如slf4j-log4j12-<version>.jar,slf4j-jdk14-<version>.jar,slf4j-jcl-<version>.jar等,它們的實作類似slf4j-simple-<version>.jar的實作,并且更加簡單,因而它們對logger的實作将大部分的邏輯代理給了底層實作架構,因而這裡不再贅述。

slf4j支援上層是slf4j架構,底層還是通過commons logging的動态查找機制,隻要将slf4j-jcl-<version>.jar包加入classpath中即可(當然slf4j-api-<version>.jar也要存在)。

另外slf4j還支援上層是commons logging,而底層交給slf4j提供的靜态綁定機制查找真正的日志實作架構,隻需要将jcl-over-slf4j-<version>.jar包加入到classpath中,此時不需要引入commons-logging-<version>.jar包。它的實作隻是重寫了commons logging架構,并在logfactory中隻使用slf4jlog或slf4jlocationawarelog類。

不過需要注意,slf4j-jcl-<version>.jar包和jcl-over-slf4j-<version>.jar兩個包不能同時出現在classpath中,不然會引起循環調用而導緻棧溢出的問題,因而slf4j-jcl-<version>.jar在初始化時就會檢測這個限制,并抛出異常。

最後slf4j還支援log4j作為上層,而底層交給slf4j靜态綁定要真正實作日志列印的架構,可以将log4j-over-slf4j-<version>.jar包加入到classpath中。其實作也類似jcl-over-slf4j-<version>.jar的實作,重寫大部分的log4j的内部邏輯,而在logger類實作中,将真正的日志列印邏輯代理給slf4j的loggerfactory。

最後給我現在在公司開發的這個系統吐個槽,我們隊日志并沒有統一的管理,有人用commons logging,也有人直接用log4j,其實所有代碼都沒有統一管理,很是換亂,不過slf4j竟然可以滿足這種情況的遷移,即可以将log4j-over-slf4j-<version>.jar和jcl-over-slf4j-<version>.jar包同時放到classpath下。而到這個時候我才意識到為什麼slf4j為什麼會慢慢的使用那麼廣泛了。