學習如何用自己的 appender 來擴展 log4j 框架

日誌記錄不僅是開發和測試周期中的一個重要元素――提供關鍵調試信息,而且對於系統已部署到生產環境之後調試錯誤也是很有用的――提供修復錯誤所需的準確上下文信息。在本文中,Orange Soft 公司(這是一家專業從事面向對象技術服務器Java 平臺和 Web 可訪問性的西班牙公司)的共同創辦人 Ruth Zamorano 和 Rafael Luque 闡述瞭如何利用 log4j 的擴展能力,使得分佈式 Java 應用程序能夠通過即時消息傳送(instant messaging,IM)來監視。

不管您編寫多少設計良好的測試用例,即使是最小的應用程序也會在部署到生產環境之後隱藏着一個或多個錯誤。雖然測試驅動的開發和 QA 手段可以提高代碼質量並增強對應用程序的信心,但是當某個系統失敗時,開發人員和系統管理員需要了解系統的相關執行上下文信息。有了適當的信息,他們就能確定問題的本質並快速解決問題,從而節省時間金錢

監視分佈式應用程序要求能夠對遠程資源進行日誌記錄――通常是一臺中央日誌服務器或者系統管理員的計算機。log4j 環境提供一組適用於遠程日誌記錄的 appender,比如 SocketAppender 、 JMSAppender 和SMTPAppender 。在本文中,我們將向您展示一種新的遠程類(remote-class)appender: IMAppender 。

讓我們首先簡要回顧一下 log4j ,然後再深入研究 appender。自然地,理解 appender 的最好方式就是試着編寫一個 appender,因此我們將在最後一節實現一個例子 IM(即時消息傳送)appender,以說明 AppenderSkeleton 類的工作原理

讀者應該熟悉 log4j 框架。關於 log4j 的更多信息,請參見本文後面的 參考資料 。

log4j 概述
log4j 框架是用 Java 語言編寫的事實上的標準日誌記錄框架。作爲 Jakarta 項目的一部分,它在 Apache 軟件許可證(Apache Software License)下分發,Apache 軟件許可證是由開放源代碼促進會(Open Source Initiative ,OSI)認證的一種流行的開放源代碼許可證。log4j 環境是完全可配置的,或者通過編程方式完成,或者通過屬性中的配置文件或者 XML 格式的配置文件完成。此外,它還允許開發人員無需修改源代碼就可以選擇性地篩選出日誌記錄請求。

log4j 環境包括三個主要組件:

  • logger(日誌記錄器)控制要啓用或禁用哪些日誌記錄語句。可以對日誌記錄器指定如下級別: ALL 、DEBUG 、 INFO 、 WARN 、 ERROR , FATA或 OFF 。 
  • layout(佈局):根據用戶的願望格式化日誌記錄請求。 
  • appender:向目的地發送格式化的輸出。

理解 appender
log4j 框架允許向任何日誌記錄器附加多個 appender。可以在任何時候對某個日子記錄器添加(或刪除)appender。附隨 log4j 分發的 appender 有多個,包括:

  • ConsoleAppender
  • FileAppender
  • SMTPAppender
  • JDBCAppender
  • JMSAppender
  • NTEventLogAppender
  • SyslogAppender

也可以創建自己的自定義 appender。

log4j 最主要的特性之一就是它的靈活性。遺憾的是,沒有多少現存文檔說明了如何編寫自己的 appender。學習編寫 appender 的方式之一就是分析可用的源代碼,然後嘗試推斷 appender 是如何工作的――本文將幫助 您完成這個任務。

揭開面紗
所有的 appender 都必須擴展 org.apache.log4j.AppenderSkeleton 類,這是一個抽象類,它實現了 org.apache.log4j.Appender 和 org.apache.log4j.spi.OptionHandler 接口。 AppenderSkeleton 類的 UML 類圖看起來如圖1所示:

圖 1. AppenderSkeleton 的 UML 類圖 
AppenderSkeleton UML class diagram

下面讓我們研究一下 AppenderSkeleton 類所實現的 Appender 接口的方法。如清單1所示, Appender 接口中的幾乎所有方法都是 setter 方法和 getter 方法:

