纯java代码实现log4j_深入Log4J源码之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接口定义比较简单,因而其代码也比较简单:

1 publicabstractclassLayoutimplementsOptionHandler {2 publicfinalstaticString LINE_SEP=System.getProperty("line.separator");3 publicfinalstaticintLINE_SEP_LEN=LINE_SEP.length();4 abstractpublicString format(LoggingEvent event);5 publicString getContentType() {6 return"text/plain";7 }8 publicString getHeader() {9 returnnull;10 }11 publicString getFooter() {12 returnnull;13 }14 abstractpublicbooleanignoresThrowable();15 }

SimpleLayout类

SimpleLayout是最简单的Layout,它只是打印消息级别和渲染后的消息,并且不处理异常信息。不过这里很奇怪为什么把sbuf作为成员变量?个人感觉这个会在多线程中引起问题~~~~其代码如下:

1 publicString 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 returnsbuf.toString();8 }9 publicbooleanignoresThrowable() {10 returntrue;11 }

测试用例:

1 @Test2 publicvoidtestSimpleLayout() {3 configSetup(newSimpleLayout());4 logTest();5 }

测试结果:

INFO-Begin to execute testBasic() method

9b8a8a44dd1c74ae49c20a7cd451974e.png

INFO-Executing

9b8a8a44dd1c74ae49c20a7cd451974e.png

ERROR-Catching an Exception

java.lang.Exception: Deliberatelythrowan Exception

9b8a8a44dd1c74ae49c20a7cd451974e.png

at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:48)

at levin.log4j.layout.LayoutTest.testSimpleLayout(LayoutTest.java:25)

9b8a8a44dd1c74ae49c20a7cd451974e.png

INFO-Execute testBasic() method finished.

HTMLLayout类

HTMLLayout将日志消息打印成HTML格式,Log4J中HTMLLayout的实现中将每一条日志信息打印成表格中的一行,因而包含了一些Header和Footer信息。并且HTMLLayout类还支持配置是否打印位置信息和自定义title。最终HTMLLayout的日志打印格式如下:

HTML PUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">

${title}
Log session start time ${currentTime}
TimeThreadLevelCategoryFile:LineMessage
${timeElapsedFromStart}${theadName}#if(${level}==“DEBUG”)DEBUG#elseif(${level}>=“WARN”)${level}#else${level}levin.log4j.test.TestBasic${fileName}:${lineNumber}${renderedMessage}
NDC: ${NDC}
java.lang.Exception: Deliberatelythrowan Exception

9b8a8a44dd1c74ae49c20a7cd451974e.png
        at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:51)
        at levin.log4j.layout.LayoutTest.testHTMLLayout(LayoutTest.java:34)

9b8a8a44dd1c74ae49c20a7cd451974e.png

以上所有HTML内容信息都要经过转义,即: ’ < ‘>’ => > ‘&’ => & ‘”’ => "从上信息可以看到HTMLLayout支持异常处理,并且它也实现了getContentType()方法:

1 publicString getContentType() {2 return"text/html";3 }4 publicbooleanignoresThrowable() {5 returnfalse;6 }

测试用例:

1 @Test2 publicvoidtestHTMLLayout() {3 HTMLLayout layout=newHTMLLayout();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"?>log4j:eventSet PUBLIC"-//APACHE//DTD LOG4J 1.2//EN""log4j.dtd"[]>&data;

XMLLayout还支持设置是否支持打印位置信息以及MDC(Mapped Diagnostic Context)信息,他们的默认值都为false:

1 privatebooleanlocationInfo=false;2 privatebooleanproperties=false;

XMLLayout的输出格式如下:

#if${ndc}!=null#endif

#if${throwableInfo}!=null

9b8a8a44dd1c74ae49c20a7cd451974e.png

at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:54)

at levin.log4j.layout.LayoutTest.testXMLLayout(LayoutTest.java:43)

9b8a8a44dd1c74ae49c20a7cd451974e.png

]]>

