slf4j介紹以及實現原理窺探

阿豪聊乾貨發表於2016-05-12

一、概述

  slf4j(全稱是Simple Loging Facade For Java)是一個為Java程式提供日誌輸出的統一介面,並不是一個具體的日誌實現方案,就好像我們經常使用的JDBC一樣,只是一種規則而已。因此單獨的slf4j是不能工作的,它必須搭配其他具體的日誌實現方案,比如apacheorg.apache.log4j.Logger,jdk自帶的java.util.logging.Logger等等。

  其中對與jar包:

    slf4j-log4j12-x.x.x.jar是使用org.apache.log4j.Logger提供的驅動

    slf4j-jdk14-x.x.x.jar是使用java.util.logging提供的驅動

    slf4j-simple-x.x.x.jar直接繫結System.err

    slf4j-jcl-x.x.x.jar是使用commons-logging提供的驅動

    logback-classic-x.x.x.jar是使用logback提供的驅動

二、slf4j優勢

  1.與客戶端很好的解耦

    比如:我們發現了一位大牛開發了一個非常好而且又剛好能夠滿足自己需求的類庫,類庫裡使用了apacheorg.apache.log4j.Logger,然而你自己的程式在開發的時候使用的是jdk自帶的java.util.logging.Logger,那麼現在憂傷的問題來了:如果你想要使用,你是不是需要同時支援log4j和jdk兩種日誌系統?這樣的話,你就需要新增兩個實現同樣功能的jar包並且維護兩套日子配置,你是不是需要耗費更多的精力來進行維護?此時寶寶心裡苦,寶寶不說。

  2.節省記憶體

    log4j這些傳統的日誌系統裡面並沒有佔位符的概念,當我們需要列印資訊的時候,我們需要如下方式進行使用。

 1 package com.hafiz.zhang;
 2 
 3 import org.apache.log4j.Logger;
 4 
 5 /**
 6  * @author hafiz.zhang
 7  * @date 16/5/12 18:01
 8  */
 9 public class TestLog4j {
10 private static final Logger LOGGER = Logger.getLogger(TestLog4j.class);
11 
12 public static void main(String[] args) {
13         String message = "伺服器出錯啦.";
14         LOGGER.info("Error message is : " + message);
15     }
16 }

檢視原始碼,我們發現了log4j的info函式有兩種方式可供選擇:

1 public void info(Objectmessage)
2 public void info(Objectmessage, Throwable t)

第一個引數是要輸出的資訊,假設我們要輸出的是一個字串,並且字串中包含變數,則Objectmessage引數就必須使用字串相加操作,就比如上面測試程式碼的14行一樣。姑且不說字串相加是一個比較消耗效能的操作,字串是一個不可變物件,一旦建立就不能被修改,建立的字串會儲存在String池中,佔用記憶體。更糟糕的是如果配置檔案中配置的日誌級別是ERROR的話,這行info日誌根本不會輸出,則相加得到的字串物件是一個非必須物件,白白浪費了記憶體空間。這時候有人會說了,那我可以這樣寫啊:

 1 package hafiz.zhang;
 2  
 3  import org.apache.log4j.Logger;
 4  
 5  /**
 6   * @author hafiz.zhang
 7   * @date 16/5/12 18:04
 8   */
 9  public class TestLog4j {
10  private static final Logger LOGGER = Logger.getLogger(TestLog4j.class);
11  
12  public static void main(String[] args) {
13          String message = "伺服器出錯啦.";
14  if (LOGGER.isInfoEnabled()) {
15              LOGGER.info("Error message is: " + message);
16          }
17      }
18  }

 

這樣不就解決了白白浪費記憶體的問題了嗎?沒錯,這是一個變通方案,但是這樣的程式碼太繁瑣,不直觀!

下面再來看看slf4j的打日誌的方式:(爽爆了)

 1 package com.hafiz.zhang;
 2 
 3 
 4 import org.slf4j.Logger;
 5 import org.slf4j.LoggerFactory;
 6 
 7 /**
 8  * @author hafiz.zhag
 9  * @date 15/8/26 21:54
10  */
11 public class TestLog4j {
12 private static final Logger LOGGER = LoggerFactory.getLogger(TestLog4j.class);
13 
14 public static void main(String[] args) {
15         String message = "伺服器出錯啦.";
16         LOGGER.info("Error message is: {}", message);
17     }
18 }

看到沒有,打日誌的時候使用了{}佔位符,這樣就不會有字串拼接操作,減少了無用String物件的數量,節省了記憶體。並且記住,在生產最終日誌資訊的字串之前,這個方法會檢查一個特定的日誌級別是不是開啟了,這不僅降低了記憶體消耗而且預先降低了CPU去處理字串連線命令的時間。這裡是使用SLF4J日誌方法的程式碼,來自於slf4j-log4j12-1.6.1.jar中的Log4j的介面卡類Log4jLoggerAdapter。