清單1. Appender 接口

 package org.apache.log4j;  
 public interface Appender { 
   void addFilter(Filter newFilter);  
   void clearFilters() ;  
   void close();  
   void doAppend(LoggingEvent event);  
   ErrorHandler getErrorHandler();  
   Filter getFilter();  
   Layout getLayout();  
   String getName();  
   boolean requiresLayout();  
   void setErrorHandler(ErrorHandler errorHandler);  
   void setLayout(Layout layout);  
   void setName(String name);  
 } 

 

這些方法處理 appender 的如下屬性:

  • name:Appender 是命名的實體,因此有一個針對其名稱的 setter/getter。 
  • layout: Appender 可以具有關聯的 Layout,因此還有另一個針對 layout 的setter/getter 方法。注意我們說的是“可以”而不是“必須”。這是因爲有些 appender 不需要 layout。lauout 管理格式輸出――也就是說,它返回LoggingEvent 的 String 表示形式。另一方面, JMSAppender 發送的事件是 串行化的,因此您不需要對它附加 layout。如果自定義的 appender 不需要 layout,那麼 requiresLayout() 方法必須返回 false ,以避免 log4j 抱怨說丟失了 layout 信息。 
  • errorHandler : 另一個 setter/getter 方法是爲 ErrorHandler 而存在的。appender 可能把它們的錯誤處理委託給一個 ErrorHandler 對象――即 org.apache.log4j.spi 包中的一個接口。實現類有兩個: OnlyOnceErrorHandler 和 FallbackErrorHandler 。 OnlyOnceErrorHandle 實現 log4j 的默認錯誤處理策略,它發送出第一個錯誤的消息並忽略其餘的所有錯誤。錯誤消息將輸出到 System.err 。 FallbackErrorHandler 實現 ErrorHandler 接口,以便能夠指定一個輔助的 appender。如果主 appender 失敗,輔助 appender 將接管工作。錯誤消息將輸出到 System.err ,然後登錄到新的輔助 appender。

還有管理過濾器的其他方法(比如 ddFilter() 、 clearFilters() 和 getFilter() 方法 )。儘管 log4j 具有過濾日誌請求的多種內置方法(比如知識庫範圍級、日誌記錄器級和 appender 閾值級),但它使用自定義過濾器方法的能力也是非常強大的。

一個 appender 可以包含多個過濾器。自定義過濾器必須擴展 org.apache.log4j.spi.Filter 抽象類。這個抽象類要求把過濾器組織爲線性鏈。 對每個過濾器的 decide(LoggingEvent) 方法的調用要按照過濾器被添加到鏈中的順序來進行。自定義過濾器基於三元邏輯。 decide() 方法必須返回 DENY 、 NEUTRAL 或者 ACCEPT 這三個整型常量值之一。

除了 setter/getter 方法以及和過濾器相關的方法外,還有另外兩個方法: close() 和 doAppend() 。 close() 方法釋放 appender 中分配的任何資源,比如文件句柄、網絡連接,等等。在編寫自定義 appender 代碼時,務必要實現這個方法,以便當您的 appender 關閉時,它的 closed 字段將被設置爲 true 。

如清單2所示的 doAppend() 方法遵循“四人組模板方法(Gang of Four Template Method )”設計模式(參見 參考資料)。這個方法提供了一個算法框架,它把某些步驟推遲到子類中來實現。

清單2:doAppend() 方法的實際源代碼

public synchronized void doAppend (LoggingEvent event) {  
  if (closed) { 
    // step 1 
    LogLog.error("Attempted to append to closed appender [" + name + "]."); 
    return; 
  } if ( !isAsSevereAsThreshold (event.level) ) { 
    // step 2 
    return; 
  } 
  Filter f = this.headFilter; 
  // step 3 
  FILTER_LOOP: 
  while ( f != null) { 
    switch ( f .decide(event) ) { 
      case Filter.DENY: return; 
      case Filter.ACCEPT: break FILTER_LOOP; 
      case Filter.NEUTRAL: f = f.next; 
    } 
  } 
  this.append(event); 
  // step 4 
} 

 

如清單2所示,該算法:

  1. 檢查 appender 是否關閉。附加關閉的 appender 是一個編程錯誤。
  2. 檢查正在記錄日誌的事件是否處於 appender 的閾值之下。
  3. 檢查是否有過濾器附加到 appender,如果有,則拒絕請求。
  4. 調用 appender 的 append() 方法。這個步驟被委託給每個子類。