#endif

#if${locationInfo}!=null#endif

#if${properties}!=null#foreach ${key} in ${keyset}#end#endif

从以上日志格式也可以看出XMLLayout已经处理了异常信息。

1 publicbooleanignoresThrowable() {2 returnfalse;3 }

测试用例:

1 @Test2 publicvoidtestXMLLayout() {3 XMLLayout layout=newXMLLayout();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 publicString 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 returnbuf.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 @Test2 publicvoidtestTTCCLayout() {3 TTCCLayout layout=newTTCCLayout();4 layout.setDateFormat("ISO8601");5 configSetup(layout);6 logTest();7 }

测试结果:

2012-07-0223:07:34,017[main] INFO levin.log4j.test.TestBasic-Begin to execute testBasic() method

9b8a8a44dd1c74ae49c20a7cd451974e.png2012-07-0223:07:34,018[main] INFO levin.log4j.test.TestBasic-Executing

9b8a8a44dd1c74ae49c20a7cd451974e.png2012-07-0223:07:34,019[main] ERROR levin.log4j.test.TestBasic-Catching an Exception

java.lang.Exception: Deliberatelythrowan Exception

9b8a8a44dd1c74ae49c20a7cd451974e.png

at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:63)

9b8a8a44dd1c74ae49c20a7cd451974e.png2012-07-0223:07:34,022[main] INFO levin.log4j.test.TestBasic-Execute testBasic() method finished.

PatternLayout类

个人感觉PatternLayout是Log4J中最常用也是最复杂的Layout了。PatternLayout的设计理念是LoggingEvent实例中所有的信息是否显示、以何种格式显示都是可以自定义的,比如要用PatternLayout实现TTCCLayout中的格式,可以这样设置:

1 @Test2 publicvoidtestPatternLayout() {3 PatternLayout layout=newPatternLayout();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引入了解释器模式:

3.%E6%B7%B1%E5%85%A5Log4J%E6%BA%90%E7%A0%81%E4%B9%8BLayout_PatternParser%E7%B1%BB%E7%BB%93%E6%9E%84%E5%9B%BE.png

其中PatternParser负责解析PatternLayout中设置的conversion pattern,它将conversion pattern解析出一个链状的PatternConverter,而后在每次格式化LoggingEvent实例是,只需要遍历该链即可:

1 publicString format(LoggingEvent event) {2 PatternConverter c=head;3 while(c!=null) {4 c.format(sbuf, event);5 c=c.next;6 }7 returnsbuf.toString();8 }

在解析conversion pattern时,PatternParser使用了有限状态机的方法:

3.%E6%B7%B1%E5%85%A5Log4J%E6%BA%90%E7%A0%81%E4%B9%8BLayout_PatternParser%E7%8A%B6%E6%80%81%E5%9B%BE.png

即PatternParser定义了五种状态,初始化时LITERAL_STATE,当遍历完成,则退出;否则,如果当前字符不是’%’,则将该字符添加到currentLiteral中,继续遍历;否则,若下一字符是’%’,则将其当做基本字符处理,若下一字符是’n’,则添加换行符,否则,将之前收集的literal字符创建LiteralPatternConverter实例,添加到相应的PatternConverter链中,清空currentLiteral实例,并添加下一字符,解析器进入CONVERTER_STATE状态:

1 caseLITERAL_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 caseESCAPE_CHAR:11 currentLiteral.append(c);12 i++;//move pointer13 break;14 case'n':15 currentLiteral.append(Layout.LINE_SEP);16 i++;//move pointer17 break;18 default:19 if(currentLiteral.length()!=0) {20 addToList(newLiteralPatternConverter(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 caseCONVERTER_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 }else15 finalizeConverter(c);16 }//switch17 break;

进入MIN_STATE状态,首先判断当期字符是否为数字,若是,则继续计算精度的最小值;若遇到’.’,则进入DOT_STATE状态;否则,根据字符解析出不同的PatternConverter,并且如果存在可选项信息(’{}’中的信息),一起提取出来,并将状态重新设置成LITERAL_STATE状态:

1 caseMIN_STATE:2 currentLiteral.append(c);3 if(c>='0'&&c<='9')4 formattingInfo.min=formattingInfo.min*10+(c-'0');5 elseif(c=='.')6 state=DOT_STATE;7 else{8 finalizeConverter(c);9 }10 break;

进入DOT_STATE状态,如果当前字符是数字,则进入MAX_STATE状态;格式出错,回到LITERAL_STATE状态:

1 caseDOT_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"+i8 +".\n Was expecting digit, instead got char \""9 +c+"\".");10 state=LITERAL_STATE;11 }12 break;

