log4j源碼解析

Log4J將寫日誌功能抽象成七個核心類/接口:LoggerLoggerRepositoryLevelLoggingEventAppenderLayoutObjectRender。其類圖如下:


更詳細的,實現Log4J主要功能相關的類圖:

其實Log4J最核心的也就5個類:Logger用於對日誌記錄行爲的抽象,提供記錄不同級別日誌的接口;Level對日誌級別的抽象;Appender是對記錄日誌形式的抽象;Layout是對日誌行格式的抽象;而LoggingEvent是對一次日誌記錄過程中所能取到信息的抽象。另外兩個LoggerRepository是Logger實例的容器,而ObjectRender是對日誌實例的解析接口,它們主要提供了一種擴展支持。

簡單的一次記錄日誌過程的序列圖如下:


即獲取Logger實例->判斷Logger實例對應的日誌記錄級別是否要比請求的級別低->若是調用forceLog記錄日誌->創建LoggingEvent實例->將LoggingEvent實例傳遞給Appender->Appender調用Layout實例格式化日誌消息->Appender將格式化後的日誌信息寫入該Appender對應的日誌輸出中。

包含Log4J其他模塊類的更詳細序列圖如下:

在簡單的介紹了Log4J各個模塊類的作用後,以下將詳細的介紹各個模塊的具體作用以及代碼實現。

Logger類

Logger是對記錄日誌動作的抽象,它提供了記錄不同級別日誌的接口,日誌信息可以包含異常信息也可以不包含:

 1 public void debug(Object message) {
 2     if(isLevelEnabled(Level.DEBUG)) {
 3         forceLog(FQCN, Level.DEBUG, message, null);
 4     }
 5 }
 6 public void debug(Object message, Throwable cause) {
 7     if(isLevelEnabled(Level.DEBUG)) {
 8         forceLog(FQCN, Level.DEBUG, message, cause);
 9     }
10 }
11 protected void forceLog(String fqcn, Level level, Object message, Throwable t) {
12     callAppenders(new LoggingEvent(fqcn, this, level, message, t));
13 }

Logger類包含Level信息 ,如果當前Logger未設置Level值,它也可以中父節點中繼承下來,該值可以用來控制該Logger可以記錄的日誌級別:

 1 protected Level level;
 2 public Level getEffectiveLevel() {
 3     for(Logger logger = this; logger != null; logger = logger.parent) {
 4         if(logger.level != null) {
 5             return logger.level;
 6         }
 7     }
 8     return null;
 9 }
10 public boolean isLevelEnabled(Level level) {
11     return level.isGreaterOrEqual(this.getEffectiveLevel());
12 }
13 public boolean isDebugEnabled() {
14     return isLevelEnabled(Level.DEBUG);
15 }

Logger是一個命名的實體,其名字一般用”.”分割以體現不同Logger的層次關係,其中Level和Appender信息可以從父節點中獲取,因而Logger類中還具有name和parent屬性。

1 private String name;
2 protected Logger parent;

在某些情況下,我們希望某些Logger只將日誌記錄到特定的Appender中,而不想記錄在父節點中的Appender中,Log4J爲這種需求提供了additivity屬性,即對當前Logger節點,如果其additivity屬性設置爲false,則該Logger不會繼承父節點的Appender信息,但是其子節點依然會繼承該Logger的Appender信息,除非子節點的additivity屬性也設置成了false。

 1 private boolean additive = true;
 2 public void callAppenders(LoggingEvent event) {
 3     int writes = 0;
 4     
 5     for(Logger logger = this; logger != null; logger = logger.parent) {
 6         synchronized(logger) {
 7             if(logger.appenders != null) {
 8                 writes += logger.appenders.appendLoopOnAppenders(event);
 9             }
10             if(!logger.additive) {
11                 break;
12             }
13         }
14     }
15     
16     if(writes == 0) {
17         System.err.println("No Appender is configed.");
18     }
19 }