我們已經介紹了 AppenderSkeleton 從 Appender 繼承來的方法和屬性。下面讓我們看看“爲什麼” AppenderSkeleton 要實現 OptionHandler 接口。 OptionHandler 僅包含一個方法: activateOptions() 。這個方法在對屬性調用 setter 方法之後由一個配置器類調用。有些屬性彼此依賴,因此它們在全部加載完成之前是無法激活的,比如在 activateOptions() 方法中就是這樣。這個方法是開發人員在 appender 變爲激活和就緒之前用來執行任何必要任務的機制。

除了上面提到的所有方法,讓我們再回頭觀察一下 圖1。注意 AppenderSkeleton 提供了一個新的抽象方法( append() 方法)和一個新的 JavaBean 屬性( threshold )。 threshold 屬性由 appender 用來過濾日誌記錄請求,只有超過閾值的請求才會得到處理。我們在談到 doAppend() 方法之前就提到了 append() 方法。它是自定義 appender 必須實現的一個抽象方法,因爲框架在 doAppend() 方法內調用 append() 方法。 append() 方法是框架的鉤子(hook)之一。

現在我們已經看到了 AppenderSkeleton 類中的所有可用方法,下面讓我們看看幕後發生的事情。圖2演示了 log4j 中的一個 appender 對象的 生命週期。

圖 2. appender 的生命週期圖 
Life cycle of an appender object inside log4j

讓我們逐步地研究一下這個圖表:

  • appender 實例不存在。或許框架還沒有配置好。 
  • 框架實例化了一個新的 appender。這發生在配置器類分析配置腳本中的一個 appender 聲明的時候。配置器類調用 Class.newInstance(YourCustomAppender.class) ,這等價於動態調用 new YourCustomAppender() 。框架這樣做是爲了避免被硬編碼爲任何特定的 appender 名稱;框架是通用的,適用於任何 appender。 
  • 框架判斷 appender 是否需要 layout。如果該 appender 不需要 layout,配置器就不會嘗試從配置腳本中加載 layout 信息。 
  • Log4j 配置器調用 setter 方法。在所有屬性都已設置好之後,框架就會調用這個方法。程序員可以在這裏激活必須同時激活的屬性。 
  • 配置器調用 activateOptions() 方法。在所有屬性都已設置好之後,框架就會調用這個方法。程序員可以在這裏激活必須同時激活的屬性。 
  • Appender 準備就緒。 此刻,框架可以調用 append() 方法來處理日誌記錄請求。這個方法由 AppenderSkeleton.doAppend() 方法調用。 
  • 最後,關閉appender。 當框架即將要刪除您的自定義 appender 實例時,它會調用您的 appender 的 close() 方法。 close() 是一個清理方法,意味着 您需要釋放已分配的所有資源。它是一個必需的方法,並且不接受任何參數。它必須把 closed 字段設置爲 true ,並在有人嘗試使用關閉的 appender 時向框架發出警報。

現在我們已經回顧了與建立自己的 appender 相關的概念,下面讓我們考慮一個包括真實例子appender 的完整案例研究。

 

編寫自定義 appender 的訣竅

  1. 擴展 AppenderSkeleton 抽象類。 
  2. 指定您的 appender 是否需要 layout。
  3. 如果某些屬性必須同時激活,則應該在 activateOptions() 方法內完成。 
  4. 實現 close() 方法。它必須把 closed 字段的值設置爲 true 。記得釋放所有資源。 
  5. 可選地指定要使用的默認 ErrorHandler 對象。 
  6. 編寫 append() 方法的代碼。這個方法負責附加日誌記錄事件,並在錯誤發生時負責調用錯誤處理程序。

 

 

編寫基於 IM 的 appender
本文給出的代碼說明了如何擴展 log4j 框架以集成 IM 特性。它被設計來使得 log4j 相容的應用程序能夠把輸出記錄到 IM 網絡上。IM appender 實際上充當一個自定義的 客戶機。然而,它不是把 System.out 、文件或者 TCP 套接字當作底層輸出設備,而是把 IM 網絡當作底層輸出設備。

爲了提供 IM 支持,我們不需要在開發特定解決方案時完全重新開始。相反,我們將利用一個我們認爲是該類別中最好的工具:Jabber。Jabber 是一種用於即時消息傳送和展示的基於 XML 的開放協議,它由 Jabber 社區開發,非 營利性的 Jabber 軟件基金會(Jabber Software Foundation)對它提供技術支持。

