1. 程式人生 > >slf4j對於log4j的日誌封裝-正確獲取呼叫堆疊

slf4j對於log4j的日誌封裝-正確獲取呼叫堆疊

日誌是專案中必用的東西,日誌產品裡最普及應該就是log4j了。(logback這裡暫不討論。) 先看一下常用的log4j的用法,一般來說log4j都會配合slf4j或者common-logging使用,這裡已slf4j為例。新增gradle依賴:

dependencies {
compile('log4j:log4j:1.2.17',
'org.slf4j:slf4j-api:1.7.5',
'org.slf4j:slf4j-log4j12:1.7.5')
}

最直接的用法就是在每個需要記錄日誌的類裡,構造一個屬於自己類的log例項,實際上很多著名的開源專案也是這麼做的。如spring。

private static final Log logger = LogFactory.getLog(BeanUtils.class);

那麼我們也先從這種用法開始,先配置好最基本的log4j.xml配置檔案。

<?xml version= "1.0" encoding ="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "http://log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >

     <appender
name="Console" class="org.apache.log4j.ConsoleAppender" > <layout class= "org.apache.log4j.PatternLayout" > <param name= "ConversionPattern" value ="%d >> %-5p >> %t >> %l >> %m%n" /> </layout> </appender > <root
> <level value= "info" /> <appender-ref ref= "Console" /> </root > </log4j:configuration>

引數說明:

%p 輸出優先順序,即DEBUG,INFO,WARN,ERROR,FATAL
%r 輸出自應用啟動到輸出該log資訊耗費的毫秒數
%c 輸出所屬的類目,通常就是所在類的全名
%t 輸出產生該日誌事件的執行緒名
%n 輸出一個回車換行符,Windows平臺為“\r\n”,Unix平臺為“\n”
%d 輸出日誌時間點的日期或時間,預設格式為ISO8601,也可以在其後指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},輸出類似:2002年10月18日 22:10:28,921
%l 輸出日誌事件的發生位置,包括類目名、發生的執行緒,以及在程式碼中的行數。

日誌測試類:

package com.coderli.log4jpackage;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* log4j封裝實驗室中的使用者模擬類
*
* @author OneCoder
* @date 2013年11月25日 下午4:39:19
*/
public class UserClass {

     private static final Logger logger = LoggerFactory
              . getLogger(UserClass.class);
     public static void main(String[] args) {
           logger.info("這是一個Info級別的log4j日誌。" );
     }
}

輸出日誌:

2013-11-26 11:09:21,305 >> INFO  >> main >> com.coderli.log4jpackage.UserClass.main(UserClass.java:18) >> 這是一個Info級別的log4j日誌。

這裡包含的日誌發生時的類、執行緒、行號等資訊。很完整。本身這麼做沒什麼問題,只是可能有的專案考慮如果每個類裡都寫這樣一個開頭,有點麻煩,同時,如果每個類一個獨立的宣告,log4j記憶體會快取很多的例項,佔用記憶體,可能有時候也不便於統一配置管理。所以,有些專案裡考慮了對log進行封裝,提供統一的一個靜態方法呼叫:

package com.coderli.log4jpackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* @author lihzh(OneCoder)
* @date 2013年11月25日 下午4:39:54
* @blog http://www.coderli.com
*/
public class MyLog4j {
     private static final Logger logger = LoggerFactory.getLogger("MyLog4j");

     public static void info(String msg) {
           logger.info(msg);
     }
}

記錄日誌程式碼變為:

/**
 * log4j封裝實驗室中的使用者模擬類
 *
 * @author OneCoder
 * @date 2013年11月25日 下午4:39:19
 */
public class UserClass {

     public static void main(String[] args) {
          MyLog4j. info("這是一個Info級別的log4j日誌。" );
     }
}

日誌輸出:

2013-11-26 11:23:29,936 >> INFO  >> main >> com.coderli.log4jpackage.MyLog4j.info(MyLog4j.java:16 ) >> 這是一個Info級別的log4j日誌。

咋一看沒什麼問題,仔細分析就發現,對我們除錯很有幫助的日誌發生時的類名、行號都程式設計了封裝類裡面的類和行,這對於依靠日誌進行錯誤除錯來說是悲劇的。這種封裝雖然解決了例項過多的問題,但是也是失敗的,甚至是災難性的。

也有人把發生日誌的Class資訊也傳遞進來一起列印。如:

public static void info(String className, String msg) {
           logger.info(className + ">> " + msg);
     }

這種方式顯然是治標不治本的。於是很多人想到了另外的封裝方式,即提供一個統一獲取logger例項的位置,然後在自己的類裡進行嗲用:

public static Logger getLogger() {
           return logger;
     }

呼叫程式碼:

public static void main(String[] args) {
          MyLog4j. getLogger().info("這是一個Info級別的log4j日誌。" );
     }

