級別:高級 |
Ruth Zamorano (ruth.zamorano@orange-soft.com),軟件架構師,Orange Soft
Rafael Luque (rafael.luque@orange-soft.com), CTO,Orange Soft
2003年 9 月
日志記錄不僅是開(kāi)發(fā)和測試周期中的一個(gè)重要元素——提供關(guān)鍵調試信息,而且對于系統已部署到生產(chǎn)環(huán)境之后調試錯誤也是很有用的——提供修復錯誤所需的準確上下文信息。在本文中,Orange Soft 公司(這是一家專(zhuān)業(yè)從事面向對象技術(shù)、服務(wù)器端Java 平臺和 Web 可訪(fǎng)問(wèn)性的西班牙公司)的共同創(chuàng )辦人 Ruth Zamorano 和 Rafael Luque 闡述了如何利用 log4j 的擴展能力,使得分布式 Java 應用程序能夠通過(guò)即時(shí)消息傳送(instant messaging,IM)來(lái)監視。
不管您編寫(xiě)多少設計良好的測試用例,即使是最小的應用程序也會(huì )在部署到生產(chǎn)環(huán)境之后隱藏著(zhù)一個(gè)或多個(gè)錯誤。雖然測試驅動(dòng)的開(kāi)發(fā)和 QA 手段可以提高代碼質(zhì)量 并增強對應用程序的信心,但是當某個(gè)系統失敗時(shí),開(kāi)發(fā)人員和系統管理員需要了解系統的相關(guān)執行上下文信息。有了適當的信息,他們就能確定問(wèn)題的本質(zhì)并快速解決問(wèn)題,從而節省時(shí)間和金錢(qián)。
監視分布式應用程序要求能夠對遠程資源進(jìn)行日志記錄——通常是一臺中央日志服務(wù)器或者系統管理員的計算機。log4j 環(huán)境提供一組適用于遠程日志記錄的 appender,比如 SocketAppender、JMSAppender 和 SMTPAppender。在本文中,我們將向您展示一種新的遠程類(lèi)(remote-class)appender:IMAppender。
讓我們首先簡(jiǎn)要回顧一下 log4j ,然后再深入研究 appender。自然地,理解 appender 的最好方式就是試著(zhù)編寫(xiě)一個(gè) appender,因此我們將在最后一節實(shí)現一個(gè)例子 IM(即時(shí)消息傳送)appender,以說(shuō)明 AppenderSkeleton 類(lèi)的工作原理。
讀者應該熟悉 log4j 框架。關(guān)于 log4j 的更多信息,請參見(jiàn)本文后面的 參考資料 。
log4j 概述
log4j 框架是用 Java 語(yǔ)言編寫(xiě)的事實(shí)上的標準日志記錄框架。作為 Jakarta 項目的一部分,它在 Apache 軟件許可證(Apache Software License)下分發(fā),Apache 軟件許可證是由開(kāi)放源代碼促進(jìn)會(huì )(Open Source Initiative ,OSI)認證的一種流行的開(kāi)放源代碼許可證。log4j 環(huán)境是完全可配置的,或者通過(guò)編程方式完成,或者通過(guò)屬性中的配置文件或者 XML 格式的配置文件完成。此外,它還允許開(kāi)發(fā)人員無(wú)需修改源代碼就可以選擇性地篩選出日志記錄請求。
log4j 環(huán)境包括三個(gè)主要組件:
ALL、DEBUG、INFO、WARN、ERROR, FATA或OFF。理解 appender
log4j 框架允許向任何日志記錄器附加多個(gè) appender??梢栽谌魏螘r(shí)候對某個(gè)日子記錄器添加(或刪除)appender。附隨 log4j 分發(fā)的 appender 有多個(gè),包括:
ConsoleAppender FileAppender SMTPAppender JDBCAppender JMSAppender NTEventLogAppender SyslogAppender也可以創(chuàng )建自己的自定義 appender。
log4j 最主要的特性之一就是它的靈活性。遺憾的是,沒(méi)有多少現存文檔說(shuō)明了如何編寫(xiě)自己的 appender。學(xué)習編寫(xiě) appender 的方式之一就是分析可用的源代碼,然后嘗試推斷 appender 是如何工作的——本文將幫助 您完成這個(gè)任務(wù)。
揭開(kāi)面紗
所有的 appender 都必須擴展 org.apache.log4j.AppenderSkeleton 類(lèi),這是一個(gè)抽象類(lèi),它實(shí)現了 org.apache.log4j.Appender 和 org.apache.log4j.spi.OptionHandler 接口。AppenderSkeleton 類(lèi)的 UML 類(lèi)圖看起來(lái)如圖1所示:
圖 1. AppenderSkeleton 的 UML 類(lèi)圖

