Log4j源碼解析--Appender接口解析

本文轉自上善若水的博客,原文出處:http://www.blogjava.net/DLevin/archive/2012/07/10/382676.html。感謝做者的無私的分享。html

Appender負責定義日誌輸出的目的地,它能夠是控制檯(ConsoleAppender)、文件(FileAppender)、JMS服務器(JmsLogAppender)、以Email的形式發送出去(SMTPAppender)等。Appender是一個命名的實體,另外它還包含了對LayoutErrorHandlerFilter等引用:
java

 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(在additivitytrue的狀況下),調用doAppend()方法,實現日誌的寫入。在doAppend方法中,若當前Appender註冊了Filter,則doAppend還會判斷當前日誌時候經過了Filter的過濾,經過了Filter的過濾後,若是當前Appender繼承自SkeletonAppender,還會檢查當前日誌級別時候要比當前Appender自己的日誌級別閥門要打,全部這些都經過後,纔會將LoggingEvent實例傳遞給Layout實例以格式化成一行日誌信息,最後寫入相應的目的地,在這些操做中,任何出現的錯誤都由ErrorHandler字段來處理。sql

Log4J中的Appender類圖結構:
數據庫


Log4J Core一小節中已經簡單的介紹過了AppenderSkeletonWriterAppenderConsoleAppender以及 Filter,因小節將直接介紹具體的幾個經常使用的Appenderapache

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須要設置的兩個屬性:datePatternfileName。其中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,即只有等到bufferLoggingEvent實例到達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實例,則將全部bufferdiscardMap中的LoggingEvent實例拷貝到數組中,清空bufferdiscardMap,並調用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  }

最後,AsyncAppenderAppender的一個容器,它實現了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()以自定義插入LoggingEventSQL語句。

JMSAppender

JMSAppender類將LoggingEvent實例序列化成ObjectMessage,並將其發送到JMS Server的一個指定Topic中。它的實現比較簡單,設置相應的connectionFactoryNametopicNameproviderURLuserNamepasswordJMS相應的信息,在activateOptions()方法中建立相應的JMS連接,在doAppend()方法中將LoggingEvent序列化成ObjectMessage發送到JMS Server中,它也能夠經過locationInfo字段是否須要計算位置信息。不過這裏的實現感受有一些bug:在序列化LoggingEvent實例以前沒有先緩存必要的信息,如threadName,由於這些信息默認是不設置的,具體能夠參考JDBCAppenderAsyncAppender等。

  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將日誌消息以郵件的形式發送出來,默認實現,它會先緩存日誌信息,只有當遇到日誌級別是ERRORERROR以上的日誌消息時才經過郵件的形式發送出來,若是在遇到觸發發送的日誌發生以前緩存中的日誌信息已滿,則最先的日誌信息會被覆蓋。用戶能夠經過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序列化實例)發送到指定Hostport端口。在建立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提供了兩個日誌服務器的實現類:SimpleSocketServerSocketServer。他們都會接收客戶端的鏈接,爲每一個客戶端連接建立一個SocketNode實例,並根據指定的配置文件打印日誌消息。它們的不一樣在於SimpleSocketServer同時支持xmlproperties配置文件,而SocketServer只支持properties配置文件;另外,SocketServer支持不一樣客戶端使用不一樣的配置文件(以客戶端主機名做爲選擇配置文件的方式),而SimpleSocketServer不支持。

最後,使用SocketAppender時,在應用程序退出時,最好顯示的調用LoggerManager.shutdown()方法,否則若是是經過垃圾回收器來隱式的關閉(finalize()方法)SocketAppender,在Windows平臺中可能會存在TCP管道中未傳輸的數據丟失的狀況。另外,在網絡鏈接不可用時,SocketAppender可能會阻塞應用程序,當網絡可用,可是遠程日誌服務器不可用時,相應的日誌會被丟失。若是日誌傳輸給遠程日誌服務器的速度要慢於日誌產生速度,此時會影響應用程序性能。這些問題在下一小節的SocketHubAppender中一樣存在。

測試代碼

可使用一下代碼測試SocketAppenderSocketNode

 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.dllNTEventLogAppender.amd64.dllNTEventLogAppender.ia64.dllNTEventLogAppender.x86.dll動態連接庫在Windows PATH路徑中。在Windows 7中能夠經過控制面板->管理工具->查看事件日誌中查看相應的日誌信息。

SyslogAppender將日誌打印到syslog中,我對syslog不怎麼了解,經過查看網上信息的結果,看起來syslog只是存在於LinuxUnix中。SyslogAppender額外的須要配置FacilitySyslogHost字段。具體能夠查看:http://www.cnblogs.com/frankliiu-java/articles/1754835.htmlhttp://sjsky.iteye.com/blog/962870

NullAppender做爲一個Null Object模式存在,不會輸出任何打印日誌消息。

相關文章
相關標籤/搜索