进入MAX_STATE状态,若为数字,则继续计算最大精度值,否则,根据字符解析出不同的PatternConverter,并且如果存在可选项信息(’{}’中的信息),一起提取出来,并将状态重新设置成LITERAL_STATE状态:

1 caseMAX_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 publicclassNDC {2 publicstaticString get();3 publicstaticString pop();4 publicstaticString peek();5 publicstaticvoidpush(String message);6 publicstaticvoidremove();7 }

即使用前,将和当前上下文信息push如当前线程栈,使用完后pop出来:

1 @Test2 publicvoidtestNDC() {3 PatternLayout layout=newPatternLayout();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

9b8a8a44dd1c74ae49c20a7cd451974e.png14 Levin Ding-Executing

9b8a8a44dd1c74ae49c20a7cd451974e.png15 Levin Ding-Catching an Exception16 java.lang.Exception: Deliberatelythrowan Exception

9b8a8a44dd1c74ae49c20a7cd451974e.png17 at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:86)18 9b8a8a44dd1c74ae49c20a7cd451974e.png19 Levin Ding-Execute testBasic() method finished.

NDC所有的操作都是针对当前线程的,因而不会影响其他线程。而在NDC实现中,使用一个Hashtable,其Key是线程实例,这样的实现导致用户需要手动的调用remove方法,移除那些push进去的数据以及移除那些已经过期的线程数据,不然就会出现内存泄露的情况;另外,如果使用线程池,在没有及时调用remove方法的情况下,容易前一线程的数据影响后一线程的结果。很奇怪为什么这里没有ThreadLocal或者是WeakReference,这样就可以部分的解决忘记调用remove引起的后果,貌似是出于兼容性的考虑?

MDC使用了TheadLocal,因而它只能使用在JDK版本大于1.2的环境中,然而其代码实现和接口也更加简洁:

1 publicclassMDC {2 publicstaticvoidput(String key, Object o);3 publicstaticObject get(String key);4 publicstaticvoidremove(String key);5 publicstaticvoidclear();6 }

类似NDC,MDC在使用前也需要向其添加数据,结束后将其remove,但是remove操作不是必须的,因为它使用了TheadLocal,因而不会引起内存问题;不过它还是可能在使用线程池的情况下引起问题,除非线程池在每一次线程运行结束后或每一次线程运行前将ThreadLocal的数据清除:

1 @Test2 publicvoidtestMDC() {3 PatternLayout layout=newPatternLayout();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();10 MDC.remove("ip");11 MDC.remove("name");12 }13 IP:127.0.0.1Name:levin-Begin to execute testBasic() method

9b8a8a44dd1c74ae49c20a7cd451974e.png14 IP:127.0.0.1Name:levin-Executing

9b8a8a44dd1c74ae49c20a7cd451974e.png15 IP:127.0.0.1Name:levin-Catching an Exception16 java.lang.Exception: Deliberatelythrowan Exception

9b8a8a44dd1c74ae49c20a7cd451974e.png17 at levin.log4j.layout.LayoutTest.logTest(LayoutTest.java:100)18 9b8a8a44dd1c74ae49c20a7cd451974e.png19 IP:127.0.0.1Name:levin-Execute testBasic() method finished.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值