我們之所以選擇 Jabber 而沒有選擇其他 IM 系統,是因爲 Jabber 提供了廣泛的好處,包括它的:

  • 開放性質: 不像其他的專有系統,Jabber 的規範和源代碼是可以免費獲得的,從而允許任何人無成本地創建 Jabber 實現。 
  • 簡單性:Jabber 使用基於 XML 的簡單協議作爲它的標準數據格式,並且遵循大多數人都理解的客戶機/服務器 架構。 
  • 與其他 IM 系統的互操作性:Jabber 傳輸模塊(transport module)使得 Jabber 用戶訪問諸如 AIM、Yahoo!Messager 和 ICQ 等其他即時消息傳送系統成爲可能。 
  • 資源敏感:Jabber 對多客戶機訪問提供明確的支持。同一個用戶可以通過不同的客戶機(或者說 資源)同時地連接到Jabber 服務器,消息將被恰當地路由到可用的最佳資源。

爲什麼要把日誌記錄到 IM 網絡?
日誌記錄是開發人員必須養成的良好編碼習慣,就像編寫單元測試、處理異常或者編寫 Javadoc 註釋一樣。插入到代碼中明確位置的日誌記錄語句起着審覈工具的功能,提供了關於應用程序內部狀態的有用信息。與主流意見相反,我們認爲在許多情況下,將日誌語句保留在生產代碼中是方便的。如果您擔心計算成本,就必須考慮從應用程序中刪除日誌記錄功能所帶來的少量性能提升是否值得。此外,log4j 的靈活性允許您聲明式地控制日誌記錄行爲。您可以建立嚴格的日誌記錄策略來降低日誌的累贅性並改進性能。

圖3顯示了 IMAppender 的一個使用場景:一個配置爲使用 IMAppender 的 log4j 應用程序記錄 它的被包裝爲 IM 消息的調試數據。即時消息通過 Jabber 公司網絡被路由到系統管理員的Jabber 地址(注意,公開可用的 Jabber 服務器對生產應用可能不足夠可靠)。因而,無論何時系統管理員需要檢查應用程序的狀態,他們只需加載最喜歡的 Jabber 客戶機,然後連接到Jabber 服務器。如圖3所示,管理員可以通過不同的設備來訪問。他可以使用辦公室的 PC 來登錄服務器,或者當他離開辦公桌時,可以使用運行在手持設備上的 Jabber 客戶機來檢查消息。

圖 3. IMAppender 使用場景 
IMAppender usage scenario

但是爲什麼需要 IM appender 呢?因爲向 IM 服務器發送消息將允許您通過自由選擇的工具(比如Jabber客戶機)來更容易地監視應用程序行爲。

IMAppender 提供了多個優點:

  • 獲得實時通知――我們稱之爲“即時日誌記錄”。
  • 支持一對一(聊天)和一對多(小組聊天)模式。
  • Jabber 不只是用於臺式計算機。用於諸如 PDA 和移動電話等無線設備的客戶機也正在開發之中。
  • 要進入或退出應用程序正在向其轉發日誌數據的聊天室很容易。而要訂閱和取銷訂閱由 SMTPAppender 發送的電子郵件則很困難。 
  • 通過安全套接字層(SSL)上的隧道來保證安全很容易。當然,您可以加密電子郵件,但是 SSL上 的 Jabber 既方便又快捷。

進階
IMAppender 模仿隨 log4j 一起分發的 SMTPAppender 的日誌記錄策略。 IMAppender 把日誌記錄事件存儲在一個內部循環緩衝區(cyclic buffer)中,並且僅當所接收到的日誌記錄請求觸發了某個用戶指定的條件時,才把這些事件作爲即時消息來發送。或者,用戶也可以提供一個觸發事件鑑別器類(triggering event evaluator class)。然而在默認情況下,消息傳送是由指定爲 ERROR 或者更高級別的事件所觸發的。

每個消息中傳送的日誌記錄事件的數量是由緩衝區的大小決定的。循環緩衝區僅保留最後的 bufferSize 個日誌記錄事件,當它裝滿時就會溢出並丟棄較舊的事件。

爲了連接到 Jabber 服務器, IMAppender 需要依賴 Jive Software 公司的 Smack API。Smack 是一個開放源代碼的高級庫,它處理與 Jabber 服務器通信的協議細節。這樣, 您無需任何特別的 Jabber 或者 XML 專業經驗就能理解代碼。

IMAppender 的屬性總結在表 1中:

表 1. IMAppender 屬性