下面讓我們研究一下 AppenderSkeleton 類(lèi)所實(shí)現的 Appender 接口的方法。如清單1所示,Appender 接口中的幾乎所有方法都是 setter 方法和 getter 方法:
|
這些方法處理 appender 的如下屬性:
LoggingEvent 的 String 表示形式。另一方面,JMSAppender 發(fā)送的事件是 串行化的,因此您不需要對它附加 layout。如果自定義的 appender 不需要 layout,那么 requiresLayout() 方法必須返回 false,以避免 log4j 抱怨說(shuō)丟失了 layout 信息。errorHandler: 另一個(gè) setter/getter 方法是為 ErrorHandler 而存在的。appender 可能把它們的錯誤處理委托給一個(gè) ErrorHandler 對象——即 org.apache.log4j.spi 包中的一個(gè)接口。實(shí)現類(lèi)有兩個(gè):OnlyOnceErrorHandler 和 FallbackErrorHandler。OnlyOnceErrorHandle 實(shí)現 log4j 的默認錯誤處理策略,它發(fā)送出第一個(gè)錯誤的消息并忽略其余的所有錯誤。錯誤消息將輸出到 System.err。FallbackErrorHandler 實(shí)現 ErrorHandler 接口,以便能夠指定一個(gè)輔助的 appender。如果主 appender 失敗,輔助 appender 將接管工作。錯誤消息將輸出到 System.err,然后登錄到新的輔助 appender。 還有管理過(guò)濾器的其他方法(比如 ddFilter()、clearFilters()和 getFilter() 方法 )。盡管 log4j 具有過(guò)濾日志請求的多種內置方法(比如知識庫范圍級、日志記錄器級和 appender 閾值級),但它使用自定義過(guò)濾器方法的能力也是非常強大的。
一個(gè) appender 可以包含多個(gè)過(guò)濾器。自定義過(guò)濾器必須擴展 org.apache.log4j.spi.Filter 抽象類(lèi)。這個(gè)抽象類(lèi)要求把過(guò)濾器組織為線(xiàn)性鏈。 對每個(gè)過(guò)濾器的 decide(LoggingEvent) 方法的調用要按照過(guò)濾器被添加到鏈中的順序來(lái)進(jìn)行。自定義過(guò)濾器基于三元邏輯。decide() 方法必須返回 DENY、NEUTRAL 或者 ACCEPT 這三個(gè)整型常量值之一。
除了 setter/getter 方法以及和過(guò)濾器相關(guān)的方法外,還有另外兩個(gè)方法:close() 和 doAppend()。close() 方法釋放 appender 中分配的任何資源,比如文件句柄、網(wǎng)絡(luò )連接,等等。在編寫(xiě)自定義 appender 代碼時(shí),務(wù)必要實(shí)現這個(gè)方法,以便當您的 appender 關(guān)閉時(shí),它的 closed 字段將被設置為 true。
如清單2所示的 doAppend() 方法遵循“四人組模板方法(Gang of Four Template Method )”設計模式(參見(jiàn) 參考資料)。這個(gè)方法提供了一個(gè)算法框架,它把某些步驟推遲到子類(lèi)中來(lái)實(shí)現。
|
如清單2所示,該算法:
append() 方法。這個(gè)步驟被委托給每個(gè)子類(lèi)。 我們已經(jīng)介紹了 AppenderSkeleton 從 Appender 繼承來(lái)的方法和屬性。下面讓我們看看“為什么”AppenderSkeleton 要實(shí)現 OptionHandler 接口。OptionHandler 僅包含一個(gè)方法:activateOptions()。這個(gè)方法在對屬性調用 setter 方法之后由一個(gè)配置器類(lèi)調用。有些屬性彼此依賴(lài),因此它們在全部加載完成之前是無(wú)法激活的,比如在 activateOptions() 方法中就是這樣。這個(gè)方法是開(kāi)發(fā)人員在 appender 變?yōu)榧せ詈途途w之前用來(lái)執行任何必要任務(wù)的機制。
除了上面提到的所有方法,讓我們再回頭觀(guān)察一下圖1。注意 AppenderSkeleton 提供了一個(gè)新的抽象方法(append() 方法)和一個(gè)新的 JavaBean 屬性(threshold)。threshold 屬性由 appender 用來(lái)過(guò)濾日志記錄請求,只有超過(guò)閾值的請求才會(huì )得到處理。我們在談到 doAppend() 方法之前就提到了 append() 方法。它是自定義 appender 必須實(shí)現的一個(gè)抽象方法,因為框架在 doAppend() 方法內調用 append() 方法。append()方法是框架的鉤子(hook)之一。
現在我們已經(jīng)看到了 AppenderSkeleton 類(lèi)中的所有可用方法,下面讓我們看看幕后發(fā)生的事情。圖2演示了 log4j 中的一個(gè) appender 對象的 生命周期。

