本文转载上善若水的博客,原文出处:http://www.blogjava.net/DLevin/archive/2012/07/04/382131.html。感谢作者的分享。
Layout负责将LoggingEvent中的信息格式化成一行日志信息。对不同格式的日志可能还需要提供头和尾等信息。另外有些Layout不会处理异常信息,此时ignoresThrowable()方法返回false,并且异常信息需要Appender来处理,如PatternLayout。
Log4J自身实现了7个Layout类,我们可以通过继承自Layout类以实现用户自定义的日志消息格式。Log4J中已定义的Layout类结构如图:
测试类
简单的写了一个功能性测试的类,从而对不同Layout的输出有比较直观的了解。为了简单起见,所有的测试都打印到控制台。
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());
logTest();
}
private void logTest() {
Logger log = Logger.getLogger("levin.log4j.test.TestBasic");
log.info("Begin to execute testBasic() method");
log.info("Executing");
try {
throw new Exception("Deliberately throw an Exception");
} catch(Exception e) {
log.error("Catching an Exception", e);
}
log.info("Execute testBasic() method finished.");
}
private void configSetup(Layout layout) {
root.addAppender(createConsoleAppender(layout));
}
private Appender createConsoleAppender(Layout layout) {
return new ConsoleAppender(layout);
}
}
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接口定义比较简单,因而其代码也比较简单:
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;
}
public String getFooter() {
return null;
}
abstract public boolean ignoresThrowable();
}
SimpleLayout类
SimpleLayout是最简单的Layout,它只是打印消息级别和渲染后的消息,并且不处理异常信息。不过这里很奇怪为什么把sbuf作为成员变量?个人感觉这个会在多线程中引起问题~~~~其代码如下:
);
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() {
return true;
}
测试用例:
public void testSimpleLayout() {
configSetup(new SimpleLayout());
logTest();
}
测试结果:
INFO - Executing
ERROR - Catching an Exception
java.lang.Exception: Deliberately throw an Exception
at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:)
at levin.log4j.layout.LayoutTest.testSimpleLayout(LayoutTest.java:)
INFO - Execute testBasic() method finished.
HTMLLayout类
HTMLLayout将日志消息打印成HTML格式,Log4J中HTMLLayout的实现中将每一条日志信息打印成表格中的一行,因而包含了一些Header和Footer信息。并且HTMLLayout类还支持配置是否打印位置信息和自定义title。最终HTMLLayout的日志打印格式如下:
<html>
<head>
<title>${title}</title>
<style type="text/css">
<!--
body, table {font-family: arial,sans-serif; font-size: x-small;}
th {background: #; color: #FFFFFF; text-align: left;}
-->
</style>
</head>
">
" noshade>
Log session start time ${currentTime}<br>
<br>
" 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”)
”><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>
" title="Nested Diagnostic Context">NDC: ${NDC}</td></tr>
">java.lang.Exception: Deliberately throw an Exception
)
)
</td></tr>
以上所有HTML内容信息都要经过转义,即: ’<’ => < ‘>’ => > ‘&’ => & ‘”’ => "从上信息可以看到HTMLLayout支持异常处理,并且它也实现了getContentType()方法:
return "text/html";
}
public boolean ignoresThrowable() {
return false;
}
测试用例:
public void testHTMLLayout() {
HTMLLayout layout = new HTMLLayout();
layout.setLocationInfo(true);
layout.setTitle("Log4J Log Messages HTMLLayout test");
configSetup(layout);
logTest();
}
XMLLayout类
XMLLayout将日志消息打印成XML文件格式,打印出的XML文件不是一个完整的XML文件,它可以外部实体引入到一个格式正确的XML文件中。如XML文件的输出名为abc,则可以通过以下方式引入:
<!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:
private boolean properties = false;
XMLLayout的输出格式如下:
<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:)
at levin.log4j.layout.LayoutTest.testXMLLayout(LayoutTest.java:)
]]></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已经处理了异常信息。
return false;
}
测试用例:
public void testXMLLayout() {
XMLLayout layout = new XMLLayout();
layout.setLocationInfo(true);
layout.setProperties(true);
configSetup(layout);
logTest();
}
TTCCLayout类
TTCCLayout貌似有特殊含义,不过这个我还不太了解具体是什么意思。从代码角度上,该Layout包含了time, thread, category, nested diagnostic context information, and rendered message等信息。其中是否打印thread(threadPrinting), category(categoryPrefixing), nested diagnostic(contextPrinting)信息是可以配置的。TTCCLayout不处理异常信息。其中format()函数代码:
);
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());
buf.append(' ');
if (this.categoryPrefixing) {
buf.append(event.getLoggerName());
buf.append(' ');
}
if (this.contextPrinting) {
String ndc = event.getNDC();
if (ndc != null) {
buf.append(ndc);
buf.append(' ');
}
}
buf.append("- ");
buf.append(event.getRenderedMessage());
buf.append(LINE_SEP);
return buf.toString();
}
这里唯一需要解释的就是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信息有更好的性能。
测试用例:
public void testTTCCLayout() {
TTCCLayout layout = new TTCCLayout();
layout.setDateFormat("ISO8601");
configSetup(layout);
logTest();
}
测试结果:
[main] INFO levin.log4j.test.TestBasic - Executing
[main] ERROR levin.log4j.test.TestBasic - Catching an Exception
java.lang.Exception: Deliberately throw an Exception
at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:)
[main] INFO levin.log4j.test.TestBasic - Execute testBasic() method finished.
PatternLayout类
个人感觉PatternLayout是Log4J中最常用也是最复杂的Layout了。PatternLayout的设计理念是LoggingEvent实例中所有的信息是否显示、以何种格式显示都是可以自定义的,比如要用PatternLayout实现TTCCLayout中的格式,可以这样设置:
public void testPatternLayout() {
PatternLayout layout = new PatternLayout();
layout.setConversionPattern("%r [%t] %p %c %x - %m%n");
configSetup(layout);
logTest();
}
该测试用例的运行结果和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实例是,只需要遍历该链即可:
PatternConverter c = head;
while (c != null) {
c.format(sbuf, event);
c = c.next;
}
return sbuf.toString();
}
在解析conversion pattern时,PatternParser使用了有限状态机的方法:
即PatternParser定义了五种状态,初始化时LITERAL_STATE,当遍历完成,则退出;否则,如果当前字符不是’%’,则将该字符添加到currentLiteral中,继续遍历;否则,若下一字符是’%’,则将其当做基本字符处理,若下一字符是’n’,则添加换行符,否则,将之前收集的literal字符创建LiteralPatternConverter实例,添加到相应的PatternConverter链中,清空currentLiteral实例,并添加下一字符,解析器进入CONVERTER_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)) {
case ESCAPE_CHAR:
currentLiteral.append(c);
i++; // move pointer
break;
case 'n':
currentLiteral.append(Layout.LINE_SEP);
i++; // move pointer
break;
default:
) {
addToList(new LiteralPatternConverter(
currentLiteral.toString()));
// LogLog.debug("Parsed LITERAL converter: \""
// +currentLiteral+"\".");
}
);
currentLiteral.append(c); // append %
state = CONVERTER_STATE;
formattingInfo.reset();
}
} else {
currentLiteral.append(c);
}
break;
对CONVERTER_STATE状态,若当前字符是’-‘,则表明左对齐;若遇到’.’,则进入DOT_STATE状态;若遇到数字,则进入MIN_STATE状态;若遇到其他字符,则根据字符解析出不同的PatternConverter,并且如果存在可选项信息(’{}’中的信息),一起提取出来,并将状态重新设置成LITERAL_STATE状态:
2 currentLiteral.append(c);
3 switch (c) {
4 case '-':
5 formattingInfo.leftAlign = true;
6 break;
7 case '.':
8 state = DOT_STATE;
9 break;
default:
') {
';
state = MIN_STATE;
} else
finalizeConverter(c);
} // switch
break;
进入MIN_STATE状态,首先判断当期字符是否为数字,若是,则继续计算精度的最小值;若遇到’.’,则进入DOT_STATE状态;否则,根据字符解析出不同的PatternConverter,并且如果存在可选项信息(’{}’中的信息),一起提取出来,并将状态重新设置成LITERAL_STATE状态:
2 currentLiteral.append(c);
')
');
5 else if (c == '.')
6 state = DOT_STATE;
7 else {
8 finalizeConverter(c);
9 }
break;
进入DOT_STATE状态,如果当前字符是数字,则进入MAX_STATE状态;格式出错,回到LITERAL_STATE状态:
2 currentLiteral.append(c);
') {
';
5 state = MAX_STATE;
6 } else {
7 LogLog.error("Error occured in position " + i
8 + ".\n Was expecting digit, instead got char \""
9 + c + "\".");
state = LITERAL_STATE;
}
break;
进入MAX_STATE状态,若为数字,则继续计算最大精度值,否则,根据字符解析出不同的PatternConverter,并且如果存在可选项信息(’{}’中的信息),一起提取出来,并将状态重新设置成LITERAL_STATE状态:
currentLiteral.append(c);
')
');
else {
finalizeConverter(c);
state = LITERAL_STATE;
}
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主要的使用接口有:
public static String get();
public static String pop();
public static String peek();
public static void push(String message);
public static void remove();
}
即使用前,将和当前上下文信息push如当前线程栈,使用完后pop出来:
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();
NDC.pop();
NDC.pop();
}
Levin Ding - Begin to execute testBasic() method
Levin Ding - Executing
Levin Ding - Catching an Exception
java.lang.Exception: Deliberately throw an Exception
)
Levin Ding - Execute testBasic() method finished.
NDC所有的操作都是针对当前线程的,因而不会影响其他线程。而在NDC实现中,使用一个Hashtable,其Key是线程实例,这样的实现导致用户需要手动的调用remove方法,移除那些push进去的数据以及移除那些已经过期的线程数据,不然就会出现内存泄露的情况;另外,如果使用线程池,在没有及时调用remove方法的情况下,容易前一线程的数据影响后一线程的结果。很奇怪为什么这里没有ThreadLocal或者是WeakReference,这样就可以部分的解决忘记调用remove引起的后果,貌似是出于兼容性的考虑?
MDC使用了TheadLocal,因而它只能使用在JDK版本大于1.2的环境中,然而其代码实现和接口也更加简洁:
public static void put(String key, Object o);
public static Object get(String key);
public static void remove(String key);
public static void clear();
}
类似NDC,MDC在使用前也需要向其添加数据,结束后将其remove,但是remove操作不是必须的,因为它使用了TheadLocal,因而不会引起内存问题;不过它还是可能在使用线程池的情况下引起问题,除非线程池在每一次线程运行结束后或每一次线程运行前将ThreadLocal的数据清除:
2 public void testMDC() {
3 PatternLayout layout = new PatternLayout();
4 layout.setConversionPattern("IP:%X{ip} Name:%X{name} - %m%n");
5 configSetup(layout);
6
7 MDC.put("ip", "127.0.0.1");
8 MDC.put("name", "levin");
9 logTest();
MDC.remove("ip");
MDC.remove("name");
}
IP:127.0.0.1 Name:levin - Begin to execute testBasic() method
IP:127.0.0.1 Name:levin - Executing
IP:127.0.0.1 Name:levin - Catching an Exception
java.lang.Exception: Deliberately throw an Exception
)
IP:127.0.0.1 Name:levin - Execute testBasic() method finished.
虽然Log4J提供了NDC和MDC机制,但是感觉它的实现还是有一定的侵入性的,如果要替换Log模块,则会出现一定的改动,虽然我也想不出更好的解决方法,但是总感觉这个不是一个比较好的方法,在我自己的项目中基本上没有用到这个特性。