屬性 說明 類型 是否必需
host 服務器的主機名稱 String
port Jabber服務器的端口號 int 否,默認爲 5222
username 應用程序的Jabber帳戶用戶名 String
password 應用程序的Jabber帳戶密碼 String
recipient 接收方的Jabber地址。Jabber地址也稱爲Jabber ID,它在一個@字符後面指定用戶的Jabber 域,就像電子郵件地址一樣 這個屬性可以保存任何聊天地址或者聊天室地址。例如,您可以指定這樣的聊天地址:sysadmin@company.com;或者您可能希望向 [email protected] 小組聊天服務器上名爲"java-apps"的某個聊天小組發送日誌記錄消息(例如, [email protected] ) String
chatroom 接受一個布爾值。如果爲 true , recipient 值將被接受爲小組聊天地址。如果要設置這個選項,還應該設置 nickname 選項。默認情況下,recipient 值被解釋爲一個聊天地址 boolean 否,默認爲 false
nickname 僅當設置了 chatroom 屬性時纔會考慮這個屬性。否則,它將被忽略 
用戶可以選擇 appender 使用的任意小組聊天暱稱來加入小組聊天。暱稱不一定要和 Jabber用戶名有關
String
SSL 用於保護與 Jabber 服務器的連接 boolean 否,默認爲 false
bufferSize 可以保留在循環緩衝區中的日誌記錄事件的最大數量 int 否,默認爲 16
evaluatorClass 這個屬性的值被當作一個類的完全限定名稱的字符串表示形式,該類實現了 org.apache.log4j.spi. TriggeringEventEvaluator 接口(換句話說,也就是一個包含自定義觸發邏輯的類,它覆蓋了默認的觸發邏輯)。如果沒有指定這個選項, IMAppender 將使用 DefaultEvaluator 類的一個實例,這個類根據被指定爲 ERROR 或更高級別的事件觸發響應 String 否,默認爲 DefaultEvaluator

 

現在讓我們進一步觀察代碼。IMAppender 類遵循清單3所示的結構:

清單 3. IMAppender 類的總體結構

package com.orangesoft.logging;  
import org.apache.log4j.AppenderSkeleton; 
import org.apache.log4j.spi.LoggingEvent; 
import org.apache.log4j.spi.TriggeringEventEvaluator;  
public class IMAppender extends AppenderSkeleton {  
  private String host; 
  private int port = 5222; 
  private String username; 
  private String password; 
  private String recipient; 
  private boolean chatroom = false; 
  private String nickname; 
  private boolean SSL = false; 
  private int bufferSize = 16; 
  protected TriggeringEventEvaluator evaluator;  
  // Set/Get methods for properties  
  public void setHost(String host) { 
    this.host = host;  
  }  
  public String getHost() {  
    return this.host;  
  }  ...other set/get methods...  
  // AppenderSkeleton callback methods  
  public boolean requiresLayout() { ... } 
  public void activateOptions() { ... } 
  public void append(LoggingEvent event) { ... } 
  public synchronized void close() { ... }  
} 

 

請注意關於我們的 appender 的如下幾個方面:

  • IMAppender 類擴展 org.apache.log4j.AppenderSkeleton ,這是所有自定義 appender 都必須要做的。 IMAppender 從 AppenderSkeleton 繼承諸如 appender 閾值和自定義過濾之類的公共功能。 
  • 我們的 appender 的第一部分很簡單。每個 appender 都有字段和 set/get 方法。屬性和方法簽名遵守 JavaBeans 命名約定。因而,log4j 能夠通過反射來分析 appender,透明地處理 appender 配置。爲節省篇幅,上述代碼片斷僅顯示了 setHost() 和 getHost() 方法。 
  • 爲了完成我們的 appender,我們必須實現 log4j 框架調用來管理我們的 appender 的回調方法: requiresLayout() 、 activateOptions() 、 append() 和 close() 。

log4j 框架調用 requiresLayout() 方法來判斷自定義 appender 是否需要 layout。注意,有些appender 使用內置格式或者根本就不格式化事件,因此它們不需要 Layout 對象。 IMAppender 需要 layout,因而該方法返回 true ,如 清單4所示:

清單 4. requiresLayout() 方法

 public boolean requiresLayout() { return true; } 

 

