Layout抽象類
Layout類是所有Log4J中Layout的基類,它是一個抽象類,定義了Layout的接口。
1. format()方法:將LoggingEvent類中的信息格式化成一行日志。
2. getContentType():定義日志文件的內容類型,目前在Log4J中只是在SMTPAppender中用到,用于設置發送郵件的郵件內容類型。而Layout本身也只有HTMLLayout實現了它。
3. getHeader():定義日志文件的頭,目前在Log4J中只是在HTMLLayout中實現了它。
4. getFooter():定義日志文件的尾,目前在Log4J中只是HTMLLayout中實現了它。
5. ignoresThrowable():定義當前layout是否處理異常類型。在Log4J中,不支持處理異常類型的有:TTCLayout、PatternLayout、SimpleLayout。
6. 實現OptionHandler接口,該接口定義了一個activateOptions()方法,用于配置文件解析完后,同時應用所有配置,以解決有些配置存在依賴的情況。該接口將在配置文件相關的小節中詳細介紹。
由于Layout接口定義比較簡單,因而其代碼也比較簡單:
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 }
SimpleLayout類
SimpleLayout是最簡單的Layout,它只是打印消息級別和渲染后的消息,并且不處理異常信息。不過這里很奇怪為什么把sbuf作為成員變量?個人感覺這個會在多線程中引起問題~~~~其代碼如下:
1 public String format(LoggingEvent event) {
2 sbuf.setLength(0);
3 sbuf.append(event.getLevel().toString());
4 sbuf.append(" - ");
5 sbuf.append(event.getRenderedMessage());
6 sbuf.append(LINE_SEP);
7 return sbuf.toString();
8 }
9 public boolean ignoresThrowable() {
10 return true;
11 }
1 @Test
2 public void testSimpleLayout() {
3 configSetup(new SimpleLayout());
4 logTest();
5 }
INFO - Begin to execute testBasic() method
INFO - Executing
ERROR - Catching an Exception
java.lang.Exception: Deliberately throw an Exception
at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:48)
at levin.log4j.layout.LayoutTest.testSimpleLayout(LayoutTest.java:25)

INFO - Execute testBasic() method finished. HTMLLayout類
HTMLLayout將日志消息打印成HTML格式,Log4J中HTMLLayout的實現中將每一條日志信息打印成表格中的一行,因而包含了一些Header和Footer信息。并且HTMLLayout類還支持配置是否打印位置信息和自定義title。最終HTMLLayout的日志打印格式如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>${title}</title>
<style type="text/css">
<!--
body, table {font-family: arial,sans-serif; font-size: x-small;}
th {background: #336699; color: #FFFFFF; text-align: left;}
-->
</style>
</head>
<body bgcolor="#FFFFFF" topmargin="6" leftmargin="6">
<hr size="1" noshade>
Log session start time ${currentTime}<br>
<br>
<table cellspacing="0" cellpadding="4" border="1" bordercolor="#224466" width="100%">
<tr>
<th>Time</th>
<th>Thread</th>
<th>Level</th>
<th>Category</th>
<th>File:Line</th>
<th>Message</th>
</tr>
<tr>
<td>${timeElapsedFromStart}</td>
<td title="${threadName} thread">${theadName}</td>
<td title="Level">
#if(${level} == “DEBUG”)
<font color="#339933">DEBUG</font>
#elseif(${level} >= “WARN”)
<font color=”#993300”><strong>${level}</Strong></font>
#else
${level}
</td>
<td title="${loggerName} category">levin.log4j.test.TestBasic</td>
<td>${fileName}:${lineNumber}</td>
<td title="Message">${renderedMessage}</td>
</tr>
<tr><td bgcolor="#EEEEEE" style="font-size : xx-small;" colspan="6" title="Nested Diagnostic Context">NDC: ${NDC}</td></tr>
<tr><td bgcolor="#993300" style="color:White; font-size : xx-small;" colspan="6">java.lang.Exception: Deliberately throw an Exception
<br> at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:51)
<br> at levin.log4j.layout.LayoutTest.testHTMLLayout(LayoutTest.java:34)