最後,爲了支持國際化,Log4J還提供了兩個l7dlog()方法,通過指定的key,以從資源文件中獲取消息內容。爲了使用這兩個方法,需要設置資源文件。同樣,資源文件也是可以從父節點中繼承的。

 1 private ResourceBundle resourceBundle;
 2 public void l7dlog(Level level, String key, Throwable cause) {
 3     if(isLevelEnabled(level)) {
 4         String message = getResourceBundleString(key);
 5         if(message == null) {
 6             message = key;
 7         }
 8         forceLog(FQCN, level, message, cause);
 9     }
10 }
11 
12 public void l7dlog(Level level, String key, Object[] params, Throwable cause) {
13     
14         if(pattern == null) {
15             message = key;
16         } else {
17             message = MessageFormat.format(pattern, params);
18         }
19     
20 }
21 
22 protected String getResourceBundleString(String key) {
23     ResourceBundle rb = getResourceBundle();
24     
25     return rb.getString(key);
26 
27 public ResourceBundle getResourceBundle() {
28     for(Logger logger = this; logger != null; logger = logger.parent) {
29         if(logger.resourceBundle != null) {
30             return logger.resourceBundle;
31         }
32     }
33     return null;    
34 }

另外,在實際開發中經常會遇到要把日誌信息同時寫到不同地方,如同時寫入文件和控制檯,因而一個Logger實例中可以包含多個Appender,爲了管理多個Appender,Log4J抽象出了AppenderAttachable接口,它定義了幾個用於管理多個Appender實例的方法,這些方法由AppenderAttachableImpl類實現,而Logger會實例化AppenderAttachableImpl,並將這些方法代理給該實例:

 1 public interface AppenderAttachable {
 2     public void addAppender(Appender newAppender);
 3     public Enumeration getAllAppenders();
 4     public Appender getAppender(String name);
 5     public boolean isAttached(Appender appender);
 6     void removeAllAppenders();
 7     void removeAppender(Appender appender);
 8     void removeAppender(String name);
 9 }

RootLogger類

Log4J中,所有Logger實例組成一個單根的樹狀結構,由於Logger實例的根節點有一點特殊:它的名字爲“root”,它沒有父節點,它的Level字段必須設值以防止其他Logger實例都沒有設置Level值的情況。基於這些考慮,Log4J通過繼承Logger類實現了RootLogger類,它用於表達所有Logger實例的根節點:
 1 public final class RootLogger extends Logger {
 2     public RootLogger(Level level) {
 3         super("root");
 4         setLevel(level);
 5     }
 6     public final Level getChainedLevel() {
 7         return level;
 8     }
 9     public final void setLevel(Level level) {
10         if (level == null) {
11             LogLog.error("You have tried to set a null level to root.",
12                     new Throwable());
13         } else {
14             this.level = level;
15         }
16     }
17 }

NOPLogger類

有時候,爲了測試等其他需求,我們希望Logger本身不做什麼事情,Log4J爲這種需求提供了NOPLogger類,它繼承自Logger,但是基本上的方法都爲空。

Level類

Level是對日誌級別的抽象,目前Log4J支持的級別有FATAL、ERROR、WARN、INFO、DEBUG、TRACE,從頭到尾一次級別遞減,另外Log4J還支持兩種特殊的級別:ALL和OFF,它們分別表示打開和關閉日誌功能。

 1 public static final int OFF_INT = Integer.MAX_VALUE;
 2 public static final int FATAL_INT = 50000;
 3 public static final int ERROR_INT = 40000;
 4 public static final int WARN_INT  = 30000;
 5 public static final int INFO_INT  = 20000;
 6 public static final int DEBUG_INT = 10000;
 7 public static final int TRACE_INT = 5000;
 8 public static final int ALL_INT = Integer.MIN_VALUE;
 9 
10 public static final Level OFF = new Level(OFF_INT, "OFF"0);
11 public static final Level FATAL = new Level(FATAL_INT, "FATAL"0);
12 public static final Level ERROR = new Level(ERROR_INT, "ERROR"3);
13 public static final Level WARN = new Level(WARN_INT, "WARN"4);
14 public static final Level INFO = new Level(INFO_INT, "INFO"6);
15 public static final Level DEBUG = new Level(DEBUG_INT, "DEBUG"7);
16 public static final Level TRACE = new Level(TRACE_INT, "TRACE"7);
17 public static final Level ALL = new Level(ALL_INT, "ALL"7);

每個Level實例包含了該Level代表的int值(一般是從級別低到級別高一次增大)、該Level的String表達、該Level和系統Level的對應值。

1 protected transient int level;
2 protected transient String levelStr;
3 protected transient int syslogEquivalent;
4 protected Level(int level, String levelStr, int syslogEquivalent) {
5     this.level = level;
6     this.levelStr = levelStr;
7     this.syslogEquivalent = syslogEquivalent;
8 }

Level類主要提供了判斷哪個Level級別更高的方法isGreaterOrEqual()以及將int值或String值轉換成Level實例的toLevel()方法:

 1 public boolean isGreaterOrEqual(Level level) {
 2     return this.level >= level.level;
 3 }
 4 public static Level toLevel(int level) {
 5     return toLevel(level, DEBUG);
 6 }
 7 public static Level toLevel(int level, Level defaultLevel) {
 8     switch(level) {
 9         case OFF_INT: return OFF;
10         case FATAL_INT: return FATAL;
11         case ERROR_INT: return ERROR;
12         case WARN_INT: return WARN;
13         case INFO_INT: return INFO;
14         case DEBUG_INT: return DEBUG;
15         case TRACE_INT: return TRACE;
16         case ALL_INT: return ALL;
17     }
18     return defaultLevel;
19 }

另外,由於對相同級別的Level實例來說,它必須是單例的,因而Log4J對序列化和反序列化做了一些處理。即它的三個成員都是transient,真正序列化和反序列化的代碼自己寫,並且加入readResolve()方法的支持,以保證反序列化出來的相同級別的Level實例是相同的實例。

 1 private void readObject(final ObjectInputStream input) throws IOException, ClassNotFoundException {
 2     input.defaultReadObject();
 3     level = input.readInt();
 4     syslogEquivalent = input.readInt();
 5     levelStr = input.readUTF();
 6     if(levelStr == null) {
 7         levelStr = "";
 8     }
 9 }
10 private void writeObject(final ObjectOutputStream output) throws IOException {
11     output.defaultWriteObject();
12     output.writeInt(level);
13     output.writeInt(syslogEquivalent);
14     output.writeUTF(levelStr);
15 }
16 private Object readResolve() throws ObjectStreamException {
17     if(this.getClass() == Level.class) {
18         return toLevel(level);
19     }
20     return this;
21 }

如果要實現自己的Level類,可以繼承自Level,並且實現相應的靜態toLevel()方法即可。關於如何實現自己的Level類將會在配置文件相關小節中詳細討論。

LoggerRepository類

LoggerRepository從概念以及字面上來說它就是一個Logger實例的容器:一方面相同名字的Logger實例只需要創建一次,在後面的使用中,只需要從這個容器中取即可;另一方面,Logger容器可以存放從配置文件中解析出來的信息,從而使配置信息可以無縫的應用到Log4J內部系統中;最後Logger容器還爲維護Logger的樹狀層次結構提供了方面,每個Logger只維護父節點的信息,有了Logger容器的存在則可以很容易的找到一個新的Logger實例的父節點;關於Logger容器將在下一節中詳細講解。

LoggingEvent類

LoggingEvent個人感覺用LoggingContext更合適一些,它是對一次日誌記錄時哪能獲取到的數據的封裝。它包含了以下信息以提供Layout在format()方法中使用:

1.       fqnOfCategoryClass:日誌記錄接口(默認爲Logger)的類全名,該信息主要用於計算日誌記錄點的源文件、調用方法以及行號等位置信息。

2.       locationInfo:通過fqnOfCategoryClass計算位置信息,位置信息的計算由LocationInfo類實現,這些信息可以提供給Layout使用。

3.       logger:目前來看主要是通過Logger實例取得LogRepository實例,並通過LogRepository取得註冊的ObjectRender實例,如果有的話。

4.       loggerName:當前日誌記錄的Logger名稱,提供給Layout使用。

5.       threadName:當前線程名,提供給Layout使用。

6.       level:當前日誌的級別,提供給Layout使用。

7.       message:當前日誌類,一般是String類型,但是也可以通過註冊ObjectRender,然後傳入相應的其他對象類型。

8.       renderedMessage:經過ObjectRender處理後的日誌信息,提供給Layout使用。

9.       throwableInfo:異常信息,如果存在的話,提供給Layout使用。

10.   timestamp:創建LoggingEvent實例的時間,提供給Layout使用。

11.   其他相對不常用的信息將會在後面小節中講解。

LoggingEvent只是一個簡單的數據對象(DO),因而其實現還是比較簡單的,即在創建實例時將數據提供給它,在其他類(Layout等)使用它時通過getXXX()方法取數據。不過還是有幾個方法可以簡單的講解一下。

LocationInfo類計算位置信息

LocationInfo所指的位置信息主要包括記錄日誌所在的源文件名、類名、方法名、所在源文件的行號。

1     transient String lineNumber;
2     transient String fileName;
3     transient String className;
4     transient String methodName;
5     //fully.qualified.classname.of.caller.methodName(Filename.java:line)
6     public String fullInfo;

我們知道在異常棧中每一條記錄都包含了方法調用對應的這些信息,Log4J的這些信息正是利用了這個原理,即通過構建一個Throwable實例,而後在該Throwable的棧信息中解析出來的:

1 public LocationInfo getLocationInformation() {
2     if (locationInfo == null) {
3         locationInfo = new LocationInfo(new Throwable(), 
4 fqnOfCategoryClass);
5     }
6     return locationInfo;
7 }

以上Throwable一般會產生如下異常棧:

1 java.lang.Throwable
2 
3 at org.apache.log4j.PatternLayout.format(PatternLayout.java:413)
4 at org.apache.log4j.FileAppender.doAppend(FileAppender.java:183)
5 at org.apache.log4j.Category.callAppenders(Category.java:131)
6 at org.apache.log4j.Category.log(Category.java:512)
7 at callers.fully.qualified.className.methodName(FileName.java:74)
8 

因而我們就可以通過callers.fully.qualified.className信息來找到改行信息,這個className信息即是傳入的fqnOfCategoryClass。

如果當前JDK版本是1.4以上,我們就可以通過JDK提供的一些方法來查找:

 1 getStackTraceMethod = Throwable.class.getMethod("getStackTrace",
 2         noArgs);
 3 Class stackTraceElementClass = Class
 4         .forName("java.lang.StackTraceElement");
 5 getClassNameMethod = stackTraceElementClass.getMethod(
 6         "getClassName", noArgs);
 7 getMethodNameMethod = stackTraceElementClass.getMethod(
 8         "getMethodName", noArgs);
 9 getFileNameMethod = stackTraceElementClass.getMethod("getFileName",
10         noArgs);
11 getLineNumberMethod = stackTraceElementClass.getMethod(
12         "getLineNumber", noArgs);
13 
14 Object[] noArgs = null;
15 Object[] elements = (Object[]) getStackTraceMethod.invoke(t,
16         noArgs);
17 String prevClass = NA;
18 for (int i = elements.length - 1; i >= 0; i--) {
19     String thisClass = (String) getClassNameMethod.invoke(
20             elements[i], noArgs);
21     if (fqnOfCallingClass.equals(thisClass)) {
22         int caller = i + 1;
23         if (caller < elements.length) {
24             className = prevClass;
25             methodName = (String) getMethodNameMethod.invoke(
26                     elements[caller], noArgs);
27             fileName = (String) getFileNameMethod.invoke(
28                     elements[caller], noArgs);
29             if (fileName == null) {
30                 fileName = NA;
31             }
32             int line = ((Integer) getLineNumberMethod.invoke(
33                     elements[caller], noArgs)).intValue();
34             if (line < 0) {
35                 lineNumber = NA;
36             } else {
37                 lineNumber = String.valueOf(line);
38             }
39             StringBuffer buf = new StringBuffer();
40             buf.append(className);
41             buf.append(".");
42             buf.append(methodName);
43             buf.append("(");
44             buf.append(fileName);
45             buf.append(":");
46             buf.append(lineNumber);
47             buf.append(")");
48             this.fullInfo = buf.toString();
49         }
50         return;
51     }
52     prevClass = thisClass;
53 }

否則,則需要我們通過字符串查找的方式來查找:

 1 String s;
 2 // Protect against multiple access to sw.
 3 synchronized (sw) {
 4     t.printStackTrace(pw);
 5     s = sw.toString();
 6     sw.getBuffer().setLength(0);
 7 }
 8 int ibegin, iend;
 9 ibegin = s.lastIndexOf(fqnOfCallingClass);
10 if (ibegin == -1)
11     return;
12 // See bug 44888.
13 if (ibegin + fqnOfCallingClass.length() < s.length()
14         && s.charAt(ibegin + fqnOfCallingClass.length()) != '.') {
15     int i = s.lastIndexOf(fqnOfCallingClass + ".");
16     if (i != -1) {
17         ibegin = i;
18     }
19 }
20 
21 ibegin = s.indexOf(Layout.LINE_SEP, ibegin);
22 if (ibegin == -1)
23     return;
24 ibegin += Layout.LINE_SEP_LEN;
25 
26 // determine end of line
27 iend = s.indexOf(Layout.LINE_SEP, ibegin);
28 if (iend == -1)
29     return;
30 
31 // VA has a different stack trace format which doesn't
32 // need to skip the inital 'at'
33 if (!inVisualAge) {
34     // back up to first blank character
35     ibegin = s.lastIndexOf("at ", iend);
36     if (ibegin == -1)
37         return;
38     // Add 3 to skip "at ";
39     ibegin += 3;
40 }
41 // everything between is the requested stack item
42 this.fullInfo = s.substring(ibegin, iend);

對於通過字符串查找到的fullInfo值,在獲取其他單個值時還需要做相應的字符串解析:
className:

 1 // Starting the search from '(' is safer because there is
 2 // potentially a dot between the parentheses.
 3 int iend = fullInfo.lastIndexOf('(');
 4 if (iend == -1)
 5     className = NA;
 6 else {
 7     iend = fullInfo.lastIndexOf('.', iend);
 8 
 9     // This is because a stack trace in VisualAge looks like:
10 
11     // java.lang.RuntimeException
12     // java.lang.Throwable()
13     // java.lang.Exception()
14     // java.lang.RuntimeException()
15     // void test.test.B.print()
16     // void test.test.A.printIndirect()
17     // void test.test.Run.main(java.lang.String [])
18     int ibegin = 0;
19     if (inVisualAge) {
20         ibegin = fullInfo.lastIndexOf(' ', iend) + 1;
21     }
22 
23     if (iend == -1)
24         className = NA;
25     else
26         className = this.fullInfo.substring(ibegin, iend);

 

fileName:
1 
2 int iend = fullInfo.lastIndexOf(':');
3 if (iend == -1)
4     fileName = NA;
5 else {
6     int ibegin = fullInfo.lastIndexOf('(', iend - 1);
7     fileName = this.fullInfo.substring(ibegin + 1, iend);
8 }
lineNumber:
1 int iend = fullInfo.lastIndexOf(')');
2 int ibegin = fullInfo.lastIndexOf(':', iend - 1);
3 if (ibegin == -1)
4     lineNumber = NA;
5 else
6     lineNumber = this.fullInfo.substring(ibegin + 1, iend);
methodName:
1 int iend = fullInfo.lastIndexOf('(');
2 int ibegin = fullInfo.lastIndexOf('.', iend);
3 if (ibegin == -1)
4     methodName = NA;
5 else
6     methodName = this.fullInfo.substring(ibegin + 1, iend);

ObjectRender接口

Log4J中,對傳入的message實例,如果是非String類型,會先使用註冊的ObjectRender(在LogRepository中查找註冊的ObjectRender信息)處理成String後返回,若沒有找到相應的ObjectRender,則使用默認的ObjectRender,它只是調用該消息實例的toString()方法。

 1 public Object getMessage() {
 2     if (message != null) {
 3         return message;
 4     } else {
 5         return getRenderedMessage();
 6     }
 7 }
 8 public String getRenderedMessage() {
 9     if (renderedMessage == null && message != null) {
10         if (message instanceof String)
11             renderedMessage = (String) message;
12         else {
13             LoggerRepository repository = logger.getLoggerRepository();
14 
15             if (repository instanceof RendererSupport) {
16                 RendererSupport rs = (RendererSupport) repository;
17                 renderedMessage = rs.getRendererMap()
18                         .findAndRender(message);
19             } else {
20                 renderedMessage = message.toString();
21             }
22         }
23     }
24     return renderedMessage;
25 }

ThrowableInformation類

ThrowableInformation類用以處理異常棧信息,即通過Throwable實例獲取異常棧字符串數組。同時還支持自定義的ThrowableRender(在LogRepository中設置),默認的ThrowableRender通過系統printStackTrace()方法來獲取信息:

 1 if (throwable != null) {
 2     this.throwableInfo = new ThrowableInformation(throwable, logger);
 3 }
 4 ThrowableRenderer renderer = null;
 5 if (category != null) {
 6     LoggerRepository repo = category.getLoggerRepository();
 7     if (repo instanceof ThrowableRendererSupport) {
 8         renderer = ((ThrowableRendererSupport) repo)
 9                 .getThrowableRenderer();
10     }
11 }
12 if (renderer == null) {
13     rep = DefaultThrowableRenderer.render(throwable);
14 else {
15     rep = renderer.doRender(throwable);
16 }
17 public static String[] render(final Throwable throwable) {
18     StringWriter sw = new StringWriter();
19     PrintWriter pw = new PrintWriter(sw);
20     try {
21         throwable.printStackTrace(pw);
22     } catch (RuntimeException ex) {
23     }
24     pw.flush();
25     LineNumberReader reader = new LineNumberReader(new StringReader(
26             sw.toString()));
27     ArrayList lines = new ArrayList();
28     try {
29         String line = reader.readLine();
30         while (line != null) {
31             lines.add(line);
32             line = reader.readLine();
33         }
34     } catch (IOException ex) {
35         if (ex instanceof InterruptedIOException) {
36             Thread.currentThread().interrupt();
37         }
38         lines.add(ex.toString());
39     }
40     String[] tempRep = new String[lines.size()];
41     lines.toArray(tempRep);
42     return tempRep;
43 }

Layout類

Layout負責將LoggingEvent中的信息格式化成一行日誌信息。對不同格式的日誌可能還需要提供頭和尾等信息。另外有些Layout不會處理異常信息,此時ignoresThrowable()方法返回false,並且異常信息需要Appender來處理,如PatternLayout。

 1 public abstract class Layout implements OptionHandler {
 2     public final static String LINE_SEP = System.getProperty("line.separator");
 3     public final static int LINE_SEP_LEN = LINE_SEP.length();
 4     abstract public String format(LoggingEvent event);
 5     public String getContentType() {
 6         return "text/plain";
 7     }
 8     public String getHeader() {
 9         return null;
10     }
11     public String getFooter() {
12         return null;
13     }
14     abstract public boolean ignoresThrowable();
15 }

Layout的實現比較簡單,如SimpleLayout對一行日誌信息只是打印日誌級別信息以及日誌信息。

 1 public class SimpleLayout extends Layout {
 2     StringBuffer sbuf = new StringBuffer(128);
 3     public SimpleLayout() {
 4     }
 5     public void activateOptions() {
 6     }
 7     public String format(LoggingEvent event) {
 8         sbuf.setLength(0);
 9         sbuf.append(event.getLevel().toString());
10         sbuf.append(" - ");
11         sbuf.append(event.getRenderedMessage());
12         sbuf.append(LINE_SEP);
13         return sbuf.toString();
14     }
15     public boolean ignoresThrowable() {
16         return true;
17     }
18 }

關於Layout更詳細的信息將會在以後小節中介紹。

Appender接口

Appender負責定義日誌輸出的目的地,它可以是控制檯(ConsoleAppender)、文件(FileAppender)、JMS服務器(JmsLogAppender)、以Email的形式發送出去(SMTPAppender)等。Appender是一個命名的實體,另外它還包含了對Layout、ErrorHandler、Filter等引用:

 1 public interface Appender {
 2     void addFilter(Filter newFilter);
 3     public Filter getFilter();
 4     public void clearFilters();
 5     public void close();
 6     public void doAppend(LoggingEvent event);
 7     public String getName();
 8     public void setErrorHandler(ErrorHandler errorHandler);
 9     public ErrorHandler getErrorHandler();
10     public void setLayout(Layout layout);
11     public Layout getLayout();
12     public void setName(String name);
13     public boolean requiresLayout();
14 }

簡單的,在配置文件中,Appender會註冊到Logger中,Logger在寫日誌時,通過繼承機制遍歷所有註冊到它本身和其父節點的Appender(在additivity爲true的情況下),調用doAppend()方法,實現日誌的寫入。在doAppend方法中,若當前Appender註冊了Filter,則doAppend還會判斷當前日誌時候通過了Filter的過濾,通過了Filter的過濾後,如果當前Appender繼承自SkeletonAppender,還會檢查當前日誌級別時候要比當前Appender本身的日誌級別閥門要打,所有這些都通過後,纔會將LoggingEvent實例傳遞給Layout實例以格式化成一行日誌信息,最後寫入相應的目的地,在這些操作中,任何出現的錯誤都由ErrorHandler字段來處理。

SkeletonAppender類

目前Log4J實現的Appender都繼承自SkeletonAppender類,該類對Appender接口提供了最基本的實現,並且引入了Threshold的概念,即所有的比當前Appender定義的日誌級別閥指要大的日誌纔會記錄下來。

 1 public abstract class AppenderSkeleton implements Appender, OptionHandler {
 2     protected Layout layout;
 3     protected String name;
 4     protected Priority threshold;
 5     protected ErrorHandler errorHandler = new OnlyOnceErrorHandler();
 6     protected Filter headFilter;
 7     protected Filter tailFilter;
 8     protected boolean closed = false;
 9     public AppenderSkeleton() {
10         super();
11     }
12     public void activateOptions() {
13     }
14     abstract protected void append(LoggingEvent event);
15     public boolean isAsSevereAsThreshold(Priority priority) {
16         return ((threshold == null|| priority.isGreaterOrEqual(threshold));
17     }
18     public synchronized void doAppend(LoggingEvent event) {
19         if (closed) {
20             LogLog.error("Attempted to append to closed appender named ["
21                     + name + "].");
22             return;
23         }
24         if (!isAsSevereAsThreshold(event.getLevel())) {
25             return;
26         }
27         Filter f = this.headFilter;
28         FILTER_LOOP: while (f != null) {
29             switch (f.decide(event)) {
30             case Filter.DENY:
31                 return;
32             case Filter.ACCEPT:
33                 break FILTER_LOOP;
34             case Filter.NEUTRAL:
35                 f = f.getNext();
36             }
37         }
38         this.append(event);
39     }
40 public void finalize() {
41         if (this.closed)
42             return;
43         LogLog.debug("Finalizing appender named [" + name + "].");
44         close();
45     }
46 }

SkeletonAppender實現了doAppend()方法,它首先檢查日誌級別是否要比threshold要大;然後如果註冊了Filter,則使用Filter對LoggingEvent實例進行過濾,如果Filter返回Filter.DENY則doAppend()退出,否則執行append()方法,該方法由子類實現。

在Log4J中,Filter組成一條鏈,它定了以decide()方法,由子類實現,若返回DENY則日誌不會被記錄、NEUTRAL則繼續檢查下一個Filter實例、ACCEPT則Filter通過,繼續執行後面的寫日誌操作。使用Filter可以爲Appender加入一些出了threshold以外的其他邏輯,由於它本身是鏈狀的,而且它的執行是橫跨在Appender的doAppend方法中,因而這也是一個典型的AOP的概念。Filter的實現將會在下一小節中講解。

SkeletonAppender還重寫了finalize()方法,這是因爲Log4J本身作爲一個組件,它可能還是通過其他組件如commons-logging或slf4j組件間接的引入,因而使用它的程序不應該對它存在依賴的,然而在程序退出之前所有的Appender需要調用close()方法以釋放它所佔據的資源,爲了不在使用Log4J的程序手動的close()的方法,以減少Log4J代碼的侵入性,因而Log4J將close()的方法調用加入到finalize()方法中,即在垃圾回收器回收Appender實例時就會調用它的close()方法。

WriterAppender類和ConsoleAppender類

WriterAppender將日誌寫入Java IO中,它繼承自SkeletonAppender類。它引入了三個字段:immediateFlush,指定沒寫完一條日誌後,即將日誌內容刷新到設備中,雖然這麼做會有一點性能上的損失,但是如果不怎麼做,則會出現在程序異常終止的時候無法看到部分日誌信息,而經常這些丟失的日誌信息要用於分析爲什麼會出現異常終止的情況,因而一般推薦將該值設置爲true,即默認值;econding用於定義日誌文本的編碼方式;qw定義寫日誌的writer,它可以是文件或是控制檯等Java IO支持的流。

在寫日誌文本前,WriterAppender還會做其他檢查,如該Appender不能已經closed、qw和layout必須有值等,而後纔可以將layout格式化後的日誌行寫入設備中。若layout本身不處理異常問題,則有Appender處理異常問題。最後如果每行日誌需要刷新,則調用刷新操作。

 1 public class WriterAppender extends AppenderSkeleton {
 2     protected boolean immediateFlush = true;
 3     protected String encoding;
 4     protected QuietWriter qw;
 5     public WriterAppender() {
 6     }
 7     public WriterAppender(Layout layout, OutputStream os) {
 8         this(layout, new OutputStreamWriter(os));
 9     }
10     public WriterAppender(Layout layout, Writer writer) {
11         this.layout = layout;
12         this.setWriter(writer);
13     }
14     public void append(LoggingEvent event) {
15         if (!checkEntryConditions()) {
16             return;
17         }
18         subAppend(event);
19     }
20     protected boolean checkEntryConditions() {
21         if (this.closed) {
22             LogLog.warn("Not allowed to write to a closed appender.");
23             return false;
24         }
25         if (this.qw == null) {
26             errorHandler
27                     .error("No output stream or file set for the appender named ["
28                             + name + "].");
29             return false;
30         }
31         if (this.layout == null) {
32             errorHandler.error("No layout set for the appender named [" + name
33                     + "].");
34             return false;
35         }
36         return true;
37     }
38     protected void subAppend(LoggingEvent event) {
39         this.qw.write(this.layout.format(event));
40         if (layout.ignoresThrowable()) {
41             String[] s = event.getThrowableStrRep();
42             if (s != null) {
43                 int len = s.length;
44                 for (int i = 0; i < len; i++) {
45                     this.qw.write(s[i]);
46                     this.qw.write(Layout.LINE_SEP);
47                 }
48             }
49         }
50         if (shouldFlush(event)) {
51             this.qw.flush();
52         }
53     }
54     public boolean requiresLayout() {
55         return true;
56     }
57 }

ConsoleAppender繼承自WriterAppender,它只是簡單的將System.out或System.err實例傳遞給WriterAppender以構建相應的writer,最後實現將日誌寫入到控制檯中。

Filter類

在Log4J中,Filter組成一條鏈,它定了以decide()方法,由子類實現,若返回DENY則日誌不會被記錄、NEUTRAL則繼續檢查下一個Filter實例、ACCEPT則Filter通過,繼續執行後面的寫日誌操作。使用Filter可以爲Appender加入一些出了threshold以外的其他邏輯,由於它本身是鏈狀的,而且它的執行是橫跨在Appender的doAppend方法中,因而這也是一個典型的AOP的概念。

 1 public abstract class Filter implements OptionHandler {
 2     public Filter next;
 3     public static final int DENY = -1;
 4     public static final int NEUTRAL = 0;
 5     public static final int ACCEPT = 1;
 6     public void activateOptions() {
 7     }
 8     abstract public int decide(LoggingEvent event);
 9     public void setNext(Filter next) {
10         this.next = next;
11     }
12     public Filter getNext() {
13         return next;
14     }
15 }

Log4J本身提供了四個Filter:DenyAllFilter、LevelMatchFilter、LevelRangeFilter、StringMatchFilter。

DenyAllFilter只是簡單的在decide()中返回DENY值,可以將其應用在Filter鏈尾,實現如果之前的Filter都沒有通過,則該LoggingEvent沒有通過,類似或的操作:

1 public class DenyAllFilter extends Filter {
2     public int decide(LoggingEvent event) {
3         return Filter.DENY;
4     }
5 }

StringMatchFilter通過日誌消息中的字符串來判斷Filter後的狀態:

 1 public class StringMatchFilter extends Filter {
 2     boolean acceptOnMatch = true;
 3     String stringToMatch;
 4     public int decide(LoggingEvent event) {
 5         String msg = event.getRenderedMessage();
 6         if (msg == null || stringToMatch == null)
 7             return Filter.NEUTRAL;
 8         if (msg.indexOf(stringToMatch) == -1) {
 9             return Filter.NEUTRAL;
10         } else { // we've got a match
11             if (acceptOnMatch) {
12                 return Filter.ACCEPT;
13             } else {
14                 return Filter.DENY;
15             }
16         }
17     }
18 }

LevelMatchFilter判斷日誌級別是否和設置的級別匹配以決定Filter後的狀態:

 1 public class LevelMatchFilter extends Filter {
 2     boolean acceptOnMatch = true;    
 3 Level levelToMatch;
 4     public int decide(LoggingEvent event) {
 5         if (this.levelToMatch == null) {
 6             return Filter.NEUTRAL;
 7         }
 8         boolean matchOccured = false;
 9         if (this.levelToMatch.equals(event.getLevel())) {
10             matchOccured = true;
11         }
12         if (matchOccured) {
13             if (this.acceptOnMatch)
14                 return Filter.ACCEPT;
15             else
16                 return Filter.DENY;
17         } else {
18             return Filter.NEUTRAL;
19         }
20     }
21 }

LevelRangeFilter判斷日誌級別是否在設置的級別範圍內以決定Filter後的狀態:

 1 public class LevelRangeFilter extends Filter {
 2     boolean acceptOnMatch = false;
 3     Level levelMin;
 4     Level levelMax;
 5     public int decide(LoggingEvent event) {
 6         if (this.levelMin != null) {
 7             if (event.getLevel().isGreaterOrEqual(levelMin) == false) {
 8                 return Filter.DENY;
 9             }
10         }
11         if (this.levelMax != null) {
12             if (event.getLevel().toInt() > levelMax.toInt()) {
13                 return Filter.DENY;
14             }
15         }
16         if (acceptOnMatch) {
17             return Filter.ACCEPT;
18         } else {
19             return Filter.NEUTRAL;
20         }
21     }
22 }

總結

這一系列終於是結束了。本文主要介紹了Log4J核心類的實現和他們之間的交互關係。涉及到各個模塊本身的其他詳細信息將會在接下來的小節中詳細介紹,如LogRepository與配置信息、Appender類結構的詳細信息、Layout類結構的詳細信息以及部分LoggingEvent提供的高級功能。而像Level、Logger本身,由於內容不多,已經在這一小節中全部介紹完了。


轉自:http://www.blogjava.net/DLevin/archive/2012/06/28/381667.html

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