注意, AppenderSkeleton 實現了 org.apache.log4j.spi.OptionHandler 接口(參見 圖 1 )。 AppenderSkeleton 把這個接口的單個方法 activateOptions() 實現爲一個空方法。我們的 IMAppender需要這個方法是由於其屬性之間的相互依賴性。例如,與 Jabber 服務器的連接依賴 Host 、 Port 和 SSL 屬性,因此 IMAppender 在這三個屬性被初始化之前無法建立連接。log4j 框架調用 activateOptions()方法來通知 appender 所有屬性都已設置就緒。

IMAppender.activateOptions() 方法激活指定的屬性(比如 Jabber 主機、端口、 bufferSize ,等等),所採取的方式是實例化依賴這些屬性值的更高級對象,如清單5所示:

清單 5. 只有在調用 activateOptions() 方法之後,屬性纔會被激活且變得有效

protected org.apache.log4j.helpers.CyclicBuffer cb;  
protected org.jivesoftware.smack.XMPPConnection con; 
protected org.jivesoftware.smack.Chat chat; 
protected org.jivesoftware.smack.GroupChat groupchat;  
public void activateOptions() { 
  try {  
    cb = new CyclicBuffer(bufferSize);  
    if (SSL) { 
      con = new SSLXMPPConnection(host, port); 
    } else { 
      con = new XMPPConnection(host, port); 
    }  
    con.login(username, password);  
    if (chatroom) { 
      groupchat = con.createGroupChat(recipient); 
      groupchat.join(nickname != null ? nickname : username); 
    } else { 
      chat = con.createChat(recipient); 
    }  
  } catch (Exception e) { 
    errorHandler.error("Error while activating options for appender  named [" + name + "]",  e, ErrorCode.GENERIC_FAILURE); 
  }  
} 

 

activateOptions() 方法完成以下任務:

  • 建立 bufferSize 個事件的最大循環緩衝區。我們使用了 org.apache.log4j.helpers.CyclicBuffer 的一個實例, org.apache.log4j.helpers.CyclicBuffer 是 log4j 附帶的一個輔助類,它提供了緩衝區的邏輯。 
  • Smack 的 XMPPConnection 類創建了一個到 XMPP (Jabber) 服務器的連接,這個服務器是通過 host 和 port 屬性來指定的。爲了創建一個 SSL 連接,我們要使用 SSLXMPPConnection 子類。 
  • 大多數服務器都要求您在執行其他任務之前首先登錄,因此我們使用由 username 和 password 屬性所定義的 Jabber 帳戶來登錄,同時調用 XMPPConnection.login() 方法。 
  • 在登錄之後,我們創建一個 Chat 或者 GroupChat 對象,具體視 chatroom 值而定。

在 activateOptions() 方法返回之後,appender 就準備好處理日誌記錄請求了。如 清單6所示,由 AppenderSkeleton.doAppend() 調用的 append() 方法將執行大多數實際的日誌附加工作。

清單 6. append() 執行實際的輸出操作

 public void append(LoggingEvent event) {  
  // check pre-conditions 
  if (!checkEntryConditions()) { 
    return; 
  }  
  cb.add(event); 
  if (evaluator.isTriggeringEvent(event)) { sendBuffer(); } 
}  
protected boolean checkEntryConditions() { 
  if ((this.chat == null) && (this.groupchat == null)) { 
    errorHandler.error("Chat object not configured"); 
    return false; 
  }  
  if (this.layout == null) { 
    errorHandler.error("No layout set for appender named [" + name + "]"); 
    return false; 
  } 
  return true; 
} 

 

append() 方法中的第一個語句判斷進行附加嘗試是否有意義。 checkEntryConditions() 方法檢查是否有可用於附加到輸出的 Chat 或者 GroupChat 對象,以及是否有用於格式化傳入 event 對象的 Layout 對象。如果這些前提條件得不到滿足,那麼 append() 將輸出一條警告消息並返回,從而不會繼續進行輸出操作。下一個語句把事件添加到循環緩衝區實例 cb 。然後, if 語句把日誌記錄事件提交給 evaluator ,這是一個 TriggeringEventEvaluator 實例。如果 evaluator 返回 true ,這意味着該事件與觸發條件匹配, sendBuffer() 就會被調用。

清單7顯示了 sendBuffer() 方法的代碼 

清單 7. sendBuffer()方法