</td></tr> 以上所有HTML內容信息都要經過轉義,即: ’<’ => < ‘>’ => > ‘&’ => & ‘”’ => "從上信息可以看到HTMLLayout支持異常處理,并且它也實現了getContentType()方法:
1 public String getContentType() {
2 return "text/html";
3 }
4 public boolean ignoresThrowable() {
5 return false;
6 }
1 @Test
2 public void testHTMLLayout() {
3 HTMLLayout layout = new HTMLLayout();
4 layout.setLocationInfo(true);
5 layout.setTitle("Log4J Log Messages HTMLLayout test");
6 configSetup(layout);
7 logTest();
8 }
XMLLayout類
XMLLayout將日志消息打印成XML文件格式,打印出的XML文件不是一個完整的XML文件,它可以外部實體引入到一個格式正確的XML文件中。如XML文件的輸出名為abc,則可以通過以下方式引入:
<?xml version="1.0" ?>
<!DOCTYPE log4j:eventSet PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd" [<!ENTITY data SYSTEM "abc">]>
<log4j:eventSet version="1.2" xmlns:log4j="http://jakarta.apache.org/log4j/">
&data;
</log4j:eventSet>
XMLLayout還支持設置是否支持打印位置信息以及MDC(Mapped Diagnostic Context)信息,他們的默認值都為false:
1 private boolean locationInfo = false;
2 private boolean properties = false;
<log4j:event logger="${loggerName}" timestamp="${eventTimestamp}" level="${Level}" thread="${threadName}">
<log4j:message><![CDATA[${renderedMessage}]]></log4j:message>
#if ${ndc} != null
<log4j:NDC><![CDATA[${ndc}]]</log4j:NDC>
#endif
#if ${throwableInfo} != null
<log4j:throwable><
at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:54)
at levin.log4j.layout.LayoutTest.testXMLLayout(LayoutTest.java:43)

]]></log4j:throwable>
#endif
#if ${locationInfo} != null
<log4j:locationInfo class="${className}" method="${methodName}" file="${fileName}" line="${lineNumber}"/>
#endif
#if ${properties} != null
<log4j:properties>
#foreach ${key} in ${keyset}
<log4j:data name=”${key}” value=”${propValue}”/>
#end
</log4j:properties>
#endif
</log4j:event> 從以上日志格式也可以看出XMLLayout已經處理了異常信息。
1 public boolean ignoresThrowable() {
2 return false;
3 }
1 @Test
2 public void testXMLLayout() {
3 XMLLayout layout = new XMLLayout();
4 layout.setLocationInfo(true);
5 layout.setProperties(true);
6 configSetup(layout);
7 logTest();
8 }
TTCCLayout類
TTCCLayout貌似有特殊含義,不過這個我還不太了解具體是什么意思。從代碼角度上,該Layout包含了time, thread, category, nested diagnostic context information, and rendered message等信息。其中是否打印thread(threadPrinting), category(categoryPrefixing), nested diagnostic(contextPrinting)信息是可以配置的。TTCCLayout不處理異常信息。其中format()函數代碼:
1 public String format(LoggingEvent event) {
2 buf.setLength(0);
3 dateFormat(buf, event);
4 if (this.threadPrinting) {
5 buf.append('[');
6 buf.append(event.getThreadName());
7 buf.append("] ");
8 }
9 buf.append(event.getLevel().toString());
10 buf.append(' ');
11 if (this.categoryPrefixing) {
12 buf.append(event.getLoggerName());
13 buf.append(' ');
14 }
15 if (this.contextPrinting) {
16 String ndc = event.getNDC();
17 if (ndc != null) {
18 buf.append(ndc);
19 buf.append(' ');
20 }
21 }
22 buf.append("- ");
23 buf.append(event.getRenderedMessage());
24 buf.append(LINE_SEP);
25 return buf.toString();
26 }
這里唯一需要解釋的就是dateFormat()函數,它是在其父類DateLayout中定義的,用于格式化時間信息。DateLayout支持的時間格式有:
NULL_DATE_FORMAT:NULL,此時dateFormat字段為null
RELATIVE_TIME_DATE_FORMAT:RELATIVE,默認值,此時dateFormat字段為RelativeTimeDateFormat實例。其實現即將LoggingEvent中的timestamp-startTime(RelativeTimeDateFormat實例化是初始化)。
ABS_TIME_DATE_FORMAT:ABSOLUTE,此時dateFormat字段為AbsoluteTimeDateFormat實例。它將時間信息格式化成HH:mm:ss,SSS格式。這里對性能優化有一個可以參考的地方,即在格式化是,它只是每秒做一次格式化計算,而對后綴sss的變化則直接計算出來。
DATE_AND_TIME_DATE_FORMAT:DATE,此時dateFormat字段為DateTimeDateFormat實例,此時它將時間信息格式化成dd MMM yyyy HH:mm:ss,SSS。
ISO8601_DATE_FORMAT:ISO8601,此時dateFormat字段為ISO8601DateFormat實例,它將時間信息格式化成yyyy-MM-dd HH:mm:ss,SSS。
以及普通的SimpleDateFormat中設置pattern的支持。
Log4J推薦使用自己定義的DateFormat,其文檔上說Log4J中定義的DateFormat信息有更好的性能。
測試用例:
1 @Test
2 public void testTTCCLayout() {
3 TTCCLayout layout = new TTCCLayout();
4 layout.setDateFormat("ISO8601");
5 configSetup(layout);
6 logTest();
7 }
2012-07-02 23:07:34,017 [main] INFO levin.log4j.test.TestBasic - Begin to execute testBasic() method
2012-07-02 23:07:34,018 [main] INFO levin.log4j.test.TestBasic - Executing
2012-07-02 23:07:34,019 [main] ERROR levin.log4j.test.TestBasic - Catching an Exception
java.lang.Exception: Deliberately throw an Exception
at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:63)