讓我們逐步地研究一下這個(gè)圖表:
Class.newInstance(YourCustomAppender.class),這等價(jià)于動(dòng)態(tài)調用 new YourCustomAppender()??蚣苓@樣做是為了避免被硬編碼為任何特定的 appender 名稱(chēng);框架是通用的,適用于任何 appender。close() 方法。close() 是一個(gè)清理方法,意味著(zhù) 您需要釋放已分配的所有資源。它是一個(gè)必需的方法,并且不接受任何參數。它必須把 closed 字段設置為 true,并在有人嘗試使用關(guān)閉的 appender 時(shí)向框架發(fā)出警報。 現在我們已經(jīng)回顧了與建立自己的 appender 相關(guān)的概念,下面讓我們考慮一個(gè)包括真實(shí)例子appender 的完整案例研究。
|
編寫(xiě)基于 IM 的 appender
本文給出的代碼說(shuō)明了如何擴展 log4j 框架以集成 IM 特性。它被設計來(lái)使得 log4j 相容的應用程序能夠把輸出記錄到 IM 網(wǎng)絡(luò )上。IM appender 實(shí)際上充當一個(gè)自定義的 客戶(hù)機。然而,它不是把 System.out、文件或者 TCP 套接字當作底層輸出設備,而是把 IM 網(wǎng)絡(luò )當作底層輸出設備。
為了提供 IM 支持,我們不需要在開(kāi)發(fā)特定解決方案時(shí)完全重新開(kāi)始。相反,我們將利用一個(gè)我們認為是該類(lèi)別中最好的工具:Jabber。Jabber 是一種用于即時(shí)消息傳送和展示的基于 XML 的開(kāi)放協(xié)議,它由 Jabber 社區開(kāi)發(fā),非 營(yíng)利性的 Jabber 軟件基金會(huì )(Jabber Software Foundation)對它提供技術(shù)支持。
我們之所以選擇 Jabber 而沒(méi)有選擇其他 IM 系統,是因為 Jabber 提供了廣泛的好處,包括它的:
為什么要把日志記錄到 IM 網(wǎng)絡(luò )?
日志記錄是開(kāi)發(fā)人員必須養成的良好編碼習慣,就像編寫(xiě)單元測試、處理異?;蛘呔帉?xiě) Javadoc 注釋一樣。插入到代碼中明確位置的日志記錄語(yǔ)句起著(zhù)審核工具的功能,提供了關(guān)于應用程序內部狀態(tài)的有用信息。與主流意見(jiàn)相反,我們認為在許多情況下,將日志語(yǔ)句保留在生產(chǎn)代碼中是方便的。如果 您擔心計算成本,就必須考慮從應用程序中刪除日志記錄功能所帶來(lái)的少量性能提升是否值得。此外,log4j 的靈活性允許您聲明式地控制日志記錄行為。您可以建立嚴格的日志記錄策略來(lái)降低日志的累贅性并改進(jìn)性能。
圖3顯示了 IMAppender 的一個(gè)使用場(chǎng)景:一個(gè)配置為使用 IMAppender 的 log4j 應用程序記錄 它的被包裝為 IM 消息的調試數據。即時(shí)消息通過(guò) Jabber 公司網(wǎng)絡(luò )被路由到系統管理員的Jabber 地址(注意,公開(kāi)可用的 Jabber 服務(wù)器對生產(chǎn)應用可能不足夠可靠)。因而,無(wú)論何時(shí)系統管理員需要檢查應用程序的狀態(tài),他們只需加載最喜歡的 Jabber 客戶(hù)機,然后連接到Jabber 服務(wù)器。如圖3所示,管理員可以通過(guò)不同的設備來(lái)訪(fǎng)問(wèn)。他可以使用辦公室的 PC 來(lái)登錄服務(wù)器,或者當他離開(kāi)辦公桌時(shí),可以使用運行在手持設備上的 Jabber 客戶(hù)機來(lái)檢查消息。