protected void sendBuffer() { 
try { 
  StringBuffer buf = new StringBuffer();  
  int len = cb.length(); 
  for (int i = 0; i < len; i++) { 
    LoggingEvent event = cb.get(); 
    buf.append(layout.format(event));  
    // if layout doesn't handle exceptions, the appender has  
    // to do it 
    if (layout.ignoresThrowable()) { 
      String[] s = event.getThrowableStrRep(); 
      if (s != null) { 
        for (int j = 0; j < s.length; j++) { 
          buf.append(LINE_SEP); 
          buf.append(s[j]); 
        } 
      } 
    } 
  }  
  if (chatroom) { 
    groupchat.sendMessage(buf.toString()); 
  } else { 
    chat.sendMessage(buf.toString()); 
  }  
} catch(Exception e) { 
  errorHandler.error("Could not send message  in IMAppender [" + name + "]",  e, ErrorCode.GENERIC_FAILURE); 
} 
}  

 

sendBuffer() 方法把緩衝區的內容作爲IM消息來發送。此方法逐項遍歷保留在緩衝區中的事件,同時調用 layout 對象的 format() 方法來格式化每個事件。事件的字符串表示形式被附加到 StringBuffer 對象。最後, sendBuffer() 調用 chat 或者 groupchat 對象的 sendMessage() 方法,把消息發送出去。

請注意以下幾點:

  • AppenderSkeleton.doAppend() 方法(它調用 append() )是經過同步的,因此 sendBuffer() 已經擁有 appender 的監視器。這使得我們不必在 cb 上執行同步操作。 
  • 異常提供了極其有用的信息。由於這個原因,如果指定的 layout 忽略了包含在 LoggingEvent 對象中的可拋出對象,自定義 appender 的開發人員必須輸出包括在事件中的異常信息。如果 layout 忽略了可拋出的對象,那麼 layout 的 ignoresThrowable() 方法應該返回 true ,並且 sendBuffer() 可以使用 LoggingEvent.getThrowableStrRep() 方法來檢索包含在該事件中的可拋出信息的 String[] 表示形式。

 

下載源代碼
執行這個示例所必需的全部JAR文件(instantlogging.jar、smack-1.1.0.jar 和 log4j-1.2.8.jar)包括在本文的源代碼的 lib/ 目錄下。相應的 zip 檔案文件可從 參考資料 下載

 

把全部內容組合起來
下面將通過展示 IMAppender 的實際工作效果來結束本文的討論。我們將使用一個相當簡單的名爲 com.orangesoft.logging.example.EventCounter 的應用程序,如 清單8所示。這個示例應用程序在命令行接受兩個參數。第一個參數是一個整數,對應於要產生的日誌記錄事件的數量。第二個參數必須是以屬性的格式提供的一個 log4j 配置文件名。這個應用程序總是以 ERROR 事件結束,該事件將觸發一次 IM 消息傳送。

清單 8. EventCounter 示例應用程序

package com.orangesoft.logging.examples;  
import org.apache.log4j.Logger; 
import org.apache.log4j.PropertyConfigurator;   
/** * Generates the number of logging events indicated by the first  
* argument value. The application ends with an ERROR level event  
* to trigger the IMAppender action. 
*/  
public class EventCounter {  
  private static Logger logger = Logger.getLogger(EventCounter.class);  
  public static void main(String args[]) {  
    int numEvents = Integer.parseInt(args[0]);  
    String log4jConfigFile = args[1];  
    PropertyConfigurator.configure(log4jConfigFile);  
    for (int i = 1; i <= numEvents; i++) { logger.info("Event #" + i); }  
      logger.error("This error event triggers the delivery",  new Exception("This is a mock exception")); 
    }  
} 

 

我們可以使用類似清單9所示的配置文件:

清單 9. 示例 IMAppender 配置文件

log4j.rootLogger = ALL,im  
log4j.appender.im = com.orangesoft.logging.IMAppender  
log4j.appender.im.host = JABBER_SERVER (e.g. jabber.org) 
log4j.appender.im.username = APP_JABBER_ACCOUNT_USERNAME 
log4j.appender.im.password = APP_JABBER_ACCOUNT_PASSWORD 
log4j.appender.im.recipient = YOUR_JABBER_ADDRESS (e.g. [email protected])  
log4j.appender.im.layout=org.apache.log4j.PatternLayout 
log4j.appender.im.layout.ConversionPattern = %n%r [%-5p] %M:%L - %m 

 

上面的配置文件腳本把 IMAppender 添加到根日誌記錄器(root logger),這樣所接收到的每個日誌記錄請求都將被分派到我們的 appender。

