layout負責将loggingevent中的資訊格式化成一行日志資訊。對不同格式的日志可能還需要提供頭和尾等資訊。另外有些layout不會處理異常資訊,此時ignoresthrowable()方法傳回false,并且異常資訊需要appender來處理,如patternlayout。
log4j自身實作了7個layout類,我們可以通過繼承自layout類以實作使用者自定義的日志消息格式。log4j中已定義的layout類結構如圖:
簡單的寫了一個功能性測試的類,進而對不同layout的輸出有比較直覺的了解。為了簡單起見,所有的測試都列印到控制台。
1 public class layouttest {
2 private logger root;
3 @before
4 public void setup() {
5 root = logmanager.getrootlogger();
6 }
7 @test
8 public void testxxxlayout() {
9 configsetup(new xxxlayout());
10 logtest();
11 }
12 private void logtest() {
13 logger log = logger.getlogger("levin.log4j.test.testbasic");
14 log.info("begin to execute testbasic() method
");
15 log.info("executing
16 try {
17 throw new exception("deliberately throw an exception
18 } catch(exception e) {
19 log.error("catching an exception", e);
20 }
21 log.info("execute testbasic() method finished.");
22 }
23 private void configsetup(layout layout) {
24 root.addappender(createconsoleappender(layout));
25 }
26 private appender createconsoleappender(layout layout) {
27 return new consoleappender(layout);
28 }
29 }
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是最簡單的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将日志消息列印成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>
<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><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>&nbsp;&nbsp;&nbsp;&nbsp; at levin.log4j.layout.layouttest.logtest(layouttest.java:51)
<br>&nbsp;&nbsp;&nbsp;&nbsp; at levin.log4j.layout.layouttest.testhtmllayout(layouttest.java:34)
</td></tr>
以上所有html内容資訊都要經過轉義,即: ’<’ => &lt; ‘>’ => &gt; ‘&’ => &amp; ‘”’ => &quot;從上資訊可以看到htmllayout支援異常處理,并且它也實作了getcontenttype()方法:
1 public string getcontenttype() {
2 return "text/html";
3 }
4 public boolean ignoresthrowable() {
5 return false;
6 }
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将日志消息列印成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;
xmllayout的輸出格式如下:
<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><![cdata[java.lang.exception: deliberately throw an exception
at levin.log4j.layout.layouttest.logtest(layouttest.java:54)
at levin.log4j.layout.layouttest.testxmllayout(layouttest.java:43)
]]></log4j:throwable>
#if ${locationinfo} != null
<log4j:locationinfo class="${classname}" method="${methodname}" file="${filename}" line="${linenumber}"/>
#if ${properties} != null
<log4j:properties>
#foreach ${key} in ${keyset}
<log4j:data name=”${key}” value=”${propvalue}”/>
#end
</log4j:properties>
</log4j:event>
從以上日志格式也可以看出xmllayout已經處理了異常資訊。
1 public boolean ignoresthrowable() {
2 return false;
2 public void testxmllayout() {
3 xmllayout layout = new xmllayout();
5 layout.setproperties(true);
ttcclayout貌似有特殊含義,不過這個我還不太了解具體是什麼意思。從代碼角度上,該layout包含了time, thread, category, nested diagnostic context information, and rendered message等資訊。其中是否列印thread(threadprinting), category(categoryprefixing), nested diagnostic(contextprinting)資訊是可以配置的。ttcclayout不處理異常資訊。其中format()函數代碼:
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(' ');
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資訊有更好的性能。
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
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是log4j中最常用也是最複雜的layout了。patternlayout的設計理念是loggingevent執行個體中所有的資訊是否顯示、以何種格式顯示都是可以自定義的,比如要用patternlayout實作ttcclayout中的格式,可以這樣設定:
2 public void testpatternlayout() {
3 patternlayout layout = new patternlayout();
4 layout.setconversionpattern("%r [%t] %p %c %x - %m%n");
該測試用例的運作結果和ttcclayout中預設的結果是一樣的。完整的,patternlayout中可以設定的參數有(模拟c語言的printf中的參數):
格式字元
結果
c
顯示logger name,可以配置精度,如%c{2},從後開始截取。
顯示日志寫入接口的雷鳴,可以配置精度,如%c{1},從後開始截取。注:會影響性能,慎用。
d
顯示時間資訊,後可定義格式,如%d{hh:mm:ss,sss},或log4j中定義的格式,如%d{iso8601},%d{absolute},log4j中定義的時間格式有更好的性能。
f
顯示檔案名,會影響性能,慎用。
l
顯示日志列印是的詳細位置資訊,一般格式為full.qualified.caller.class.method(filename:linenumber)。注:該參數會極大的影響性能,慎用。
顯示日志列印所在源檔案的行号。注:該參數會極大的影響性能,慎用。
m
顯示渲染後的日志消息。
顯示列印日志所在的方法名。注:該參數會極大的影響性能,慎用。
n
輸出平台相關的換行符。
p
顯示日志level
r
顯示相對時間,即從程式開始(實際上是初始化loggingevent類)到日志列印的時間間隔,以毫秒為機關。
t
顯示列印日志對應的線程名稱。
x
顯示與目前線程相關聯的ndc(nested diagnostic context)資訊。
顯示和目前想成相關聯的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();
在解析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;
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:
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:
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;
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語句塊即可,減少了因為需求改變而引起的代碼的傾入性。
在log4j文檔中指出patternlayout中存在同步問題以及其他問題,因而推薦使用enhancedpatternlayout來替換它。對這句話我個人并沒有了解,首先關于同步問題,感覺其他layout中也有涉及到,而且對一個appender來說,它的doappend()方法是同步方法,因而隻要不在多個appender之間共享同一個layout執行個體,也不會出現同步問題;更令人費解的是關于其他問題的表述,說實話,我還沒有發現具體有什麼其他問題,是以期待其他人來幫我解答。
但是不管怎麼樣,我們還是來簡單的了解一下enhancedpatternlayout的一些設計思想吧。enhancedpatternlayout提供了和patternlayout相同的接口,隻是其内部實作有一些改變。enhancedpatternlayout引入了loggingeventpatternconverter,它會根據不同的子類的定義從loggingevent執行個體中擷取相應的資訊;使用patternparser解析出關于patternconverters和formattinginfo兩個相對獨立的集合,周遊這兩個集合,建構出兩個對應的數組,以在以後的解析中使用。大體上,enhancedpatternlayout還是類似patternlayout的設計。這裡不再贅述。
有時候,一段相同的代碼需要處理不同的請求,進而導緻一些看似相同的日志其實是在處理不同的請求。為了避免這種情況,進而使日志能夠提供更多的資訊。
要實作這種功能,一個簡單的做法每個請求都有一個唯一的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();
即使用前,将和目前上下文資訊push如目前線程棧,使用完後pop出來:
1 @test
2 public void testndc() {
3 patternlayout layout = new patternlayout();
4 layout.setconversionpattern("%x - %m%n");
5 configsetup(layout);
6
7 ndc.push("levin");
8 ndc.push("ding");
9 logtest();
10 ndc.pop();
11 ndc.pop();
12 }
13 levin ding - begin to execute testbasic() method
14 levin ding - executing
15 levin ding - catching an exception
16 java.lang.exception: deliberately throw an exception
17 at levin.log4j.layout.layouttest.logtest(layouttest.java:86)
18
19 levin ding - execute testbasic() method finished.
ndc所有的操作都是針對目前線程的,因而不會影響其他線程。而在ndc實作中,使用一個hashtable,其key是線程執行個體,這樣的實作導緻使用者需要手動的調用remove方法,移除那些push進去的資料以及移除那些已經過期的線程資料,不然就會出現記憶體洩露的情況;另外,如果使用線程池,在沒有及時調用remove方法的情況下,容易前一線程的資料影響後一線程的結果。很奇怪為什麼這裡沒有threadlocal或者是weakreference,這樣就可以部分的解決忘記調用remove引起的後果,貌似是出于相容性的考慮?
mdc使用了theadlocal,因而它隻能使用在jdk版本大于1.2的環境中,然而其代碼實作和接口也更加簡潔:
1 public class mdc {
2 public static void put(string key, object o);
3 public static object get(string key);
4 public static void remove(string key);
5 public static void clear();
類似ndc,mdc在使用前也需要向其添加資料,結束後将其remove,但是remove操作不是必須的,因為它使用了theadlocal,因而不會引起記憶體問題;不過它還是可能在使用線程池的情況下引起問題,除非線程池在每一次線程運作結束後或每一次線程運作前将threadlocal的資料清除:
2 public void testmdc() {
4 layout.setconversionpattern("ip:%x{ip} name:%x{name} - %m%n");
7 mdc.put("ip", "127.0.0.1");
8 mdc.put("name", "levin");
10 mdc.remove("ip");
11 mdc.remove("name");
13 ip:127.0.0.1 name:levin - begin to execute testbasic() method
14 ip:127.0.0.1 name:levin - executing
15 ip:127.0.0.1 name:levin - catching an exception
17 at levin.log4j.layout.layouttest.logtest(layouttest.java:100)
19 ip:127.0.0.1 name:levin - execute testbasic() method finished.
雖然log4j提供了ndc和mdc機制,但是感覺它的實作還是有一定的侵入性的,如果要替換log子產品,則會出現一定的改動,雖然我也想不出更好的解決方法,但是總感覺這個不是一個比較好的方法,在我自己的項目中基本上沒有用到這個特性。