但是為什么需要 IM appender 呢?因為向 IM 服務(wù)器發(fā)送消息將允許您通過(guò)自由選擇的工具(比如Jabber客戶(hù)機)來(lái)更容易地監視應用程序行為。
IMAppender提供了多個(gè)優(yōu)點(diǎn):
SMTPAppender 發(fā)送的電子郵件則很困難。進(jìn)階IMAppender 模仿隨 log4j 一起分發(fā)的 SMTPAppender 的日志記錄策略。IMAppender 把日志記錄事件存儲在一個(gè)內部循環(huán)緩沖區(cyclic buffer)中,并且僅當所接收到的日志記錄請求觸發(fā)了某個(gè)用戶(hù)指定的條件時(shí),才把這些事件作為即時(shí)消息來(lái)發(fā)送?;蛘?,用戶(hù)也可以提供一個(gè)觸發(fā)事件鑒別器類(lèi)(triggering event evaluator class)。然而在默認情況下,消息傳送是由指定為 ERROR 或者更高級別的事件所觸發(fā)的。
每個(gè)消息中傳送的日志記錄事件的數量是由緩沖區的大小決定的。循環(huán)緩沖區僅保留最后的 bufferSize 個(gè)日志記錄事件,當它裝滿(mǎn)時(shí)就會(huì )溢出并丟棄較舊的事件。
為了連接到 Jabber 服務(wù)器,IMAppender 需要依賴(lài) Jive Software 公司的 Smack API。Smack 是一個(gè)開(kāi)放源代碼的高級庫,它處理與 Jabber 服務(wù)器通信的協(xié)議細節。這樣, 您無(wú)需任何特別的 Jabber 或者 XML 專(zhuān)業(yè)經(jīng)驗就能理解代碼。
IMAppender 的屬性總結在表 1中:
表 1. IMAppender 屬性
| 屬性 | 說(shuō)明 | 類(lèi)型 | 是否必需 |
| host | 服務(wù)器的主機名稱(chēng) | String | 是 |
| port | Jabber服務(wù)器的端口號 | int | 否,默認為 5222 |
| username | 應用程序的Jabber帳戶(hù)用戶(hù)名 | String | 是 |
| password | 應用程序的Jabber帳戶(hù)密碼 | String | 是 |
| recipient | 接收方的Jabber地址。Jabber地址也稱(chēng)為Jabber ID,它在一個(gè)@字符后面指定用戶(hù)的Jabber 域,就像電子郵件地址一樣 這個(gè)屬性可以保存任何聊天地址或者聊天室地址。例如,您可以指定這樣的聊天地址:sysadmin@company.com;或者 您可能希望向 | String | 是 |
| chatroom | 接受一個(gè)布爾值。如果為 true,recipient 值將被接受為小組聊天地址。如果要設置這個(gè)選項,還應該設置 nickname 選項。默認情況下,recipient 值被解釋為一個(gè)聊天地址 | boolean | 否,默認為 false |
| nickname | 僅當設置了chatroom 屬性時(shí)才會(huì )考慮這個(gè)屬性。否則,它將被忽略用戶(hù)可以選擇 appender 使用的任意小組聊天昵稱(chēng)來(lái)加入小組聊天。昵稱(chēng)不一定要和 Jabber用戶(hù)名有關(guān) | String | 否 |
| SSL | 用于保護與 Jabber 服務(wù)器的連接 | boolean | 否,默認為false |
| bufferSize | 可以保留在循環(huán)緩沖區中的日志記錄事件的最大數量 | int | 否,默認為16 |
| evaluatorClass | 這個(gè)屬性的值被當作一個(gè)類(lèi)的完全限定名稱(chēng)的字符串表示形式,該類(lèi)實(shí)現了 | String | 否,默認為 DefaultEvaluator |
現在讓我們進(jìn)一步觀(guān)察代碼。IMAppender 類(lèi)遵循清單3所示的結構:
清單 3. IMAppender 類(lèi)的總體結構 |
請注意關(guān)于我們的 appender 的如下幾個(gè)方面:
IMAppender 類(lèi)擴展 org.apache.log4j.AppenderSkeleton,這是所有自定義 appender 都必須要做的。IMAppender 從 AppenderSkeleton 繼承諸如 appender 閾值和自定義過(guò)濾之類(lèi)的公共功能。setHost() 和 getHost() 方法。requiresLayout()、activateOptions()、append() 和 close()。 log4j 框架調用 requiresLayout() 方法來(lái)判斷自定義 appender 是否需要 layout。注意 ,有些appender 使用內置格式或者根本就不格式化事件,因此它們不需要 Layout 對象。IMAppender 需要 layout,因而該方法返回 true,如 清單4所示:
|
注意,AppenderSkeleton 實(shí)現了 org.apache.log4j.spi.OptionHandler 接口(參見(jiàn) 圖 1 )。AppenderSkeleton 把這個(gè)接口的單個(gè)方法 activateOptions() 實(shí)現為一個(gè)空方法。我們的 IMAppender 需要這個(gè)方法是由于其屬性之間的相互依賴(lài)性。例如,與 Jabber 服務(wù)器的連接依賴(lài) Host、Port 和 SSL 屬性,因此 IMAppender 在這三個(gè)屬性被初始化之前無(wú)法建立連接。log4j 框架調用 activateOptions() 方法來(lái)通知 appender 所有屬性都已設置就緒。
IMAppender.activateOptions() 方法激活指定的屬性(比如 Jabber 主機、端口、bufferSize,等等),所采取的方式是實(shí)例化依賴(lài)這些屬性值的更高級對象,如清單5所示:
|
activateOptions() 方法完成以下任務(wù):
bufferSize 個(gè)事件的最大循環(huán)緩沖區。我們使用了 org.apache.log4j.helpers.CyclicBuffer 的一個(gè)實(shí)例,org.apache.log4j.helpers.CyclicBuffer 是 log4j 附帶的一個(gè)輔助類(lèi),它提供了緩沖區的邏輯。XMPPConnection 類(lèi)創(chuàng )建了一個(gè)到 XMPP (Jabber) 服務(wù)器的連接,這個(gè)服務(wù)器是通過(guò) host 和 port 屬性來(lái)指定的。為了創(chuàng )建一個(gè) SSL 連接,我們要使用 SSLXMPPConnection 子類(lèi)。username 和 password 屬性所定義的 Jabber 帳戶(hù)來(lái)登錄,同時(shí)調用 XMPPConnection.login() 方法。Chat 或者 GroupChat 對象,具體視 chatroom 值而定。 在 activateOptions() 方法返回之后,appender 就準備好處理日志記錄請求了。如 清單6所示,由 AppenderSkeleton.doAppend() 調用的 append() 方法將執行大多數實(shí)際的日志附加工作。
|
append() 方法中的第一個(gè)語(yǔ)句判斷進(jìn)行附加嘗試是否有意義。checkEntryConditions() 方法檢查是否有可用于附加到輸出的 Chat 或者 GroupChat 對象,以及是否有用于格式化傳入 event 對象的 Layout 對象。如果這些前提條件得不到滿(mǎn)足,那么 append() 將輸出一條警告消息并返回,從而不會(huì )繼續進(jìn)行輸出操作。下一個(gè)語(yǔ)句把事件添加到循環(huán)緩沖區實(shí)例 cb。然后,if 語(yǔ)句把日志記錄事件提交給 evaluator,這是一個(gè) TriggeringEventEvaluator 實(shí)例。如果 evaluator 返回 true,這意味著(zhù)該事件與觸發(fā)條件匹配,sendBuffer() 就會(huì )被調用。
清單7顯示了 sendBuffer() 方法的代碼:
|
sendBuffer() 方法把緩沖區的內容作為IM消息來(lái)發(fā)送。此方法逐項遍歷保留在緩沖區中的事件,同時(shí)調用 layout 對象的 format() 方法來(lái)格式化每個(gè)事件。事件的字符串表示形式被附加到 StringBuffer 對象。最后,sendBuffer() 調用 chat 或者 groupchat 對象的 sendMessage() 方法,把消息發(fā)送出去。
請注意以下幾點(diǎn):
AppenderSkeleton.doAppend() 方法(它調用 append())是經(jīng)過(guò)同步的,因此 sendBuffer() 已經(jīng)擁有 appender 的監視器。這使得我們不必在 cb 上執行同步操作。LoggingEvent 對象中的可拋出對象,自定義 appender 的開(kāi)發(fā)人員必須輸出包括在事件中的異常信息。如果 layout 忽略了可拋出的對象,那么 layout 的 ignoresThrowable() 方法應該返回 true,并且 sendBuffer() 可以使用 LoggingEvent.getThrowableStrRep() 方法來(lái)檢索包含在該事件中的可拋出信息的 String[] 表示形式。 下載源代碼 |
把全部?jì)热萁M合起來(lái)
下面將通過(guò)展示 IMAppender 的實(shí)際工作效果來(lái)結束本文的討論。我們將使用一個(gè)相當簡(jiǎn)單的名為 com.orangesoft.logging.example.EventCounter 的應用程序,如 清單8所示。這個(gè)示例應用程序在命令行接受兩個(gè)參數。第一個(gè)參數是一個(gè)整數,對應于要產(chǎn)生的日志記錄事件的數量。第二個(gè)參數必須是以屬性的格式提供的一個(gè) log4j 配置文件名。這個(gè)應用程序總是以 ERROR 事件結束,該事件將觸發(fā)一次 IM 消息傳送。
|
我們可以使用類(lèi)似清單9所示的配置文件:
清單 9. 示例 IMAppender 配置文件 |
上面的配置文件腳本把 IMAppender 添加到根日志記錄器(root logger),這樣所接收到的每個(gè)日志記錄請求都將被分派到我們的 appender。
在試驗這個(gè)示例應用程序之前,請確保將 host、username、password 和 recipient 屬性設置為 您所在環(huán)境中的適當值。下面的命令將運行 EventCounter 應用程序:
|
當運行時(shí),EventCounter 將根據 eventcounter.properties 所設置的策略記錄 100 個(gè)事件。然后一個(gè) IM 消息將從接收方的屏幕上彈出來(lái)。圖4、5、6 顯示了不同平臺上的 Jabber 客戶(hù)機接收到的結果消息:
圖 4. Windows (Rhymbox)上的 Jabber 客戶(hù)機接收到的消息的屏幕快照

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

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

注意 EventCounter 產(chǎn)生了 100 個(gè)事件。然而,由于 IMAppender 緩沖區的默認大小為 16,接收方應該收到僅包含最后 16 個(gè)事件的 IM 消息??梢钥吹?,包含在最后一個(gè)事件(消息和堆棧跟蹤)中的異常信息已經(jīng)被正確地傳送了。
這個(gè)例子應用程序只展示了 IMAppender 的一個(gè)非常小的用途,因此繼續探索它吧,您會(huì )找到很多樂(lè )趣的!
結束語(yǔ)
log4j 網(wǎng)絡(luò ) appender,SocketAppender、 JMSAppender 和 SMTPAppender 已經(jīng)提供了監視 Java 分布式應用程序的機制。然而,多個(gè)因素使得 IM 成為用于實(shí)時(shí)遠程日志記錄的合適技術(shù)。在本文中,我們介紹了通過(guò)自定義 appender 來(lái)擴展 log4j 的基礎知識,并看到了一個(gè)基本 IMAppender 的逐步實(shí)現過(guò)程。許多開(kāi)發(fā)人員和系統管理員都可以從 appender 的使用中獲益。
聯(lián)系客服