三、slf4j的使用方法以及實現原理

  上面我們提到了slf4j是不能夠獨立工作的,要想使用我們必須帶上其他的具體日誌實現方案,下面我們就以log4j為例進行使用slf4j,我們需要做的工作如下:(下面的xxx表示jar包具體版本號)

    1.將slf4j-api-xxx.jar加入工程classpath中

    2.將slf4j-log4jxx-xxx.jar加入工程classpath中

    3.將log4j-xxx.jar加入工程classpath中

    4.將log4j.properties(log4j.xml)檔案加入工程classpath中(與spring繼承 還能使用自定義檔案位置的方式指定,後續部落格中我會介紹)

    注:如果專案是maven專案,則前三步就變成一步,在pom.xml檔案中新增以下依賴。(如果沒有更高版本的slf4j-api和log4j要求,則只新增第一條依賴就可以,因為slf4j-log4j12依賴會包含slf4j-api和log4j依賴

 1 <dependency>
 2    <groupId>org.slf4j</groupId>
 3    <artifactId>slf4j-log4j12</artifactId>
 4    <version>1.6.4</version>
 5 </dependency>
 6 <dependency>
 7    <groupId>org.slf4j</groupId>
 8    <artifactId>slf4j-api</artifactId>
 9    <version>1.6.4</version>
10 </dependency>
11 <dependency>
12    <groupId>log4j</groupId>
13    <artifactId>log4j</artifactId>
14    <version>1.2.16</version>
15 </dependency>

  slf4j工作原理窺探

    首先,slf4j-api作為slf4j的介面類,使用在程式程式碼中,這個包提供了一個Logger類和LoggerFactory類,Logger類用來打日誌,LoggerFactory類用來獲取Logger;slf4j-log4j是連線slf4j和log4j的橋樑,怎麼連線的呢?我們看看slf4j的LoggerFactory類的getLogger函式的原始碼:

 1 /**
 2    * Return a logger named according to the name parameter using the statically
 3    * bound {@link ILoggerFactory} instance.
 4    * 
 5    * @param name
 6    *          The name of the logger.
 7    * @return logger
 8    */
 9   public static Logger getLogger(String name) {
10     ILoggerFactory iLoggerFactory = getILoggerFactory();
11     return iLoggerFactory.getLogger(name);
12   }
13 
14   /**
15    * Return a logger named corresponding to the class passed as parameter, using
16    * the statically bound {@link ILoggerFactory} instance.
17    * 
18    * @param clazz
19    *          the returned logger will be named after clazz
20    * @return logger
21    */
22   public static Logger getLogger(Class clazz) {
23     return getLogger(clazz.getName());
24   }
25 
26   /**
27    * Return the {@link ILoggerFactory} instance in use.
28    * 
29    * <p>
30    * ILoggerFactory instance is bound with this class at compile time.
31    * 
32    * @return the ILoggerFactory instance in use
33    */
34   public static ILoggerFactory getILoggerFactory() {
35     if (INITIALIZATION_STATE == UNINITIALIZED) {
36       INITIALIZATION_STATE = ONGOING_INITILIZATION;
37       performInitialization();
38 
39     }
40     switch (INITIALIZATION_STATE) {
41     case SUCCESSFUL_INITILIZATION:
42       return StaticLoggerBinder.getSingleton().getLoggerFactory();
43     case NOP_FALLBACK_INITILIZATION:
44       return NOP_FALLBACK_FACTORY;
45     case FAILED_INITILIZATION:
46       throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
47     case ONGOING_INITILIZATION:
48       // support re-entrant behavior.
49       // See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
50       return TEMP_FACTORY;
51     }
52     throw new IllegalStateException("Unreachable code");
53   }

查詢到現在,我們發現LoggerFactory.getLogger()首先獲取一個ILoggerFactory介面,然後使用該介面獲取具體的Logger。獲取ILoggerFactory的時候用到了一個StaticLoggerBinder類,仔細研究我們會發現StaticLoggerBinder這個類並不是slf4j-api這個包中的類,而是slf4j-log4j包中的類,這個類就是一箇中間類,它用來將抽象的slf4j變成具體的log4j,也就是說具體要使用什麼樣的日誌實現方案,就得靠這個StaticLoggerBinder類。

再看看slf4j-log4j包種的這個StaticLoggerBinder類建立ILoggerFactory長什麼樣子:

 1 /**
 2    * The ILoggerFactory instance returned by the {@link #getLoggerFactory}
 3    * method should always be the same object
 4    */
 5   private final ILoggerFactory loggerFactory;
 6 
 7   private StaticLoggerBinder() {
 8     loggerFactory = new Log4jLoggerFactory();
 9     try {
10       Level level = Level.TRACE;
11     } catch (NoSuchFieldError nsfe) {
12       Util
13           .report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
14     }
15   }
16 
17   public ILoggerFactory getLoggerFactory() {
18     return loggerFactory;
19   }
20 
21   public String getLoggerFactoryClassStr() {
22     return loggerFactoryClassStr;
23   }

可以看到slf4j-log4j中的StaticLoggerBinder類建立的ILoggerFactory其實是一個org.slf4j.impl.Log4jLoggerFactory,這個類的getLogger函式程式碼如下:

 1 /*
 2    * (non-Javadoc)
 3    * 
 4    * @see org.slf4j.ILoggerFactory#getLogger(java.lang.String)
 5    */
 6   public Logger getLogger(String name) {
 7     Logger slf4jLogger = null;
 8     // protect against concurrent access of loggerMap
 9     synchronized (this) {
10         slf4jLogger = (Logger) loggerMap.get(name);
11       if (slf4jLogger == null) {
12         org.apache.log4j.Logger log4jLogger;
13         if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) {
14            log4jLogger = LogManager.getRootLogger();
15         } else {
16           log4jLogger = LogManager.getLogger(name);
17         }
18         slf4jLogger = new Log4jLoggerAdapter(log4jLogger);
19         loggerMap.put(name, slf4jLogger);
20       }
21     }
22     return slf4jLogger;
23   }

就在其中建立了真正的org.apache.log4j.Logger,也就是我們需要的具體的日誌實現方案的Logger類。就這樣,整個繫結過程就完成了。

相關文章