即獲取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,以從資源文件中獲取消息內容。為了使用這兩個方法,需要設置資源文件。同樣,資源文件也是可以從父節點中繼承的。
另外,在實際開發中經常會遇到要把日志信息同時寫到不同地方,如同時寫入文件和控制臺,因而一個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 }
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);