Layout抽象类
Layout类是全部Log4J中Layout的基类,它是一个抽象类,定义了Layout的接口。apache
1. format()方法:将LoggingEvent类中的信息格式化成一行日志。数组
2. getContentType():定义日志文件的内容类型,目前在Log4J中只是在SMTPAppender中用到,用于设置发送邮件的邮件内容类型。而Layout自己也只有HTMLLayout实现了它。性能优化
3. getHeader():定义日志文件的头,目前在Log4J中只是在HTMLLayout中实现了它。session
4. getFooter():定义日志文件的尾,目前在Log4J中只是HTMLLayout中实现了它。多线程
5. ignoresThrowable():定义当前layout是否处理异常类型。在Log4J中,不支持处理异常类型的有:TTCLayout、PatternLayout、SimpleLayout。app
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
>&
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内容信息都要通过转义,即: ’<’ => < ‘>’ => > ‘&’ => & ‘”’ => "从上信息能够看到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
><!
[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
>
#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
}