畢業(yè)又趕上本科的同學(xué)會,還去騎車環(huán)了趟崇明島,六月貌似就沒消停過,不過終于這些事情基本上都結(jié)束了,我也可以好好的看些書、讀些源碼、寫點(diǎn)博客了。
Log4J將寫日志功能抽象成七個核心類/接口:Logger、LoggerRepository、Level、LoggingEvent、Appender、Layout、ObjectRender。其類圖如下:

更詳細(xì)的,實(shí)現(xiàn)Log4J主要功能相關(guān)的類圖:

其實(shí)Log4J最核心的也就5個類:Logger用于對日志記錄行為的抽象,提供記錄不同級別日志的接口;Level對日志級別的抽象;Appender是對記錄日志形式的抽象;Layout是對日志行格式的抽象;而LoggingEvent是對一次日志記錄過程中所能取到信息的抽象。另外兩個LoggerRepository是Logger實(shí)例的容器,而ObjectRender是對日志實(shí)例的解析接口,它們主要提供了一種擴(kuò)展支持。
簡單的一次記錄日志過程的序列圖如下:

即獲取Logger實(shí)例->判斷Logger實(shí)例對應(yīng)的日志記錄級別是否要比請求的級別低->若是調(diào)用forceLog記錄日志->創(chuàng)建LoggingEvent實(shí)例->將LoggingEvent實(shí)例傳遞給Appender->Appender調(diào)用Layout實(shí)例格式化日志消息->Appender將格式化后的日志信息寫入該Appender對應(yīng)的日志輸出中。
包含Log4J其他模塊類的更詳細(xì)序列圖如下:

