本文轉自上善若水的博客,原文出處:http://www.blogjava.net/DLevin/archive/2012/07/10/382676.html。感謝做者的無私的分享。html
簡單的,在配置文件中,Appender會註冊到Logger中,Logger在寫日誌時,經過繼承機制遍歷全部註冊到它自己和其父節點的Appender(在additivity爲true的狀況下),調用doAppend()方法,實現日誌的寫入。在doAppend方法中,若當前Appender註冊了Filter,則doAppend還會判斷當前日誌時候經過了Filter的過濾,經過了Filter的過濾後,若是當前Appender繼承自SkeletonAppender,還會檢查當前日誌級別時候要比當前Appender自己的日誌級別閥門要打,全部這些都經過後,纔會將LoggingEvent實例傳遞給Layout實例以格式化成一行日誌信息,最後寫入相應的目的地,在這些操做中,任何出現的錯誤都由ErrorHandler字段來處理。sql
Log4J中的Appender類圖結構:
數據庫
在Log4J Core一小節中已經簡單的介紹過了AppenderSkeleton、WriterAppender、ConsoleAppender以及 Filter,因小節將直接介紹具體的幾個經常使用的Appender。apache
FileAppender類
FileAppender繼承自WriterAppender,它將日誌寫入文件。主要的日誌寫入邏輯已經在WriterAppender中處理,FileAppender主要處理的邏輯主要在於將設置日誌輸出文件名,並經過設置的文件構建WriterAppender中的QuiteWriter字段實例。若是Log文件的目錄沒有建立,在setFile()方法中會先建立目錄,再設置日誌文件。另外,全部FileAppender字段在調用activateOptions()方法中生效。
windows
1
protected
boolean
fileAppend
=
true
;
2
protected
String fileName
=
null
;
3
protected
boolean
bufferedIO
=
false
;
4
protected
int
bufferSize
=
8
*
1024
;
5
6
public
void
activateOptions() {
7
if
(fileName
!=
null
) {
8
try
{
9
setFile(fileName, fileAppend, bufferedIO, bufferSize);
10
}
catch
(java.io.IOException e) {
11
errorHandler.error(
"
setFile(
"
+
fileName
+
"
,
"
+
fileAppend
12
+
"
) call failed.
"
, e, ErrorCode.FILE_OPEN_FAILURE);
13
}
14
}
else
{
15
LogLog.warn(
"
File option not set for appender [
"
+
name
+
"
].
"
);
16
LogLog.warn(
"
Are you using FileAppender instead of ConsoleAppender?
"
);
17
}
18
}
19
20
public
synchronized
void
setFile(String fileName,
boolean
append,
21
boolean
bufferedIO,
int
bufferSize)
throws
IOException {
22
LogLog.debug(
"
setFile called:
"
+
fileName
+
"
,
"
+
append);
23
if
(bufferedIO) {
24
setImmediateFlush(
false
);
25
}
26
reset();
27
FileOutputStream ostream
=
null
;
28
try
{
29
ostream
=
new
FileOutputStream(fileName, append);
30
}
catch
(FileNotFoundException ex) {
31
String parentName
=
new
File(fileName).getParent();
32
if
(parentName
!=
null
) {
33
File parentDir
=
new
File(parentName);
34
if
(
!
parentDir.exists()
&&
parentDir.mkdirs()) {
35
ostream
=
new
FileOutputStream(fileName, append);
36
}
else
{
37
throw
ex;
38
}
39
}
else
{
40
throw
ex;
41
}
42
}
43
Writer fw
=
createWriter(ostream);
44
if
(bufferedIO) {
45
fw
=
new
BufferedWriter(fw, bufferSize);
46
}
47
this
.setQWForFiles(fw);
48
this
.fileName
=
fileName;
49
this
.fileAppend
=
append;
50
this
.bufferedIO
=
bufferedIO;
51
this
.bufferSize
=
bufferSize;
52
writeHeader();
53
LogLog.debug(
"
setFile ended
"
);
54
}
DailyRollingFileAppender類
DailyRollingFileAppender繼承自FileAppender,不過這個名字感受有點不靠譜,事實上,DailyRollingFileAppender會在每隔一段時間能夠生成一個新的日誌文件,不過這個時間間隔是能夠設置的,不只僅只是每隔一天。時間間隔經過setDatePattern()方法設置,datePattern必須遵循SimpleDateFormat中的格式。支持的時間間隔有:數組
1. 天天:’.’YYYY-MM-dd(默認)緩存
2. 每星期:’.’YYYY-ww安全
3. 每個月:’.’YYYY-MM服務器
4. 每隔半天:’.’YYYY-MM-dd-a
5. 每小時:’.’YYYY-MM-dd-HH
6. 每分鐘:’.’YYYY-MM-dd-HH-mm
DailyRollingFileAppender須要設置的兩個屬性:datePattern和fileName。其中datePattern用於肯定時間間隔以及當日志文件過了一個時間間隔後用於重命名以前的日誌文件;fileName用於設置日誌文件的初始名字。在實現過程當中,datePattern用於實例化SimpleDateFormat,記錄當前時間以及計算下一個時間間隔時間。在每次寫日誌操做以前先判斷當前時間是否已經操做計算出的下一間隔時間,如果,則將以前的日誌文件重命名(向日志文件名尾添加datePattern指定的時間信息),並創新的日誌文件,同時從新設置當前時間以及下一次的時間間隔。
1
public
void
activateOptions() {
2
super
.activateOptions();
3
if
(datePattern
!=
null
&&
fileName
!=
null
) {
4
now.setTime(System.currentTimeMillis());
5
sdf
=
new
SimpleDateFormat(datePattern);
6
int
type
=
computeCheckPeriod();
7
printPeriodicity(type);
8
rc.setType(type);
9
File file
=
new
File(fileName);
10
scheduledFilename
=
fileName
11
+
sdf.format(
new
Date(file.lastModified()));
12
13
}
else
{
14
LogLog.error(
"
Either File or DatePattern options are not set for appender [
"
15
+
name
+
"
].
"
);
16
}
17
}
18
void
rollOver()
throws
IOException {
19
if
(datePattern
==
null
) {
20
errorHandler.error(
"
Missing DatePattern option in rollOver().
"
);
21
return
;
22
}
23
24
String datedFilename
=
fileName
+
sdf.format(now);
25
if
(scheduledFilename.equals(datedFilename)) {
26
return
;
27
}
28
this
.closeFile();
29
File target
=
new
File(scheduledFilename);
30
if
(target.exists()) {
31
target.delete();
32
}
33
File file
=
new
File(fileName);
34
boolean
result
=
file.renameTo(target);
35
if
(result) {
36
LogLog.debug(fileName
+
"
->
"
+
scheduledFilename);
37
}
else
{
38
LogLog.error(
"
Failed to rename [
"
+
fileName
+
"
] to [
"
39
+
scheduledFilename
+
"
].
"
);
40
}
41
try
{
42
this
.setFile(fileName,
true
,
this
.bufferedIO,
this
.bufferSize);
43
}
catch
(IOException e) {
44
errorHandler.error(
"
setFile(
"
+
fileName
+
"
, true) call failed.
"
);
45
}
46
scheduledFilename
=
datedFilename;
47
}
48
protected
void
subAppend(LoggingEvent event) {
49
long
n
=
System.currentTimeMillis();
50
if
(n
>=
nextCheck) {
51
now.setTime(n);
52
nextCheck
=
rc.getNextCheckMillis(now);
53
try
{
54
rollOver();
55
}
catch
(IOException ioe) {
56
if
(ioe
instanceof
InterruptedIOException) {
57
Thread.currentThread().interrupt();
58
}
59
LogLog.error(
"
rollOver() failed.
"
, ioe);
60
}
61
}
62
super
.subAppend(event);
63
}
按Log4J文檔,DailyRollingFileAppender存在線程同步問題。不過本人木有找到哪裏出問題了,望高人指點。
RollingFileAppender類
RollingFileAppender繼承自FileAppender,不一樣於DailyRollingFileAppender是基於時間做爲閥值,RollingFileAppender則是基於文件大小做爲閥值。當日志文件超過指定大小,日誌文件會被重命名成」日誌文件名.1」,若此文件已經存在,則將此文件重命名成」日誌文件名.2」,一次類推。若文件數已經超過設置的可備份日誌文件最大個數,則將最舊的日誌文件刪除。若是要設置不刪除任何日誌文件,能夠將maxBackupIndex設置成Integer最大值,若是這樣,這裏rollover()方法的實現會引發一些性能問題,由於它要衝最大值開始遍歷查找已經備份的日誌文件。
1
protected
long
maxFileSize
=
10
*
1024
*
1024
;
2
protected
int
maxBackupIndex
=
1
;
3
private
long
nextRollover
=
0
;
4
5
public
void
rollOver() {
6
File target;
7
File file;
8
if
(qw
!=
null
) {
9
long
size
=
((CountingQuietWriter) qw).getCount();
10
LogLog.debug(
"
rolling over count=
"
+
size);
11
//
if operation fails, do not roll again until
12
//
maxFileSize more bytes are written
13
nextRollover
=
size
+
maxFileSize;
14
}
15
LogLog.debug(
"
maxBackupIndex=
"
+
maxBackupIndex);
16
17
boolean
renameSucceeded
=
true
;
18
//
If maxBackups <= 0, then there is no file renaming to be done.
19
if
(maxBackupIndex
>
0
) {
20
//
Delete the oldest file, to keep Windows happy.
21
file
=
new
File(fileName
+
'
.
'
+
maxBackupIndex);
22
if
(file.exists())
23
renameSucceeded
=
file.delete();
24
25
//
Map {(maxBackupIndex - 1),
, 2, 1} to {maxBackupIndex,
, 3,
26
//
2}
27
for
(
int
i
=
maxBackupIndex
-
1
; i
>=
1
&&
renameSucceeded; i
--
) {
28
file
=
new
File(fileName
+
"
.
"
+
i);
29
if
(file.exists()) {
30
target
=
new
File(fileName
+
'
.
'
+
(i
+
1
));
31
LogLog.debug(
"
Renaming file
"
+
file
+
"
to
"
+
target);
32
renameSucceeded
=
file.renameTo(target);
33
}
34
}
35
36
if
(renameSucceeded) {
37
//
Rename fileName to fileName.1
38
target
=
new
File(fileName
+
"
.
"
+
1
);
39
this
.closeFile();
//
keep windows happy.
40
file
=
new
File(fileName);
41
LogLog.debug(
"
Renaming file
"
+
file
+
"
to
"
+
target);
42
renameSucceeded
=
file.renameTo(target);
43
//
44
//
if file rename failed, reopen file with append = true
45
//
46
if
(
!
renameSucceeded) {
47
try
{
48
this
.setFile(fileName,
true
, bufferedIO, bufferSize);
49
}
catch
(IOException e) {
50
if
(e
instanceof
InterruptedIOException) {
51
Thread.currentThread().interrupt();
52
}
53
LogLog.error(
"
setFile(
"
+
fileName
54
+
"
, true) call failed.
"
, e);
55
}
56
}
57
}
58
}
59
60
//
61
//
if all renames were successful, then
62
//
63
if
(renameSucceeded) {
64
try
{
65
this
.setFile(fileName,
false
, bufferedIO, bufferSize);
66
nextRollover
=
0
;
67
}
catch
(IOException e) {
68
if
(e
instanceof
InterruptedIOException) {
69
Thread.currentThread().interrupt();
70
}
71
LogLog.error(
"
setFile(
"
+
fileName
+
"
, false) call failed.
"
, e);
72
}
73
}
74
}
75
76
public
synchronized
void
setFile(String fileName,
boolean
append,
77
boolean
bufferedIO,
int
bufferSize)
throws
IOException {
78
super
.setFile(fileName, append,
this
.bufferedIO,
this
.bufferSize);
79
if
(append) {
80
File f
=
new
File(fileName);
81
((CountingQuietWriter) qw).setCount(f.length());
82
}
83
}
84
protected
void
setQWForFiles(Writer writer) {
85
this
.qw
=
new
CountingQuietWriter(writer, errorHandler);
86
}
87
protected
void
subAppend(LoggingEvent event) {
88
super
.subAppend(event);
89
if
(fileName
!=
null
&&
qw
!=
null
) {
90
long
size
=
((CountingQuietWriter) qw).getCount();
91
if
(size
>=
maxFileSize
&&
size
>=
nextRollover) {
92
rollOver();
93
}
94
}
95
}
AsyncAppender類
AsyncAppender顧名思義,就是異步的調用Appender中的doAppend()方法。有多種方法實現這樣的功能,好比每當調用doAppend()方法時,doAppend()方法內部啓動一個線程來處理這一次調用的邏輯,這個線程能夠是新建的線程也能夠是線程池,然而咱們知道線程是一個比較耗資源的實體,爲每一次的操做都建立一個新的線程,而這個線程在這一次調用結束後就再也不使用,這種模式是很是不划算的,性能低下;並且即便在這裏使用線程池,也會致使在很是多請求同時過來時引發消耗大量的線程池中的線程或者由於線程池已滿而阻塞請求。於是這種直接使用線程去處理每一次的請求是不可取的。
另外一種經常使用的方案可使用生產者和消費中的模式來實現相似的邏輯。即每一次請求作爲一個生產者,將請求放到一個Queue中,而由另一個或多個消費者讀取Queue中的內容以處理真正的邏輯。
在最新的Java版本中,咱們可使用BlockingQueue類簡單的實現相似的需求,然而因爲Log4J的存在遠早於BlockingQueue的建立,於是爲了實現對之前版本的兼容,它仍是本身實現了這樣一套生產者消費者模型。
AsyncAppender並不會在每一次的doAppend()調用中都直接將消息輸出,而是使用了buffer,即只有等到buffer中LoggingEvent實例到達bufferSize個的時候才真正的處理這些消息,固然咱們也能夠講bufferSize設置成1,從而實現每個LoggingEvent實例的請求都會直接執行。若是bufferSize設置過大,在應用程序異常終止時可能會丟失部分日誌。
1
public
static
final
int
DEFAULT_BUFFER_SIZE
=
128
;
2
private
final
List buffer
=
new
ArrayList();
3
private
final
Map discardMap
=
new
HashMap();
4
private
int
bufferSize
=
DEFAULT_BUFFER_SIZE;
5
private
final
Thread dispatcher;
6
private
boolean
locationInfo
=
false
;
7
private
boolean
blocking
=
true
;
對其餘字段,discardMap用於存放噹噹前LoggingEvent請求數已經超過bufferSize或當前線程被中斷的狀況下能繼續保留這些日誌信息;locationInfo用於設置是否須要保留位置信息;blocking用於設置在消費者正在處理時,是否須要生產者「暫停」下來,默認爲true;而dispatcher便是消費者線程,它在構建AsyncAppender是啓動,每次監聽buffer這個list,若是發現buffer中存在LoggingEvent實例,則將全部buffer和discardMap中的LoggingEvent實例拷貝到數組中,清空buffer和discardMap,並調用AsyncAppender內部註冊的Appender實例打印日誌。
1
public
void
run() {
2
boolean
isActive
=
true
;
3
try
{
4
while
(isActive) {
5
LoggingEvent[] events
=
null
;
6
synchronized
(buffer) {
7
int
bufferSize
=
buffer.size();
8
isActive
=
!
parent.closed;
9
10
while
((bufferSize
==
0
)
&&
isActive) {
11
buffer.wait();
12
bufferSize
=
buffer.size();
13
isActive
=
!
parent.closed;
14
}
15
if
(bufferSize
>
0
) {
16
events
=
new
LoggingEvent[bufferSize
17
+
discardMap.size()];
18
buffer.toArray(events);
19
int
index
=
bufferSize;
20
21
for
(Iterator iter
=
discardMap.values().iterator(); iter
22
.hasNext();) {
23
events[index
++
]
=
((DiscardSummary) iter.next())
24
.createEvent();
25
}
26
buffer.clear();
27
discardMap.clear();
28
buffer.notifyAll();
29
}
30
}
31
if
(events
!=
null
) {
32
for
(
int
i
=
0
; i
<
events.length; i
++
) {
33
synchronized
(appenders) {
34
appenders.appendLoopOnAppenders(events[i]);
35
}
36
}
37
}
38
}
39
}
catch
(InterruptedException ex) {
40
Thread.currentThread().interrupt();
41
}
42
}
這裏其實有一個bug,即當程序中止時只剩下discardMap中有日誌信息,而buffer中沒有日誌信息,因爲Dispatcher線程不檢查discardMap中的日誌信息,於是此時會致使discardMap中的日誌信息丟失。即便在生成者中當buffer爲空時,它也會激活buffer鎖,然而即便激活後buffer自己大小仍是爲0,於是不會處理以後的邏輯,於是這個邏輯也處理不了該bug。
對於生產者,它首先處理當消費者線程出現異常而不活動時,此時將同步的輸出日誌;然後根據配置獲取LoggingEvent中的數據;再得到buffer的對象鎖,若是buffer還沒滿,則直接將LoggingEvent實例添加到buffer中,不然若是blocking設置爲true,即生產者會等消費者處理完後再繼續下一次接收數據。若是blocking設置爲fasle或者消費者線程被打斷,那麼當前的LoggingEvent實例則會保存在discardMap中,由於此時buffer已滿。
1
public
void
append(
final
LoggingEvent event) {
2
if
((dispatcher
==
null
)
||
!
dispatcher.isAlive()
||
(bufferSize
<=
0
)) {
3
synchronized
(appenders) {
4
appenders.appendLoopOnAppenders(event);
5
}
6
return
;
7
}
8
event.getNDC();
9
event.getThreadName();
10
event.getMDCCopy();
11
if
(locationInfo) {
12
event.getLocationInformation();
13
}
14
event.getRenderedMessage();
15
event.getThrowableStrRep();
16
synchronized
(buffer) {
17
while
(
true
) {
18
int
previousSize
=
buffer.size();
19
if
(previousSize
<
bufferSize) {
20
buffer.add(event);
21
if
(previousSize
==
0
) {
22
buffer.notifyAll();
23
}
24
break
;
25
}
26
boolean
discard
=
true
;
27
if
(blocking
&&
!
Thread.interrupted()
28
&&
Thread.currentThread()
!=
dispatcher) {
29
try
{
30
buffer.wait();
31
discard
=
false
;
32
}
catch
(InterruptedException e) {
33
Thread.currentThread().interrupt();
34
}
35
}
36
if
(discard) {
37
String loggerName
=
event.getLoggerName();
38
DiscardSummary summary
=
(DiscardSummary) discardMap
39
.get(loggerName);
40
41
if
(summary
==
null
) {
42
summary
=
new
DiscardSummary(event);
43
discardMap.put(loggerName, summary);
44
}
else
{
45
summary.add(event);
46
}
47
break
;
48
}
49
}
50
}
51
}
最後,AsyncAppender是Appender的一個容器,它實現了AppenderAttachable接口,改接口的實現主要將實現邏輯代理給AppenderAttachableImpl類。
測試代碼以下:
1
@Test
2
public
void
testAsyncAppender()
throws
Exception {
3
AsyncAppender appender
=
new
AsyncAppender();
4
appender.addAppender(
new
ConsoleAppender(
new
TTCCLayout()));
5
appender.setBufferSize(
1
);
6
appender.setLocationInfo(
true
);
7
appender.activateOptions();
8
configAppender(appender);
9
10
logTest();
11
}
JDBCAppender類
JDBCAppender將日誌保存到數據庫的表中,因爲數據庫保存操做是一個比較費時的操做,於是JDBCAppender默認使用緩存機制,固然你也能夠設置緩存大小爲1實現實時向數據庫插入日誌。JDBCAppender中的Layout默認只支持PatternLayout,用戶能夠經過設置本身的PatternLayout,其中ConversionPattern設置成插入數據庫的SQL語句或經過setSql()方法設置SQL語句,JDBCAppender內部會建立相應的PatternLayout,如能夠設置SQL語句爲:
insert into LogTable(Thread, Class, Message) values(「%t」, 「%c」, 「%m」)
在doAppend()方法中,JDBCAppender經過layout獲取SQL語句,將LoggingEvent實例插入到數據庫中。
1
protected
String databaseURL
=
"
jdbc:odbc:myDB
"
;
2
protected
String databaseUser
=
"
me
"
;
3
protected
String databasePassword
=
"
mypassword
"
;
4
protected
Connection connection
=
null
;
5
protected
String sqlStatement
=
""
;
6
protected
int
bufferSize
=
1
;
7
protected
ArrayList buffer;
8
protected
ArrayList removes;
9
private
boolean
locationInfo
=
false
;
10
11
public
void
append(LoggingEvent event) {
12
event.getNDC();
13
event.getThreadName();
14
event.getMDCCopy();
15
if
(locationInfo) {
16
event.getLocationInformation();
17
}
18
event.getRenderedMessage();
19
event.getThrowableStrRep();
20
buffer.add(event);
21
if
(buffer.size()
>=
bufferSize)
22
flushBuffer();
23
}
24
public
void
flushBuffer() {
25
removes.ensureCapacity(buffer.size());
26
for
(Iterator i
=
buffer.iterator(); i.hasNext();) {
27
try
{
28
LoggingEvent logEvent
=
(LoggingEvent) i.next();
29
String sql
=
getLogStatement(logEvent);
30
execute(sql);
31
removes.add(logEvent);
32
}
catch
(SQLException e) {
33
errorHandler.error(
"
Failed to excute sql
"
, e,
34
ErrorCode.FLUSH_FAILURE);
35
}
36
}
37
buffer.removeAll(removes);
38
removes.clear();
39
}
40
protected
String getLogStatement(LoggingEvent event) {
41
return
getLayout().format(event);
42
}
43
protected
void
execute(String sql)
throws
SQLException {
44
Connection con
=
null
;
45
Statement stmt
=
null
;
46
try
{
47
con
=
getConnection();
48
stmt
=
con.createStatement();
49
stmt.executeUpdate(sql);
50
}
catch
(SQLException e) {
51
if
(stmt
!=
null
)
52
stmt.close();
53
throw
e;
54
}
55
stmt.close();
56
closeConnection(con);
57
}
58
protected
Connection getConnection()
throws
SQLException {
59
if
(
!
DriverManager.getDrivers().hasMoreElements())
60
setDriver(
"
sun.jdbc.odbc.JdbcOdbcDriver
"
);
61
if
(connection
==
null
) {
62
connection
=
DriverManager.getConnection(databaseURL, databaseUser,
63
databasePassword);
64
}
65
return
connection;
66
}
67
protected
void
closeConnection(Connection con) {
68
}
用戶能夠編寫本身的JDBCAppender,繼承自JDBCAppender,重寫getConnection()和closeConnection(),能夠實現從數據庫鏈接池中獲取connection,在每次將JDBCAppender緩存中的LoggingEvent列表插入數據庫時從鏈接池中獲取緩存,而在該操做完成後將得到的鏈接釋放回鏈接池。用戶也能夠重寫getLogstatement()以自定義插入LoggingEvent的SQL語句。
JMSAppender類
JMSAppender類將LoggingEvent實例序列化成ObjectMessage,並將其發送到JMS Server的一個指定Topic中。它的實現比較簡單,設置相應的connectionFactoryName、topicName、providerURL、userName、password等JMS相應的信息,在activateOptions()方法中建立相應的JMS連接,在doAppend()方法中將LoggingEvent序列化成ObjectMessage發送到JMS Server中,它也能夠經過locationInfo字段是否須要計算位置信息。不過這裏的實現感受有一些bug:在序列化LoggingEvent實例以前沒有先緩存必要的信息,如threadName,由於這些信息默認是不設置的,具體能夠參考JDBCAppender、AsyncAppender等。
1
String securityPrincipalName;
2
String securityCredentials;
3
String initialContextFactoryName;
4
String urlPkgPrefixes;
5
String providerURL;
6
String topicBindingName;
7
String tcfBindingName;
8
String userName;
9
String password;
10
boolean
locationInfo;
11
12
TopicConnection topicConnection;
13
TopicSession topicSession;
14
TopicPublisher topicPublisher;
15
16
public
void
activateOptions() {
17
TopicConnectionFactory topicConnectionFactory;
18
try
{
19
Context jndi;
20
LogLog.debug(
"
Getting initial context.
"
);
21
if
(initialContextFactoryName
!=
null
) {
22
Properties env
=
new
Properties();
23
env.put(Context.INITIAL_CONTEXT_FACTORY,
24
initialContextFactoryName);
25
if
(providerURL
!=
null
) {
26
env.put(Context.PROVIDER_URL, providerURL);
27
}
else
{
28
LogLog.warn(
"
You have set InitialContextFactoryName option but not the
"
29
+
"
ProviderURL. This is likely to cause problems.
"
);
30
}
31
if
(urlPkgPrefixes
!=
null
) {
32
env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes);
33
}
34
if
(securityPrincipalName
!=
null
) {
35
env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName);
36
if
(securityCredentials
!=
null
) {
37
env.put(Context.SECURITY_CREDENTIALS,
38
securityCredentials);
39
}
else
{
40
LogLog.warn(
"
You have set SecurityPrincipalName option but not the
"
41
+
"
SecurityCredentials. This is likely to cause problems.
"
);
42
}
43
}
44
jndi
=
new
InitialContext(env);
45
}
else
{
46
jndi
=
new
InitialContext();
47
}
48
LogLog.debug(
"
Looking up [
"
+
tcfBindingName
+
"
]
"
);
49
topicConnectionFactory
=
(TopicConnectionFactory) lookup(jndi,
50
tcfBindingName);
51
LogLog.debug(
"
About to create TopicConnection.
"
);
52
if
(userName
!=
null
) {
53
topicConnection
=
topicConnectionFactory.createTopicConnection(
54
userName, password);
55
}
else
{
56
topicConnection
=
topicConnectionFactory
57
.createTopicConnection();
58
}
59
LogLog.debug(
"
Creating TopicSession, non-transactional,
"
60
+
"
in AUTO_ACKNOWLEDGE mode.
"
);
61
topicSession
=
topicConnection.createTopicSession(
false
,
62
Session.AUTO_ACKNOWLEDGE);
63
LogLog.debug(
"
Looking up topic name [
"
+
topicBindingName
+
"
].
"
);
64
Topic topic
=
(Topic) lookup(jndi, topicBindingName);
65
LogLog.debug(
"
Creating TopicPublisher.
"
);
66
topicPublisher
=
topicSession.createPublisher(topic);
67
LogLog.debug(
"
Starting TopicConnection.
"
);
68
topicConnection.start();
69
jndi.close();
70
}
catch
(JMSException e) {
71
errorHandler.error(
72
"
Error while activating options for appender named [
"
73
+
name
+
"
].
"
, e, ErrorCode.GENERIC_FAILURE);
74
}
catch
(NamingException e) {
75
errorHandler.error(
76
"
Error while activating options for appender named [
"
77
+
name
+
"
].
"
, e, ErrorCode.GENERIC_FAILURE);
78
}
catch
(RuntimeException e) {
79
errorHandler.error(
80
"
Error while activating options for appender named [
"
81
+
name
+
"
].
"
, e, ErrorCode.GENERIC_FAILURE);
82
}
83
}
84
85
public
void
append(LoggingEvent event) {
86
if
(
!
checkEntryConditions()) {
87
return
;
88
}
89
try
{
90
ObjectMessage msg
=
topicSession.createObjectMessage();
91
if
(locationInfo) {
92
event.getLocationInformation();
93
}
94
msg.setObject(event);
95
topicPublisher.publish(msg);
96
}
catch
(JMSException e) {
97
errorHandler.error(
"
Could not publish message in JMSAppender [
"
98
+
name
+
"
].
"
, e, ErrorCode.GENERIC_FAILURE);
99
}
catch
(RuntimeException e) {
100
errorHandler.error(
"
Could not publish message in JMSAppender [
"
101
+
name
+
"
].
"
, e, ErrorCode.GENERIC_FAILURE);
102
}
103
}
TelnetAppender類
TelnetAppender類將日誌消息發送到指定的Socket端口(默認爲23),用戶可使用telnet鏈接以獲取日誌信息。這裏的實現貌似沒有考慮到telnet客戶端如何退出的問題。另外,在windows中可能默認沒有telnet支持,此時只須要到」控制面板」->」程序和功能」->」打開或關閉windows功能」中大概Telnet服務便可。TelnetAppender使用內部類SocketHandler封裝發送日誌消息到客戶端,若是沒有Telnet客戶端鏈接,則日誌消息將會直接被拋棄。
1
private
SocketHandler sh;
2
private
int
port
=
23
;
3
4
public
void
activateOptions() {
5
try
{
6
sh
=
new
SocketHandler(port);
7
sh.start();
8
}
catch
(InterruptedIOException e) {
9
Thread.currentThread().interrupt();
10
e.printStackTrace();
11
}
catch
(IOException e) {
12
e.printStackTrace();
13
}
catch
(RuntimeException e) {
14
e.printStackTrace();
15
}
16
super
.activateOptions();
17
}
18
protected
void
append(LoggingEvent event) {
19
if
(sh
!=
null
) {
20
sh.send(layout.format(event));
21
if
(layout.ignoresThrowable()) {
22
String[] s
=
event.getThrowableStrRep();
23
if
(s
!=
null
) {
24
StringBuffer buf
=
new
StringBuffer();
25
for
(
int
i
=
0
; i
<
s.length; i
++
) {
26
buf.append(s[i]);
27
buf.append(
"
\r\n
"
);
28
}
29
sh.send(buf.toString());
30
}
31
}
32
}
33
}
在SocketHandler中,建立一個新的線程以監聽指定的端口,若是有Telnet客戶端鏈接過來,則將其加入到connections集合中。這樣在send()方法中就能夠遍歷connections集合,並將日誌信息發送到每一個鏈接的Telnet客戶端。
1
private
Vector writers
=
new
Vector();
2
private
Vector connections
=
new
Vector();
3
private
ServerSocket serverSocket;
4
private
int
MAX_CONNECTIONS
=
20
;
5
6
public
synchronized
void
send(
final
String message) {
7
Iterator ce
=
connections.iterator();
8
for
(Iterator e
=
writers.iterator(); e.hasNext();) {
9
ce.next();
10
PrintWriter writer
=
(PrintWriter) e.next();
11
writer.print(message);
12
if
(writer.checkError()) {
13
ce.remove();
14
e.remove();
15
}
16
}
17
}
18
public
void
run() {
19
while
(
!
serverSocket.isClosed()) {
20
try
{
21
Socket newClient
=
serverSocket.accept();
22
PrintWriter pw
=
new
PrintWriter(
23
newClient.getOutputStream());
24
if
(connections.size()
<
MAX_CONNECTIONS) {
25
synchronized
(
this
) {
26
connections.addElement(newClient);
27
writers.addElement(pw);
28
pw.print(
"
TelnetAppender v1.0 (
"
29
+
connections.size()
30
+
"
active connections)\r\n\r\n
"
);
31
pw.flush();
32
}
33
}
else
{
34
pw.print(
"
Too many connections.\r\n
"
);
35
pw.flush();
36
newClient.close();
37
}
38

39
}
40

41
}
SMTPAppender類
SMTPAppender將日誌消息以郵件的形式發送出來,默認實現,它會先緩存日誌信息,只有當遇到日誌級別是ERROR或ERROR以上的日誌消息時才經過郵件的形式發送出來,若是在遇到觸發發送的日誌發生以前緩存中的日誌信息已滿,則最先的日誌信息會被覆蓋。用戶能夠經過setEvaluatorClass()方法改變觸發發送日誌的條件。
1
public
void
append(LoggingEvent event) {
2
if
(
!
checkEntryConditions()) {
3
return
;
4
}
5
event.getThreadName();
6
event.getNDC();
7
event.getMDCCopy();
8
if
(locationInfo) {
9
event.getLocationInformation();
10
}
11
event.getRenderedMessage();
12
event.getThrowableStrRep();
13
cb.add(event);
14
if
(evaluator.isTriggeringEvent(event)) {
15
sendBuffer();
16
}
17
}
18
protected
void
sendBuffer() {
19
try
{
20
String s
=
formatBody();
21
boolean
allAscii
=
true
;
22
for
(
int
i
=
0
; i
<
s.length()
&&
allAscii; i
++
) {
23
allAscii
=
s.charAt(i)
<=
0x7F
;
24
}
25
MimeBodyPart part;
26
if
(allAscii) {
27
part
=
new
MimeBodyPart();
28
part.setContent(s, layout.getContentType());
29
}
else
{
30
try
{
31
ByteArrayOutputStream os
=
new
ByteArrayOutputStream();
32
Writer writer
=
new
OutputStreamWriter(MimeUtility.encode(
33
os,
"
quoted-printable
"
),
"
UTF-8
"
);
34
writer.write(s);
35
writer.close();
36
InternetHeaders headers
=
new
InternetHeaders();
37
headers.setHeader(
"
Content-Type
"
, layout.getContentType()
38
+
"
; charset=UTF-8
"
);
39
headers.setHeader(
"
Content-Transfer-Encoding
"
,
40
"
quoted-printable
"
);
41
part
=
new
MimeBodyPart(headers, os.toByteArray());
42
}
catch
(Exception ex) {
43
StringBuffer sbuf
=
new
StringBuffer(s);
44
for
(
int
i
=
0
; i
<
sbuf.length(); i
++
) {
45
if
(sbuf.charAt(i)
>=
0x80
) {
46
sbuf.setCharAt(i,
'
?
'
);
47
}
48
}
49
part
=
new
MimeBodyPart();
50
part.setContent(sbuf.toString(), layout.getContentType());
51
}
52
}
53
54
Multipart mp
=
new
MimeMultipart();
55
mp.addBodyPart(part);
56
msg.setContent(mp);
57
58
msg.setSentDate(
new
Date());
59
Transport.send(msg);
60
}
catch
(MessagingException e) {
61
LogLog.error(
"
Error occured while sending e-mail notification.
"
, e);
62
}
catch
(RuntimeException e) {
63
LogLog.error(
"
Error occured while sending e-mail notification.
"
, e);
64
}
65
}
66
protected
String formatBody() {
67
StringBuffer sbuf
=
new
StringBuffer();
68
String t
=
layout.getHeader();
69
if
(t
!=
null
)
70
sbuf.append(t);
71
int
len
=
cb.length();
72
for
(
int
i
=
0
; i
<
len; i
++
) {
73
LoggingEvent event
=
cb.get();
74
sbuf.append(layout.format(event));
75
if
(layout.ignoresThrowable()) {
76
String[] s
=
event.getThrowableStrRep();
77
if
(s
!=
null
) {
78
for
(
int
j
=
0
; j
<
s.length; j
++
) {
79
sbuf.append(s[j]);
80
sbuf.append(Layout.LINE_SEP);
81
}
82
}
83
}
84
}
85
t
=
layout.getFooter();
86
if
(t
!=
null
) {
87
sbuf.append(t);
88
}
89
return
sbuf.toString();
90
}
SocketAppender類
SocketAppender將日誌消息(LoggingEvent序列化實例)發送到指定Host的port端口。在建立SocketAppender時,SocketAppender會根據設置的Host和端口創建和遠程服務器的連接,並建立ObjectOutputStream實例。
1
void
connect(InetAddress address,
int
port) {
2
if
(
this
.address
==
null
)
3
return
;
4
try
{
5
cleanUp();
6
oos
=
new
ObjectOutputStream(
7
new
Socket(address, port).getOutputStream());
8
}
catch
(IOException e) {
9
if
(e
instanceof
InterruptedIOException) {
10
Thread.currentThread().interrupt();
11
}
12
String msg
=
"
Could not connect to remote log4j server at [
"
13
+
address.getHostName()
+
"
].
"
;
14
if
(reconnectionDelay
>
0
) {
15
msg
+=
"
We will try again later.
"
;
16
fireConnector();
//
fire the connector thread
17
}
else
{
18
msg
+=
"
We are not retrying.
"
;
19
errorHandler.error(msg, e, ErrorCode.GENERIC_FAILURE);
20
}
21
LogLog.error(msg);
22
}
23
}
若是建立失敗,調用fireConnector()方法,建立一個Connector線程,在每間隔reconnectionDelay(默認值爲30000ms,若將其設置爲0表示在連接出問題時不建立新的線程檢測)的時間裏不斷重試連接。當連接從新創建後,Connector線程退出並將connector實例置爲null以在下一次連接出現問題時建立的Connector線程檢測。
1
實例置爲null以在下一次連接出現問題時建立的Connector線程檢測。
2
void
fireConnector() {
3
if
(connector
==
null
) {
4
LogLog.debug(
"
Starting a new connector thread.
"
);
5
connector
=
new
Connector();
6
connector.setDaemon(
true
);
7
connector.setPriority(Thread.MIN_PRIORITY);
8
connector.start();
9
}
10
}
11
12
class
Connector
extends
Thread {
13
boolean
interrupted
=
false
;
14
public
void
run() {
15
Socket socket;
16
while
(
!
interrupted) {
17
try
{
18
sleep(reconnectionDelay);
19
LogLog.debug(
"
Attempting connection to
"
20
+
address.getHostName());
21
socket
=
new
Socket(address, port);
22
synchronized
(
this
) {
23
oos
=
new
ObjectOutputStream(socket.getOutputStream());
24
connector
=
null
;
25
LogLog.debug(
"
Connection established. Exiting connector thread.
"
);
26
break
;
27
}
28
}
catch
(InterruptedException e) {
29
LogLog.debug(
"
Connector interrupted. Leaving loop.
"
);
30
return
;
31
}
catch
(java.net.ConnectException e) {
32
LogLog.debug(
"
Remote host
"
+
address.getHostName()
33
+
"
refused connection.
"
);
34
}
catch
(IOException e) {
35
if
(e
instanceof
InterruptedIOException) {
36
Thread.currentThread().interrupt();
37
}
38
LogLog.debug(
"
Could not connect to
"
39
+
address.getHostName()
+
"
. Exception is
"
+
e);
40
}
41
}
42
}
43
}
然後,在每一第二天志記錄請求時只需將LoggingEvent實例序列化到以前建立的ObjectOutputStream中便可,若該操做失敗,則會從新創建Connector線程以隔時檢測遠程日誌服務器能夠從新連接。
1
public
void
append(LoggingEvent event) {
2
if
(event
==
null
)
3
return
;
4
if
(address
==
null
) {
5
errorHandler
6
.error(
"
No remote host is set for SocketAppender named \
""
7
+
this
.name
+
"
\
"
.
"
);
8
return
;
9
}
10
if
(oos
!=
null
) {
11
try
{
12
if
(locationInfo) {
13
event.getLocationInformation();
14
}
15
if
(application
!=
null
) {
16
event.setProperty(
"
application
"
, application);
17
}
18
event.getNDC();
19
event.getThreadName();
20
event.getMDCCopy();
21
event.getRenderedMessage();
22
event.getThrowableStrRep();
23
oos.writeObject(event);
24
oos.flush();
25
if
(
++
counter
>=
RESET_FREQUENCY) {
26
counter
=
0
;
27
//
Failing to reset the object output stream every now and
28
//
then creates a serious memory leak.
29
//
System.err.println("Doing oos.reset()");
30
oos.reset();
31
}
32
}
catch
(IOException e) {
33
if
(e
instanceof
InterruptedIOException) {
34
Thread.currentThread().interrupt();
35
}
36
oos
=
null
;
37
LogLog.warn(
"
Detected problem with connection:
"
+
e);
38
if
(reconnectionDelay
>
0
) {
39
fireConnector();
40
}
else
{
41
errorHandler
42
.error(
"
Detected problem with connection, not reconnecting.
"
,
43
e, ErrorCode.GENERIC_FAILURE);
44
}
45
}
46
}
47
}
SocketNode類
Log4J爲日誌服務器的實現提供了SocketNode類,它接收客戶端的連接,並根據配置打印到相關的Appender中。
1
public
class
SocketNode
implements
Runnable {
2
Socket socket;
3
LoggerRepository hierarchy;
4
ObjectInputStream ois;
5
6
public
SocketNode(Socket socket, LoggerRepository hierarchy) {
7
this
.socket
=
socket;
8
this
.hierarchy
=
hierarchy;
9
try
{
10
ois
=
new
ObjectInputStream(
new
BufferedInputStream(
11
socket.getInputStream()));
12
}
catch
(
) {
13

14
}
15
}
16
17
public
void
run() {
18
LoggingEvent event;
19
Logger remoteLogger;
20
try
{
21
if
(ois
!=
null
) {
22
while
(
true
) {
23
event
=
(LoggingEvent) ois.readObject();
24
remoteLogger
=
hierarchy.getLogger(event.getLoggerName());
25
if
(event.getLevel().isGreaterOrEqual(
26
remoteLogger.getEffectiveLevel())) {
27
remoteLogger.callAppenders(event);
28
}
29
}
30
}
31
}
catch
(
) {
32

33
}
finally
{
34
if
(ois
!=
null
) {
35
try
{
36
ois.close();
37
}
catch
(Exception e) {
38
logger.info(
"
Could not close connection.
"
, e);
39
}
40
}
41
if
(socket
!=
null
) {
42
try
{
43
socket.close();
44
}
catch
(InterruptedIOException e) {
45
Thread.currentThread().interrupt();
46
}
catch
(IOException ex) {
47
}
48
}
49
}
50
}
51
}
事實上,Log4J提供了兩個日誌服務器的實現類:SimpleSocketServer和SocketServer。他們都會接收客戶端的鏈接,爲每一個客戶端連接建立一個SocketNode實例,並根據指定的配置文件打印日誌消息。它們的不一樣在於SimpleSocketServer同時支持xml和properties配置文件,而SocketServer只支持properties配置文件;另外,SocketServer支持不一樣客戶端使用不一樣的配置文件(以客戶端主機名做爲選擇配置文件的方式),而SimpleSocketServer不支持。
最後,使用SocketAppender時,在應用程序退出時,最好顯示的調用LoggerManager.shutdown()方法,否則若是是經過垃圾回收器來隱式的關閉(finalize()方法)SocketAppender,在Windows平臺中可能會存在TCP管道中未傳輸的數據丟失的狀況。另外,在網絡鏈接不可用時,SocketAppender可能會阻塞應用程序,當網絡可用,可是遠程日誌服務器不可用時,相應的日誌會被丟失。若是日誌傳輸給遠程日誌服務器的速度要慢於日誌產生速度,此時會影響應用程序性能。這些問題在下一小節的SocketHubAppender中一樣存在。
測試代碼
可使用一下代碼測試SocketAppender和SocketNode:
1
@Test
2
public
void
testSocketAppender()
throws
Exception {
3
SocketAppender appender
=
new
SocketAppender(
4
InetAddress.getLocalHost(),
8000
);
5
appender.setLocationInfo(
true
);
6
appender.setApplication(
"
AppenderTest
"
);
7
appender.activateOptions();
8
configAppender(appender);
9
10
Logger log
=
Logger.getLogger(
"
levin.log4j.test.TestBasic
"
);
11
for
(
int
i
=
0
;i
<
100
; i
++
) {
12
Thread.sleep(
10000
);
13
if
(i
%
2
==
0
) {
14
log.info(
"
Normal test
.
"
);
15
}
else
{
16
log.info(
"
Exception test
"
,
new
Exception());
17
}
18
}
19
}
20
21
@Test
22
public
void
testSimpleSocketServer()
throws
Exception {
23
ConsoleAppender appender
=
new
ConsoleAppender(
new
TTCCLayout());
24
appender.activateOptions();
25
configAppender(appender);
26
27
ServerSocket serverSocket
=
new
ServerSocket(
8000
);
28
while
(
true
) {
29
Socket socket
=
serverSocket.accept();
30
new
Thread(
new
SocketNode(socket,
31
LogManager.getLoggerRepository()),
32
"
SimpleSocketServer-
"
+
8000
).start();
33
}
34
}
SocketHubAppender類
SocketHubAppender相似SocketAppender,它也將日誌信息(序列化後的LoggingEvent實例)發送到指定的日誌服務器,該日誌服務器能夠是SocketNode支持的服務器。不一樣的是,SocketAppender指定日誌服務器的地址和端口號,而SocketHubAppender並不直接指定日誌服務器的地址和端口號,它本身啓動一個指定端口的服務器,由日誌服務器註冊到到該服務器,從而產生一個鏈接參數,SocketHubAppender根據這些參數發送日誌信息到註冊的日誌服務器,於是SocketHubAppender支持同時發送相同的日誌信息到多個日誌服務器。
另外,SocketHubAppender還會緩存部分LoggingEvent實力,從而支持在新註冊一個日誌服務器時,它會先將那些緩存下來的LoggingEvent發送給新註冊服務器,而後接受新的LoggingEvent日誌打印請求。
具體實現以及注意事項參考SocketAppender。最好補充一點,SocketHubAppender能夠和chainsaw一塊兒使用,它好像使用了zeroconf協議,和SocketHubAppender以及SocketAppender中的ZeroConfSupport類相關,不怎麼了解這個協議,也沒有時間細看了。
LF5Appender類
將日誌顯示在Swing窗口中。對Swing不熟,沒怎麼看代碼,不過可使用一下測試用例簡單的作一些測試,提供一些感受。
1
@Test
2
public
void
testLF5Appender()
throws
Exception {
3
LF5Appender appender
=
new
LF5Appender();
4
appender.setLayout(
new
TTCCLayout());
5
appender.activateOptions();
6
configAppender(appender);
7
8
logTest();
9
}
其餘Appender類
ExternallyRolledFileAppender類繼承自RollingFileAppender,於是它最基本的也是基於日誌文件大小來備份日誌文件。然而它同時還支持外界經過Socket發送「RollOver」給它以實如今特定狀況下手動備份日誌文件。Log4J提供Roller類來實現這樣的功能,其使用方法是:
java -cp log4j-1.2.16.jar org.apache.log4j.varia.Roller <hostname> <port>
可是因爲任何能夠和應用程序運行的服務器鏈接的代碼都能像該服務器發送「RollOver」消息,於是這種方式並不適合production環境在,在production中最好能加入一些限制信息,好比安全驗證等信息。
NTEventLogAppender將日誌打印到NT事件日誌系統中(NT event log system)。顧名思義,它只能用在Windows中,並且須要NTEventLogAppender.dll、NTEventLogAppender.amd64.dll、NTEventLogAppender.ia64.dll或NTEventLogAppender.x86.dll動態連接庫在Windows PATH路徑中。在Windows 7中能夠經過控制面板->管理工具->查看事件日誌中查看相應的日誌信息。
SyslogAppender將日誌打印到syslog中,我對syslog不怎麼了解,經過查看網上信息的結果,看起來syslog只是存在於Linux和Unix中。SyslogAppender額外的須要配置Facility和SyslogHost字段。具體能夠查看:http://www.cnblogs.com/frankliiu-java/articles/1754835.html和http://sjsky.iteye.com/blog/962870。
NullAppender做爲一個Null Object模式存在,不會輸出任何打印日誌消息。