天天看點

深入Log4J源碼之Appender

appender負責定義日志輸出的目的地,它可以是控制台(consoleappender)、檔案(fileappender)、jms伺服器(jmslogappender)、以email的形式發送出去(smtpappender)等。appender是一個命名的實體,另外它還包含了對layout、errorhandler、filter等引用:

 1 public interface appender {

 2     void addfilter(filter newfilter);

 3     public filter getfilter();

 4     public void clearfilters();

 5     public void close();

 6     public void doappend(loggingevent event);

 7     public string getname();

 8     public void seterrorhandler(errorhandler errorhandler);

 9     public errorhandler geterrorhandler();

10     public void setlayout(layout layout);

11     public layout getlayout();

12     public void setname(string name);

13     public boolean requireslayout();

14 }

簡單的,在配置檔案中,appender會注冊到logger中,logger在寫日志時,通過繼承機制周遊所有注冊到它本身和其父節點的appender(在additivity為true的情況下),調用doappend()方法,實作日志的寫入。在doappend方法中,若目前appender注冊了filter,則doappend還會判斷目前日志時候通過了filter的過濾,通過了filter的過濾後,如果目前appender繼承自skeletonappender,還會檢查目前日志級别時候要比目前appender本身的日志級别閥門要打,所有這些都通過後,才會将loggingevent執行個體傳遞給layout執行個體以格式化成一行日志資訊,最後寫入相應的目的地,在這些操作中,任何出現的錯誤都由errorhandler字段來處理。

log4j中的appender類圖結構:

深入Log4J源碼之Appender

在log4j core一小節中已經簡單的介紹過了appenderskeleton、writerappender、consoleappender以及 filter,因小節将直接介紹具體的幾個常用的appender。

fileappender繼承自writerappender,它将日志寫入檔案。主要的日志寫入邏輯已經在writerappender中處理,fileappender主要處理的邏輯主要在于将設定日志輸出檔案名,并通過設定的檔案建構writerappender中的quitewriter字段執行個體。如果log檔案的目錄沒有建立,在setfile()方法中會先建立目錄,再設定日志檔案。另外,所有fileappender字段在調用activateoptions()方法中生效。

 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繼承自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繼承自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), 

深入Log4J源碼之Appender

, 2, 1} to {maxbackupindex, 

深入Log4J源碼之Appender

, 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顧名思義,就是異步的調用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預設使用緩存機制,當然你也可以設定緩存大小為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);

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;

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類将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類将日志消息發送到指定的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();

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());

31         }

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;

 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         }

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();

38         

深入Log4J源碼之Appender

39     }

40     

深入Log4J源碼之Appender

41 }

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();

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);

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将日志消息(loggingevent序列化執行個體)發送到指定host的port端口。在建立socketappender時,socketappender會根據設定的host和端口建立和遠端伺服器的連結,并建立objectoutputstream執行個體。

 1 void connect(inetaddress address, int port) {

 2     if (this.address == null)

 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);

如果建立失敗,調用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線程以隔時檢測遠端日志伺服器可以重新連結。

 2     if (event == null)

 4     if (address == null) {

 5         errorhandler

 6                 .error("no remote host is set for socketappender named \""

 7                         + this.name + "\".");

 8         return;

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();

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     }

log4j為日志伺服器的實作提供了socketnode類,它接收用戶端的連結,并根據配置列印到相關的appender中。

 1 public class socketnode implements runnable {

 2     socket socket;

 3     loggerrepository hierarchy;

 4     objectinputstream ois;

 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 (

深入Log4J源碼之Appender

) {

13             

深入Log4J源碼之Appender

14         }

17     public void run() {

18         loggingevent event;

19         logger remotelogger;

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                     }

31         } catch (

深入Log4J源碼之Appender

32             

深入Log4J源碼之Appender

33         } finally {

34             if (ois != null) {

35                 try {

36                     ois.close();

37                 } catch (exception e) {

38                     logger.info("could not close connection.", e);

39                 }

41             if (socket != null) {

42                 try {

43                     socket.close();

44                 } catch (interruptedioexception e) {

45                     thread.currentthread().interrupt();

46                 } catch (ioexception ex) {

47                 }

事實上,log4j提供了兩個日志伺服器的實作類:simplesocketserver和socketserver。他們都會接收用戶端的連接配接,為每個用戶端連結建立一個socketnode執行個體,并根據指定的配置檔案列印日志消息。它們的不同在于simplesocketserver同時支援xml和properties配置檔案,而socketserver隻支援properties配置檔案;另外,socketserver支援不同用戶端使用不同的配置檔案(以用戶端主機名作為選擇配置檔案的方式),而simplesocketserver不支援。

最後,使用socketappender時,在應用程式退出時,最好顯示的調用loggermanager.shutdown()方法,不然如果是通過垃圾回收器來隐式的關閉(finalize()方法)socketappender,在windows平台中可能會存在tcp管道中未傳輸的資料丢失的情況。另外,在網絡連接配接不可用時,socketappender可能會阻塞應用程式,當網絡可用,但是遠端日志伺服器不可用時,相應的日志會被丢失。如果日志傳輸給遠端日志伺服器的速度要慢于日志産生速度,此時會影響應用程式性能。這些問題在下一小節的sockethubappender中同樣存在。

可以使用一下代碼測試socketappender和socketnode:

 2 public void testsocketappender() throws exception {

 3     socketappender appender = new socketappender(

 4             inetaddress.getlocalhost(), 8000);

 5     appender.setlocationinfo(true);

 6     appender.setapplication("appendertest");

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

深入Log4J源碼之Appender

.");    

15         } else {

16             log.info("exception test

深入Log4J源碼之Appender

", new exception());

19 }

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類似socketappender,它也将日志資訊(序列化後的loggingevent執行個體)發送到指定的日志伺服器,該日志伺服器可以是socketnode支援的伺服器。不同的是,socketappender指定日志伺服器的位址和端口号,而sockethubappender并不直接指定日志伺服器的位址和端口号,它自己啟動一個指定端口的伺服器,由日志伺服器注冊到到該伺服器,進而産生一個連接配接參數,sockethubappender根據這些參數發送日志資訊到注冊的日志伺服器,因而sockethubappender支援同時發送相同的日志資訊到多個日志伺服器。

另外,sockethubappender還會緩存部分loggingevent實力,進而支援在新注冊一個日志伺服器時,它會先将那些緩存下來的loggingevent發送給新注冊伺服器,然後接受新的loggingevent日志列印請求。

具體實作以及注意事項參考socketappender。最好補充一點,sockethubappender可以和chainsaw一起使用,它好像使用了zeroconf協定,和sockethubappender以及socketappender中的zeroconfsupport類相關,不怎麼了解這個協定,也沒有時間細看了。

将日志顯示在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 }

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中可以通過控制台->管理工具->檢視事件日志中檢視相應的日志資訊。

nullappender作為一個null object模式存在,不會輸出任何列印日志消息。