這種方法,日誌雖然恢復了正常,但是對於開發者實際又增加了麻煩,列印一個日誌需要兩部操作了。當然你可以使用import static。或者每個類裡還是全域性宣告一個logger例項。

那麼有沒有想過,slf4j內部是怎麼對log4j封裝的呢?我們通過slf4j呼叫為什麼就可以之間獲得你實際列印日誌的行號,同時又不會把自己類給暴露出來呢?你可能還沒明白我在說什麼,細說一下,如果我們直接使用log4j的logger,打印出來的是我們呼叫類的行號這沒什麼問題,但是這裡我們獲得的是slf4j的logger例項,它底層呼叫的是log4j的logger例項,那麼為什麼不會打印出slf4j內部呼叫類的行號呢?這就是我關心的問題。知道了這個,也許我們就可以封裝出更好用的全域性log元件了。

其實,這個問題的關鍵就集中在log4j是如何獲取你呼叫日誌的程式碼的類和行號的,在Java中可以通過Throwable來獲取呼叫堆疊, 例如我們將如下程式碼,放在MyLog4j類的info方法中:

   public static void info(String msg) {
      Throwable throwable = new Throwable();
      StackTraceElement[] ste = throwable.getStackTrace();
      for (StackTraceElement stackTraceElement : ste) {
         System.out
               .println("ClassName: " + stackTraceElement.getClassName());
         System.out.println("Method Name: "
               + stackTraceElement.getMethodName());
         System.out.println("Line number: "
               + stackTraceElement.getLineNumber());
      }
      logger.info(msg);
   }

再次通過UserClass呼叫,就可獲得如下輸出:

ClassName: com.coderli.log4jpackage.MyLog4j
Method Name: info
Line number: 28
ClassName: com.coderli.log4jpackage.UserClass
Method Name: main
Line number: 12

由此可見只要在呼叫堆疊裡找到使用者的類,就可以獲得所有我們需要的資訊。有了這個基礎,我們再來看看slf4j和log4j是怎麼做的。

在log4j的Logger中,實際對外提供了用於封裝的統一的log方法。

 /**

     This is the most generic printing method. It is intended to be
     invoked by <b>wrapper</b> classes.

     @param callerFQCN The wrapper class' fully qualified class name.
     @param level The level of the logging request.
     @param message The message of the logging request.
     @param t The throwable of the logging request, may be null.  */
  public
  void log(String callerFQCN, Priority level, Object message, Throwable t

而第一個引數callerFQCN,就是關鍵的決定呼叫者位置的引數。在LocationInfo中,可看到對該引數的使用方式為:

public LocationInfo(Throwable t, String fqnOfCallingClass) {
      if(t == null || fqnOfCallingClass == null)
return;
      if (getLineNumberMethod != null) {
          try {
              Object[] noArgs = null;
              Object[] elements =  (Object[]) getStackTraceMethod.invoke(t, noArgs);
              String prevClass = NA;
              for(int i = elements.length - 1; i >= 0; i--) {
                  String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs);
                  if(fqnOfCallingClass.equals(thisClass)) {
                      int caller = i + 1;
                      if (caller < elements.length) {
                          className = prevClass;
                          methodName = (String) getMethodNameMethod.invoke(elements[caller], noArgs);
                          fileName = (String) getFileNameMethod.invoke(elements[caller], noArgs);
                          if (fileName == null) {
                              fileName = NA;
                          }
                          int line = ((Integer) getLineNumberMethod.invoke(elements[caller], noArgs)).intValue();
                          if (line < 0) {
                              lineNumber = NA;
                          } else {
                              lineNumber = String.valueOf(line);
                          }
                          StringBuffer buf = new StringBuffer();
                          buf.append(className);
                          buf.append(".");
                          buf.append(methodName);
                          buf.append("(");
                          buf.append(fileName);
                          buf.append(":");
                          buf.append(lineNumber);
                          buf.append(")");
                          this.fullInfo = buf.toString();
                      }
                      return;
                  }
                  prevClass = thisClass;
              }
              return;
…….//省略若干

可見,log4j把傳遞進來的callerFQCN在堆疊中一一比較,相等後,再往上一層即認為是使用者的呼叫類。所以,在slf4j封裝的logger中是這樣封裝的:

final static String FQCN = Log4jLoggerAdapter.class .getName();
public void info(String msg) {
    logger.log(FQCN, Level. INFO, msg, null );
  }

使用者的程式碼呼叫的正是呼叫的這個info,所以就會正常的顯示使用者程式碼的行號了。那麼對於封裝來說,你只需要注意呼叫log4j的log方法,並傳遞進去正確的FQCN即可。

final static String FQCN = Log4jLoggerAdapter.class .getName();
public void info(String msg) {
    logger.log(FQCN, Level. INFO, msg, null );
  }