在試驗這個示例應用程序之前,請確保將 host 、 username 、 password 和 recipient 屬性設置爲 您所在環境中的適當值。下面的命令將運行 EventCounter 應用程序:

 

 java com.orangesoft.logging.examples.EventCounter 100  eventcounter.properties 

 

當運行時, EventCounter 將根據 eventcounter.properties 所設置的策略記錄 100 個事件。然後一個 IM 消息將從接收方的屏幕上彈出來。圖4、5、6 顯示了不同平臺上的 Jabber 客戶機接收到的結果消息:

圖 4. Windows (Rhymbox)上的 Jabber 客戶機接收到的消息的屏幕快照 
Windows Jabber client (Rhymbox)

圖 5. Linux (PSI)上的 Jabber 客戶機接收到的消息的屏幕快照 
Linux Jabber client (PSI)

圖 6. Pocket PC (imov)上的 Jabber 客戶機接收到的消息的屏幕快照 
Pocket PC Jabber client (imov)

注意 EventCounter 產生了 100 個事件。然而,由於 IMAppender 緩衝區的默認大小爲 16,接收方應該收到僅包含最後 16 個事件的 IM 消息。可以看到,包含在最後一個事件(消息和堆棧跟蹤)中的異常信息已經被正確地傳送了。

這個例子應用程序只展示了 IMAppender 的一個非常小的用途,因此繼續探索它吧,您會找到很多樂趣的!

結束語
log4j 網絡 appender, SocketAppender 、 JMSAppender 和 SMTPAppender 已經提供了監視 Java 分佈式應用程序的機制。然而,多個因素使得 IM 成爲用於實時遠程日誌記錄的合適技術。在本文中,我們介紹了通過自定義 appender 來擴展 log4j 的基礎知識,並看到了一個基本 IMAppender 的逐步實現過程。許多開發人員和系統管理員都可以從 appender 的使用中獲益。

參考資料

    • 您可以參閱本文在 developerWorks 全球站點上的 英文原文

    • 下載 IMAppender 類的 源代碼 、例子應用程序和必需的庫。 您可以根據需要隨便使用和擴展這些源代碼。

    • 從 log4j項目首頁 獲得最新的 log4j 版本 ――包括完整的源代碼、類文件和文檔。欲瞭解更多信息,請在 log4j官方文檔頁 查看文章和演示材料。 

    • 如果您的開發工作需要超出本文檔範圍的內容,可以參考 Ceki Gülcü 編著的一本優秀參考書 log4j完全參考手冊 (QOS.ch, 2003),該書詳細介紹了 log4j 的基本特性和高級特性。 

    • developerWorks Web services 專區提供了 LogKit 作爲“每週內容”(component of the week)” (2001年8月)。LogKit 是 Jakarta 的 Avalon項目 的日誌記錄組件。 

    • Sun 已經完成了一個名爲 JSR 47 的社區過程(community process),它定義了 Java 平臺的日誌記錄 API。JSR 47 API和 log4j 在體系結構層次上相當類似,不過 log4j 具有許多 JSR 47 所沒有的特性。 

    • 在“ Merlin的魔力:異常和日誌記錄”( developerWorks,2001年12月)中,John Zukowski展示了新的 JDK 1.4 日誌記錄 API 是如何工作的。 

    • Thomas E. Davis 撰寫的 JavaWorld 文章 “ Logging on to Internet Relay Chat (IRC)” 介紹了一種允許應用程序把輸出寫到 IRC 的簡單工具。 

    • Gerhard Poul 的文章“ Jabber” ( developerWorks,2002年5月)說明了 Jabber 如何適合於當今的電子商務 基礎設施。 

    • Smack 是 Jive Software 公司提供的一個開放源代碼庫,用於與 Jabber 服務器通信以執行即時消息傳送和聊天。 

    • Iain Shigeoka 的文章 用 Java 實現即時消息傳送 (Manning, 2002) 深入分析了各種 Jabber 協議。 

    • “模板方法(Template Method)設計模式”出自 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 所著的 設計模式 一書(Addison-Wesley出版, 1995年),這四位作者也被稱爲“四人組(Gang of Four,GoF)。 

    • 也可以在線閱讀 模板方法設計模式 的相關報道。 

    • 在 developerWorks Java技術專區 可以找到數百篇關於 Java 編程的各個方面的文章。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章