在簡單的介紹了Log4J各個模塊類的作用后,以下將詳細(xì)的介紹各個模塊的具體作用以及代碼實(shí)現(xiàn)。
Logger類
Logger是對記錄日志動作的抽象,它提供了記錄不同級別日志的接口,日志信息可以包含異常信息也可以不包含:
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信息 ,如果當(dāng)前Logger未設(shè)置Level值,它也可以中父節(jié)點(diǎn)中繼承下來,該值可以用來控制該Logger可以記錄的日志級別:
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是一個命名的實(shí)體,其名字一般用”.”分割以體現(xiàn)不同Logger的層次關(guān)系,其中Level和Appender信息可以從父節(jié)點(diǎn)中獲取,因而Logger類中還具有name和parent屬性。
2 protected Logger parent;
在某些情況下,我們希望某些Logger只將日志記錄到特定的Appender中,而不想記錄在父節(jié)點(diǎn)中的Appender中,Log4J為這種需求提供了additivity屬性,即對當(dāng)前Logger節(jié)點(diǎn),如果其additivity屬性設(shè)置為false,則該Logger不會繼承父節(jié)點(diǎn)的Appender信息,但是其子節(jié)點(diǎn)依然會繼承該Logger的Appender信息,除非子節(jié)點(diǎn)的additivity屬性也設(shè)置成了false。
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,以從資源文件中獲取消息內(nèi)容。為了使用這兩個方法,需要設(shè)置資源文件。同樣,資源文件也是可以從父節(jié)點(diǎn)中繼承的。
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 }
另外,在實(shí)際開發(fā)中經(jīng)常會遇到要把日志信息同時(shí)寫到不同地方,如同時(shí)寫入文件和控制臺,因而一個Logger實(shí)例中可以包含多個Appender,為了管理多個Appender,Log4J抽象出了AppenderAttachable接口,它定義了幾個用于管理多個Appender實(shí)例的方法,這些方法由AppenderAttachableImpl類實(shí)現(xiàn),而Logger會實(shí)例化AppenderAttachableImpl,并將這些方法代理給該實(shí)例:
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實(shí)例組成一個單根的樹狀結(jié)構(gòu),由于Logger實(shí)例的根節(jié)點(diǎn)有一點(diǎn)特殊:它的名字為“root”,它沒有父節(jié)點(diǎn),它的Level字段必須設(shè)值以防止其他Logger實(shí)例都沒有設(shè)置Level值的情況。基于這些考慮,Log4J通過繼承Logger類實(shí)現(xiàn)了RootLogger類,它用于表達(dá)所有Logger實(shí)例的根節(jié)點(diǎn):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類
有時(shí)候,為了測試等其他需求,我們希望Logger本身不做什么事情,Log4J為這種需求提供了NOPLogger類,它繼承自Logger,但是基本上的方法都為空。
Level類
Level是對日志級別的抽象,目前Log4J支持的級別有FATAL、ERROR、WARN、INFO、DEBUG、TRACE,從頭到尾一次級別遞減,另外Log4J還支持兩種特殊的級別:ALL和OFF,它們分別表示打開和關(guān)閉日志功能。
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實(shí)例包含了該Level代表的int值(一般是從級別低到級別高一次增大)、該Level的String表達(dá)、該Level和系統(tǒng)Level的對應(yīng)值。
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值轉(zhuǎn)換成Level實(shí)例的toLevel()方法:
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實(shí)例來說,它必須是單例的,因而Log4J對序列化和反序列化做了一些處理。即它的三個成員都是transient,真正序列化和反序列化的代碼自己寫,并且加入readResolve()方法的支持,以保證反序列化出來的相同級別的Level實(shí)例是相同的實(shí)例。
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 }
如果要實(shí)現(xiàn)自己的Level類,可以繼承自Level,并且實(shí)現(xiàn)相應(yīng)的靜態(tài)toLevel()方法即可。關(guān)于如何實(shí)現(xiàn)自己的Level類將會在配置文件相關(guān)小節(jié)中詳細(xì)討論。
LoggerRepository類
LoggerRepository從概念以及字面上來說它就是一個Logger實(shí)例的容器:一方面相同名字的Logger實(shí)例只需要創(chuàng)建一次,在后面的使用中,只需要從這個容器中取即可;另一方面,Logger容器可以存放從配置文件中解析出來的信息,從而使配置信息可以無縫的應(yīng)用到Log4J內(nèi)部系統(tǒng)中;最后Logger容器還為維護(hù)Logger的樹狀層次結(jié)構(gòu)提供了方面,每個Logger只維護(hù)父節(jié)點(diǎn)的信息,有了Logger容器的存在則可以很容易的找到一個新的Logger實(shí)例的父節(jié)點(diǎn);關(guān)于Logger容器將在下一節(jié)中詳細(xì)講解。
LoggingEvent類
LoggingEvent個人感覺用LoggingContext更合適一些,它是對一次日志記錄時(shí)哪能獲取到的數(shù)據(jù)的封裝。它包含了以下信息以提供Layout在format()方法中使用:
1. fqnOfCategoryClass:日志記錄接口(默認(rèn)為Logger)的類全名,該信息主要用于計(jì)算日志記錄點(diǎn)的源文件、調(diào)用方法以及行號等位置信息。
2. locationInfo:通過fqnOfCategoryClass計(jì)算位置信息,位置信息的計(jì)算由LocationInfo類實(shí)現(xiàn),這些信息可以提供給Layout使用。
3. logger:目前來看主要是通過Logger實(shí)例取得LogRepository實(shí)例,并通過LogRepository取得注冊的ObjectRender實(shí)例,如果有的話。
4. loggerName:當(dāng)前日志記錄的Logger名稱,提供給Layout使用。
5. threadName:當(dāng)前線程名,提供給Layout使用。
6. level:當(dāng)前日志的級別,提供給Layout使用。
7. message:當(dāng)前日志類,一般是String類型,但是也可以通過注冊ObjectRender,然后傳入相應(yīng)的其他對象類型。
8. renderedMessage:經(jīng)過ObjectRender處理后的日志信息,提供給Layout使用。
9. throwableInfo:異常信息,如果存在的話,提供給Layout使用。
10. timestamp:創(chuàng)建LoggingEvent實(shí)例的時(shí)間,提供給Layout使用。
11. 其他相對不常用的信息將會在后面小節(jié)中講解。
LoggingEvent只是一個簡單的數(shù)據(jù)對象(DO),因而其實(shí)現(xiàn)還是比較簡單的,即在創(chuàng)建實(shí)例時(shí)將數(shù)據(jù)提供給它,在其他類(Layout等)使用它時(shí)通過getXXX()方法取數(shù)據(jù)。不過還是有幾個方法可以簡單的講解一下。
LocationInfo類計(jì)算位置信息
LocationInfo所指的位置信息主要包括記錄日志所在的源文件名、類名、方法名、所在源文件的行號。
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;
我們知道在異常棧中每一條記錄都包含了方法調(diào)用對應(yīng)的這些信息,Log4J的這些信息正是利用了這個原理,即通過構(gòu)建一個Throwable實(shí)例,而后在該Throwable的棧信息中解析出來的:
2 if (locationInfo == null) {
3 locationInfo = new LocationInfo(new Throwable(),
4 fqnOfCategoryClass);
5 }
6 return locationInfo;
7 }
以上Throwable一般會產(chǎn)生如下異常棧:
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。
如果當(dāng)前JDK版本是1.4以上,我們就可以通過JDK提供的一些方法來查找:
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 }
否則,則需要我們通過字符串查找的方式來查找:
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值,在獲取其他單個值時(shí)還需要做相應(yīng)的字符串解析:
className:
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);
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 }
2 int ibegin = fullInfo.lastIndexOf(':', iend - 1);
3 if (ibegin == -1)
4 lineNumber = NA;
5 else
6 lineNumber = this.fullInfo.substring(ibegin + 1, iend);
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實(shí)例,如果是非String類型,會先使用注冊的ObjectRender(在LogRepository中查找注冊的ObjectRender信息)處理成String后返回,若沒有找到相應(yīng)的ObjectRender,則使用默認(rèn)的ObjectRender,它只是調(diào)用該消息實(shí)例的toString()方法。
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實(shí)例獲取異常棧字符串?dāng)?shù)組。同時(shí)還支持自定義的ThrowableRender(在LogRepository中設(shè)置),默認(rèn)的ThrowableRender通過系統(tǒng)printStackTrace()方法來獲取信息:
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負(fù)責(zé)將LoggingEvent中的信息格式化成一行日志信息。對不同格式的日志可能還需要提供頭和尾等信息。另外有些Layout不會處理異常信息,此時(shí)ignoresThrowable()方法返回false,并且異常信息需要Appender來處理,如PatternLayout。
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的實(shí)現(xiàn)比較簡單,如SimpleLayout對一行日志信息只是打印日志級別信息以及日志信息。
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 }
關(guān)于Layout更詳細(xì)的信息將會在以后小節(jié)中介紹。
Appender接口
Appender負(fù)責(zé)定義日志輸出的目的地,它可以是控制臺(ConsoleAppender)、文件(FileAppender)、JMS服務(wù)器(JmsLogAppender)、以Email的形式發(fā)送出去(SMTPAppender)等。Appender是一個命名的實(shí)體,另外它還包含了對Layout、ErrorHandler、Filter等引用:
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在寫日志時(shí),通過繼承機(jī)制遍歷所有注冊到它本身和其父節(jié)點(diǎn)的Appender(在additivity為true的情況下),調(diào)用doAppend()方法,實(shí)現(xiàn)日志的寫入。在doAppend方法中,若當(dāng)前Appender注冊了Filter,則doAppend還會判斷當(dāng)前日志時(shí)候通過了Filter的過濾,通過了Filter的過濾后,如果當(dāng)前Appender繼承自SkeletonAppender,還會檢查當(dāng)前日志級別時(shí)候要比當(dāng)前Appender本身的日志級別閥門要打,所有這些都通過后,才會將LoggingEvent實(shí)例傳遞給Layout實(shí)例以格式化成一行日志信息,最后寫入相應(yīng)的目的地,在這些操作中,任何出現(xiàn)的錯誤都由ErrorHandler字段來處理。
SkeletonAppender類
目前Log4J實(shí)現(xiàn)的Appender都繼承自SkeletonAppender類,該類對Appender接口提供了最基本的實(shí)現(xiàn),并且引入了Threshold的概念,即所有的比當(dāng)前Appender定義的日志級別閥指要大的日志才會記錄下來。
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實(shí)現(xiàn)了doAppend()方法,它首先檢查日志級別是否要比threshold要大;然后如果注冊了Filter,則使用Filter對LoggingEvent實(shí)例進(jìn)行過濾,如果Filter返回Filter.DENY則doAppend()退出,否則執(zhí)行append()方法,該方法由子類實(shí)現(xiàn)。
在Log4J中,Filter組成一條鏈,它定了以decide()方法,由子類實(shí)現(xiàn),若返回DENY則日志不會被記錄、NEUTRAL則繼續(xù)檢查下一個Filter實(shí)例、ACCEPT則Filter通過,繼續(xù)執(zhí)行后面的寫日志操作。使用Filter可以為Appender加入一些出了threshold以外的其他邏輯,由于它本身是鏈狀的,而且它的執(zhí)行是橫跨在Appender的doAppend方法中,因而這也是一個典型的AOP的概念。Filter的實(shí)現(xiàn)將會在下一小節(jié)中講解。
SkeletonAppender還重寫了finalize()方法,這是因?yàn)?/span>Log4J本身作為一個組件,它可能還是通過其他組件如commons-logging或slf4j組件間接的引入,因而使用它的程序不應(yīng)該對它存在依賴的,然而在程序退出之前所有的Appender需要調(diào)用close()方法以釋放它所占據(jù)的資源,為了不在使用Log4J的程序手動的close()的方法,以減少Log4J代碼的侵入性,因而Log4J將close()的方法調(diào)用加入到finalize()方法中,即在垃圾回收器回收Appender實(shí)例時(shí)就會調(diào)用它的close()方法。
WriterAppender類和ConsoleAppender類
WriterAppender將日志寫入Java IO中,它繼承自SkeletonAppender類。它引入了三個字段:immediateFlush,指定沒寫完一條日志后,即將日志內(nèi)容刷新到設(shè)備中,雖然這么做會有一點(diǎn)性能上的損失,但是如果不怎么做,則會出現(xiàn)在程序異常終止的時(shí)候無法看到部分日志信息,而經(jīng)常這些丟失的日志信息要用于分析為什么會出現(xiàn)異常終止的情況,因而一般推薦將該值設(shè)置為true,即默認(rèn)值;econding用于定義日志文本的編碼方式;qw定義寫日志的writer,它可以是文件或是控制臺等Java IO支持的流。
在寫日志文本前,WriterAppender還會做其他檢查,如該Appender不能已經(jīng)closed、qw和layout必須有值等,而后才可以將layout格式化后的日志行寫入設(shè)備中。若layout本身不處理異常問題,則有Appender處理異常問題。最后如果每行日志需要刷新,則調(diào)用刷新操作。
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實(shí)例傳遞給WriterAppender以構(gòu)建相應(yīng)的writer,最后實(shí)現(xiàn)將日志寫入到控制臺中。
Filter類
在Log4J中,Filter組成一條鏈,它定了以decide()方法,由子類實(shí)現(xiàn),若返回DENY則日志不會被記錄、NEUTRAL則繼續(xù)檢查下一個Filter實(shí)例、ACCEPT則Filter通過,繼續(xù)執(zhí)行后面的寫日志操作。使用Filter可以為Appender加入一些出了threshold以外的其他邏輯,由于它本身是鏈狀的,而且它的執(zhí)行是橫跨在Appender的doAppend方法中,因而這也是一個典型的AOP的概念。
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值,可以將其應(yīng)用在Filter鏈尾,實(shí)現(xiàn)如果之前的Filter都沒有通過,則該LoggingEvent沒有通過,類似或的操作:
2 public int decide(LoggingEvent event) {
3 return Filter.DENY;
4 }
5 }
StringMatchFilter通過日志消息中的字符串來判斷Filter后的狀態(tài):
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判斷日志級別是否和設(shè)置的級別匹配以決定Filter后的狀態(tài):
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判斷日志級別是否在設(shè)置的級別范圍內(nèi)以決定Filter后的狀態(tài):
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 }
總結(jié)
這一系列終于是結(jié)束了。本文主要介紹了Log4J核心類的實(shí)現(xiàn)和他們之間的交互關(guān)系。涉及到各個模塊本身的其他詳細(xì)信息將會在接下來的小節(jié)中詳細(xì)介紹,如LogRepository與配置信息、Appender類結(jié)構(gòu)的詳細(xì)信息、Layout類結(jié)構(gòu)的詳細(xì)信息以及部分LoggingEvent提供的高級功能。而像Level、Logger本身,由于內(nèi)容不多,已經(jīng)在這一小節(jié)中全部介紹完了。