2012-07-02 23:07:34,022 [main] INFO levin.log4j.test.TestBasic - Execute testBasic() method finished. PatternLayout類
個人感覺PatternLayout是Log4J中最常用也是最復雜的Layout了。PatternLayout的設計理念是LoggingEvent實例中所有的信息是否顯示、以何種格式顯示都是可以自定義的,比如要用PatternLayout實現TTCCLayout中的格式,可以這樣設置:
1 @Test
2 public void testPatternLayout() {
3 PatternLayout layout = new PatternLayout();
4 layout.setConversionPattern("%r [%t] %p %c %x - %m%n");
5 configSetup(layout);
6 logTest();
7 }
該測試用例的運行結果和TTCCLayout中默認的結果是一樣的。完整的,PatternLayout中可以設置的參數有(模擬C語言的printf中的參數):
格式字符 | 結果 |
c | 顯示logger name,可以配置精度,如%c{2},從后開始截取。 |
C | 顯示日志寫入接口的雷鳴,可以配置精度,如%C{1},從后開始截取。注:會影響性能,慎用。 |
d | 顯示時間信息,后可定義格式,如%d{HH:mm:ss,SSS},或Log4J中定義的格式,如%d{ISO8601},%d{ABSOLUTE},Log4J中定義的時間格式有更好的性能。 |
F | 顯示文件名,會影響性能,慎用。 |
l | 顯示日志打印是的詳細位置信息,一般格式為full.qualified.caller.class.method(filename:lineNumber)。注:該參數會極大的影響性能,慎用。 |
L | 顯示日志打印所在源文件的行號。注:該參數會極大的影響性能,慎用。 |
m | 顯示渲染后的日志消息。 |
M | 顯示打印日志所在的方法名。注:該參數會極大的影響性能,慎用。 |
n | 輸出平臺相關的換行符。 |
p | 顯示日志Level |
r | 顯示相對時間,即從程序開始(實際上是初始化LoggingEvent類)到日志打印的時間間隔,以毫秒為單位。 |
t | 顯示打印日志對應的線程名稱。 |
x | 顯示與當前線程相關聯的NDC(Nested Diagnostic Context)信息。 |
X | 顯示和當前想成相關聯的MDC(Mapped Diagnostic Context)信息。 |
% | %%表達顯示%字符 |
而且PatternLayout還支持在格式字符串前加入精度信息:
%-min.max[conversionChar],如%-20.30c表示顯示日志名,左對齊,最短20個字符,最長30個字符,不足用空格補齊,超過的截取(從后往前截取)。
因而PatternLayout實現中,最主要要解決的是如何解析上述定義的格式。實現上述格式的解析,一種最直觀的方法是每次遍歷格式字符串,當遇到’%’,則進入解析模式,根據’%’后不同的字符做不同的解析,對其他字符,則直接作為輸出的字符。這種代碼會比較直觀,但是它每次都要遍歷格式字符串,會引起一些性能問題,而且如果在將來引入新的格式字符,需要直接改動PatternLayout代碼,不利于可擴展性。
為了解決這個問題,PatternLayout引入了解釋器模式:

