深入Log4J源碼之Appender – JAVA編程語言程序開發技術文章

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 Core一小節中已經簡單的介紹過瞭AppenderSkeleton、WriterAppender、ConsoleAppender以及 Filter,因小節將直接介紹具體的幾個常用的Appender。

FileAppender類

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類

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負責定義日志輸出的目的地,它可以是控制臺(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 Core一小節中已經簡單的介紹過瞭AppenderSkeleton、WriterAppender、ConsoleAppender以及 Filter,因小節將直接介紹具體的幾個常用的Appender。

FileAppender類

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類

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不熟,沒怎麼看代碼,不過可以使用一下測試用例簡單的做一些測試,提供一些感覺。www.aiwalls.com

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 }
作者:上善若水

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。