其中PatternParser負責解析PatternLayout中設置的conversion pattern,它將conversion pattern解析出一個鏈狀的PatternConverter,而后在每次格式化LoggingEvent實例是,只需要遍歷該鏈即可:
1 public String format(LoggingEvent event) {
2 PatternConverter c = head;
3 while (c != null) {
4 c.format(sbuf, event);
5 c = c.next;
6 }
7 return sbuf.toString();
8 }
在解析conversion pattern時,PatternParser使用了有限狀態機的方法:
即PatternParser定義了五種狀態,初始化時LITERAL_STATE,當遍歷完成,則退出;否則,如果當前字符不是’%’,則將該字符添加到currentLiteral中,繼續遍歷;否則,若下一字符是’%’,則將其當做基本字符處理,若下一字符是’n’,則添加換行符,否則,將之前收集的literal字符創建LiteralPatternConverter實例,添加到相應的PatternConverter鏈中,清空currentLiteral實例,并添加下一字符,解析器進入CONVERTER_STATE狀態:
1 case LITERAL_STATE:
2 // In literal state, the last char is always a literal.
3 if (i == patternLength) {
4 currentLiteral.append(c);
5 continue;
6 }
7 if (c == ESCAPE_CHAR) {
8 // peek at the next char.
9 switch (pattern.charAt(i)) {
10 case ESCAPE_CHAR:
11 currentLiteral.append(c);
12 i++; // move pointer
13 break;
14 case 'n':
15 currentLiteral.append(Layout.LINE_SEP);
16 i++; // move pointer
17 break;
18 default:
19 if (currentLiteral.length() != 0) {
20 addToList(new LiteralPatternConverter(
21 currentLiteral.toString()));
22 // LogLog.debug("Parsed LITERAL converter: \""
23 // +currentLiteral+"\".");
24 }
25 currentLiteral.setLength(0);
26 currentLiteral.append(c); // append %
27 state = CONVERTER_STATE;
28 formattingInfo.reset();
29 }
30 } else {
31 currentLiteral.append(c);
32 }
33 break;
對CONVERTER_STATE狀態,若當前字符是’-‘,則表明左對齊;若遇到’.’,則進入DOT_STATE狀態;若遇到數字,則進入MIN_STATE狀態;若遇到其他字符,則根據字符解析出不同的PatternConverter,并且如果存在可選項信息(’{}’中的信息),一起提取出來,并將狀態重新設置成LITERAL_STATE狀態:
1 case CONVERTER_STATE:
2 currentLiteral.append(c);
3 switch (c) {
4 case '-':
5 formattingInfo.leftAlign = true;
6 break;
7 case '.':
8 state = DOT_STATE;
9 break;
10 default:
11 if (c >= '0' && c <= '9') {
12 formattingInfo.min = c - '0';
13 state = MIN_STATE;
14 } else
15 finalizeConverter(c);
16 } // switch
17 break;
進入MIN_STATE狀態,首先判斷當期字符是否為數字,若是,則繼續計算精度的最小值;若遇到’.’,則進入DOT_STATE狀態;否則,根據字符解析出不同的PatternConverter,并且如果存在可選項信息(’{}’中的信息),一起提取出來,并將狀態重新設置成LITERAL_STATE狀態:
1 case MIN_STATE:
2 currentLiteral.append(c);
3 if (c >= '0' && c <= '9')
4 formattingInfo.min = formattingInfo.min * 10 + (c - '0');
5 else if (c == '.')
6 state = DOT_STATE;
7 else {
8 finalizeConverter(c);
9 }
10 break;
進入DOT_STATE狀態,如果當前字符是數字,則進入MAX_STATE狀態;格式出錯,回到LITERAL_STATE狀態:
1 case DOT_STATE:
2 currentLiteral.append(c);
3 if (c >= '0' && c <= '9') {
4 formattingInfo.max = c - '0';
5 state = MAX_STATE;
6 } else {
7 LogLog.error("Error occured in position " + i
8 + ".\n Was expecting digit, instead got char \""
9 + c + "\".");
10 state = LITERAL_STATE;
11 }
12 break;
進入MAX_STATE狀態,若為數字,則繼續計算最大精度值,否則,根據字符解析出不同的PatternConverter,并且如果存在可選項信息(’{}’中的信息),一起提取出來,并將狀態重新設置成LITERAL_STATE狀態:
1 case MAX_STATE:
2 currentLiteral.append(c);
3 if (c >= '0' && c <= '9')
4 formattingInfo.max = formattingInfo.max * 10 + (c - '0');
5 else {
6 finalizeConverter(c);
7 state = LITERAL_STATE;
8 }
9 break;
對finalizeConvert()方法的實現,只是簡單的根據不同的格式字符創建相應的PatternConverter,而且各個PatternConverter中的實現也是比較簡單的,有興趣的童鞋可以直接看源碼,這里不再贅述。
PatternLayout的這種有限狀態機的設置是代碼結構更加清晰,而引入解釋器模式,以后如果需要增加新的格式字符,只需要添加一個新的PatternConverter以及一小段case語句塊即可,減少了因為需求改變而引起的代碼的傾入性。
EnhancedPatternLayout類
在Log4J文檔中指出PatternLayout中存在同步問題以及其他問題,因而推薦使用EnhancedPatternLayout來替換它。對這句話我個人并沒有理解,首先關于同步問題,感覺其他Layout中也有涉及到,而且對一個Appender來說,它的doAppend()方法是同步方法,因而只要不在多個Appender之間共享同一個Layout實例,也不會出現同步問題;更令人費解的是關于其他問題的表述,說實話,我還沒有發現具體有什么其他問題,所以期待其他人來幫我解答。
但是不管怎么樣,我們還是來簡單的了解一下EnhancedPatternLayout的一些設計思想吧。EnhancedPatternLayout提供了和PatternLayout相同的接口,只是其內部實現有一些改變。EnhancedPatternLayout引入了LoggingEventPatternConverter,它會根據不同的子類的定義從LoggingEvent實例中獲取相應的信息;使用PatternParser解析出關于patternConverters和FormattingInfo兩個相對獨立的集合,遍歷這兩個集合,構建出兩個對應的數組,以在以后的解析中使用。大體上,EnhancedPatternLayout還是類似PatternLayout的設計。這里不再贅述。
NDC和MDC
有時候,一段相同的代碼需要處理不同的請求,從而導致一些看似相同的日志其實是在處理不同的請求。為了避免這種情況,從而使日志能夠提供更多的信息。
要實現這種功能,一個簡單的做法每個請求都有一個唯一的ID或Name,從而在處理這樣的請求的日志中每次都寫入該信息從而區分看似相同的日志。但是這種做法需要為每個日志打印語句添加相同的代碼,而且這個ID或Name信息要一直隨著方法調用傳遞下去,非常不方便,而且容易出錯。Log4J提供了兩種機制實現類似的需求:NDC和MDC。NDC是Nested Diagnostic Contexts的簡稱,它提供一個線程級別的棧,用戶向這個棧中壓入信息,這些信息可以通過Layout顯示出來。MDC是Mapped Diagnostic Contexts的簡稱,它提供了一個線程級別的Map,用戶向這個Map中添加鍵值對信息,這些信息可以通過Layout以指定Key的方式顯示出來。
NDC主要的使用接口有:
1 public class NDC {
2 public static String get();
3 public static String pop();
4 public static String peek();
5 public static void push(String message);
6 public static void remove();
7 }