淺談J2EE架構下異常管理和錯誤跟蹤
(在J2EE環境下嘗試開發異常處理框架)
概述
回顧自己幾年來所做過的J2EE項目。你可曾遇到過錯誤沒有記錄或記錄不止一次的情形嗎?你是否曾花費數不盡的時間來追蹤BUG,而真正的原因是某些人在某個地方取消了異常處理?你的用戶是否能夠看到堆棧跟蹤信息?如果你有過這樣的經歷,機會來了,你可能需要一種管理異常的通用策略,以及一些補救代碼。
本文提供了在J2EE環境下開發一套策略連同錯誤處理框架的基礎,歡迎大家一起參詳。
前言
如果將Java領域中長久以來關于異常處理的辯論比作一場宗教戰爭,一點也不為過:一方面是,異常檢測的擁護者主張調用者始終都應該處理他們調用的代碼中所發生的錯誤情況。另一方面是,不檢測異常的追隨者指出異常檢測會使代碼變得混亂,并且經常不能夠立刻被客戶端處理,為什么還要強制進行呢?
作為一個初級程序員,我們首先是異常檢測的簇擁者,但過了幾年,在書寫了很多,很多try-catch塊后,我們會逐漸地將我們的“信仰”轉變為后者。為什么會這樣?我們形成了一套簡單的處理錯誤情況的規則:
u 如果處理異常是有意義的,那么就做吧。
u 如果不處理異常,那么拋出它。
u 如果你不拋出它,那把它封裝在一個不檢測地基類異常中,然后拋出它。
但是對于那些上面沒有提及的異常怎么處理呢?對于那些異常,我們建立最后的防線以確保錯誤信息能夠被記錄并合理地提示給用戶。
本文向大家展示了另外一個異常處理框架,它源自“為J2EE創建應用級別的用戶會話”一文所提及的靈巧的應用級用戶會話(詳細信息請參考該文)。J2EE應用使用該框架會:
u 對用戶總是產生有意義的錯誤信息。
u 對沒有處理的錯誤記錄日志一次,且僅記錄一次。
u 同一請求ID的相關異常記錄在日志中,以便于更精確的跟蹤調試信息。
u 對于所有層次提供一個完善的、易于擴展的,卻又簡單的策略機制。
為了更好的完成這個框架,我們有效地利用了AOP,設計模式,并且用XDoclet來產生代碼。
為什么我們需要通用錯誤處理機制?
在項目初始階段,決定架構是很有意義的:軟件的各單元如何有效地互相作用?會話狀態信息存放在何處?采用何種通訊協議?等等。然而時常會出現這樣的情況,錯誤處理策略卻未被包括在內。因而,一些隨機的策略被實現了,隨之而來的,每個開發人員都可以任意地決定錯誤信息如何定義,分類,模塊化,以及處理。作為一名工程師,你當然最希望能夠重新認識這種策略所能帶來的結果:
u 冗余的日志:每一個try-catch塊包含一條日志記錄,多余地日志項目會“玷污”源程式。
u 多余地實現:同樣類型的錯誤有不同的展示形式,使最終的處理變得復雜。
u 破壞封裝:異常定義作為組件方法的一部分,打破了接口與實現之間的清晰界限。
u 混淆的異常定義:方法僅聲明拋出java.lang.Exception異常。這樣的話,客戶端將不能確認異常相關地最細節的線索。
對于沒有定義策略的通常解釋就是“Java已經提供了錯誤處理”。這是事實,但Java也提供了一系列的方法來定義、傳遞、響應錯誤信息的方法。決定在實際的項目中如何組織這些方法是編程人員的責任。一些決定是必須要做的,包括以下:
u 應不應該檢查:新的異常是否應該檢查?
u 錯誤信息的消費者:當一沒有處理的錯誤存在時誰需要知道,誰來負責記錄日志并對操作人員進行提示?
u 基本的異常繼承機制:什么樣的異常信息應該被傳送,什么樣的異常語義可以通過繼承反射機制來處理?
u 異常信息傳遞:定義的未經處理的異常或是傳遞到其它異常類中的異常,如何在分布式環境中傳遞?
u 解釋:如何將未經處理的異常信息變為人們易讀的,甚至是多語言的信息?
將規則封裝入框架,但很倉促!
建造一個通用的異常處理策略,我們提供的“處方”包括以下幾方面因素:
u 使用未經檢測的異常:使用檢測的異常,客戶端不得不接受他們很少能夠處理的錯誤。對于未經檢測的異常則留給客戶做出選擇。當使用第三方的類庫時,你不能控制異常是否被檢測。在這種情況下,你需要封裝未經檢測的異常來捕獲檢測的異常。使用未經檢測最大的權衡就是你再也不能強制客戶端去處理它們了。然而,當被定義為接口的一部分時,他成為契約的關鍵部分,并繼而成為Javadoc文檔的一部分。
u 封裝錯誤處理并在頂層加入一個錯誤處理器:有了安全的網絡環境,就可以只關注于業務邏輯相關的異常信息。由處理器在指定層次按照標準的步驟(記錄日志、系統提示、信息轉換,等等)處理剩余的異常就可以完成一個優美的“本壘打”。
u 使用簡易接口來組織異常繼承機制:即使發現了新的異常也不要自動創建新的異常類。如果您簡單地應付其它類型的變異異常,如果客戶端能夠明確地捕獲它,問問自己,那不是很好。要記得異常是具有屬性的,至少在某種程度上能夠模擬不同狀態的對象。
u 給終端用戶傳遞有用的信息:未經處理的異常包含了不可預知的事件和錯誤。提示用戶并將細節記錄下來提供給技術人員。
盡管項目進程中需求、限制、異常繼承、通知機制等可能會不同,但有些元素卻是持久的。為什么我們不實現一個通用的策略框架,達到一勞永逸的效果呢?這個框架遵循簡單易用的原則,與開發人員有著很好的交互接口,并且易于安裝(使用jar格式)又有很好的文檔(使用javadoc格式),不是很好嗎?
但是,你不能要求開發小組延期錯誤處理直到策略和框架完全準備好。錯誤處理必須被確定,當第一個源文件被創建的同時。一個好的開始來自于定義一個基礎的異常繼承機制。
基本的例外層次
我們的實用任務是定義能夠橫跨項目的通用異常繼承機制。基類是我們自己的未經檢測的異常類UnrecoverableException,這樣命名是由于歷史的原因,可能會引入些許歧義。你也許可以考慮為你自己的類繼承起一個更好的名字。
當你想要擺脫被檢測的異常時,可以想象的一種狀況是,客戶端總能夠處理這類異常。WrappedException提供了一般的簡單傳送機制:封裝并拋出。WrappedException保留了異常起因作為內部引用,當原始異常類仍可用時能夠運行的很好。當這不是實際情況時,使用SerializableException,它類似于WrappedException,除了假設客戶端沒有類庫時仍能被使用外。
盡管我們更喜歡和推薦使用未經檢測的異常,你可能仍保留使用被檢測異常的選擇。InstrumentedException接口作為一個仿效屬性模式實現的接口,應用于被檢測的和未經檢測的異常。
下面的類圖顯示了我們基本的異常繼承機制。

到目前為止,我們已經有了策略和一套待拋出的異常。現在是建立一個安全網絡環境的時候了。
最后的防線
“為J2EE創建應用級別的用戶會話”一文中為我們展示了一個“堡壘”,企業信息系統由一個多層的架構組成,業務邏輯層由無狀態消息Bean驅動,客戶層即可以是Web應用,也可以是獨立的應用程式。在該框架中異常可以在任何一層被拋出,也可以被處理或直到調用的結尾才被處理。J2SE和J2EE服務器都能保證自己免受那些“迷離”錯誤以及RuntimeExceptions的干擾,通過將堆棧中的異常輸出到Systom.out控制臺中,記錄日志,或者是執行一些其它的缺省操作。無論如何,如果用戶獲得了任何類型的輸出,通常情況下,它被證明是完全沒有意義的,更糟的情況是,錯誤很有可能會破壞程式本身的穩定。我們必須設置自己的堡壘以提供更健全的異常處理機制作為最后的一道防線。

異常可能存在于服務器端的EJB層和Web應用層,或者是在單獨的應用程式中。一種情況是,虛擬機中產生的異常按照自己的方式傳遞到Web應用層。這就是我們安裝頂層異常處理器的地方。
另外一種情況是,異常最終到達EJB容器的邊緣,通過RMI連接到客戶層。特別要注意的是,不要將那些服務器端專有的異常信息傳送到客戶端。例如,來自于框架的對象關系映射之類的。EJB異常處理器通過SerializableException類來完成處理責任。在客戶層面,頂層的Swing異常處理器可以捕獲任意一個游離的錯誤并采取相應的處理。
異常處理框架
“堡壘”框架中的異常處理器類實現了ExceptionHandler接口。該接口僅有一個帶有兩個參數的方法:當前的線程Thread和需要處理的異常Throwable。為了方便起見,框架提供了一個缺省的實現ExceptionHandlerBase,用戶只要繼承該類,提供抽象方法的實現就可以了。
下面的類圖顯示異常處理器的繼承關系。

一些支持使用未經檢測異常的人都認為Sun應該在所有的基于J2EE框架的EJB容器中加入“鉤子”。這樣就會允許定制錯誤處理、安全等方面的信息,而不必依靠信任度不高的供應商提供的框架。遺憾的是,Sun在EJB規范中并沒有提供這樣的機制。所以我們只有選用AOP框架來完成這方面的任務:
public class EJBExceptionHandler implements AroundAdvice {
private ExceptionHandler handler;
public EJBExceptionHandler() {
handler = ConfigHelper.getEJBExceptionHandler();
}
public Object invoke(JoinPoint joinPoint) throws Throwable {
Log log = LogFactory.getLog(joinPoint.getEnclosingStaticJoinPoint()
.getClass().getName());
log.debug("EJB Exception Handler bean context aspect!!");
try {
return joinPoint.proceed();
} catch (RuntimeException e) {
handler.handle(Thread.currentThread(), e);
} catch (Error e) {
handler.handle(Thread.currentThread(), e);
}
return null;
}
}
現行的處理器是通過ConfigHelper類來完成配制和維護工作的。如果業務邏輯Bean運行期間拋出運行時異常或錯誤時,處理器將會被要求來處理異常。
類DefaultEJBExceptionHandler將來自于Sun核心包以外的異常堆棧信息序列化入SerializableException,一方面客戶端不存在的類的異常堆棧信息能夠被傳遞出去,另一方面原始的異常信息會在傳遞的過程中丟失。
EJB容器能夠如實地將產生的運行時異常(RuntimeException)和錯誤捕獲,并將它們封裝入java.rmi.RemoteException,如果客戶端是遠程的,則將它們封裝入javax.ejb.EJBException。為了能夠準確把握起因和跟蹤堆棧信息,框架在客戶端業務代理(BusinessDelegate)內部剝離了傳遞過來的無用的異常信息,然后重新拋出最原始的錯誤信息。
“堡壘”中的BusinessDelegate類將“不可知的”EJB接口暴露給客戶端,同時將EJB本地和遠程接口封裝在內。BusinessDelegate類從EJB實現類中產生不同的XDoclet,按照下面的UML結構圖:

類BusinessDelegate暴露出EJB實現類的所有業務方法,并將它們委派給相應的LocalProxy類和RemoteProxy類。通過這樣兩個代理類來處理EJB相關的異常,因此屏蔽了調用BusinessDelegate類的實現細節。下面所示的代碼顯示了LocalProxy類的一些方法:
public java.lang.String someOtherMethod() {
try {
return serviceInterface.someOtherMethod();
} catch (EJBException e) {
BusinessDelegateUtil.throwActualException(e);
}
return null; // Statement is never reached
}
變量serviceInterface存放EJB本地接口的引用。任何一個由EJB容器拋出的,表明不可期的錯誤信息的EJBException異常都由BusinessDelegateUtil類捕獲和處理,如下面所示:
public static void throwActualException(EJBException e) {
doThrowActualException(e);
}
private static void doThrowActualException(Throwable actual) {
boolean done = false;
while(!done) {
if(actual instanceof RemoteException) {
actual = ((RemoteException)actual).detail;
} else if (actual instanceof EJBException) {
actual = ((EJBException)actual).getCausedByException();
} else {
done = true;
}
}
if(actual instanceof RuntimeException) {
throw (RuntimeException)actual;
} else if (actual instanceof Error) {
throw (Error)actual;
}
}
實際的異常信息被重新提取并拋出到頂級客戶端異常處理器。當異常到傳遞到處理器時,堆棧信息將是服務器端所拋出的實際的錯誤信息。沒有多余的客戶端的附加信息被添加到里面。
Swing異常處理器
JVM為每一個控制線程提供缺省的頂級異常處理器。當運行的時候,該處理器將錯誤信息和異常信息堆砌到System.err,并殺掉該線程!對于用戶而言,上面的那些信息是無用的,從調試的角度來講,也帶來了很大的麻煩。我們需要這樣一種機制,當我們為了以后的調試而保留堆棧信息和唯一的請求ID時,允許我們給用戶做出提示。在“為J2EE創建應用級別的用戶會話”一文中講述了在各層次的應用中怎樣使請求ID變得有價值。
對于J2SE1.4來說,在線程實例中,未被捕獲的異常,會引起線程組自身的uncaughtException()方法被執行。在應用程式中控制異常處理的簡單方法就是擴展類ThreadGroup,覆蓋uncaughtException()方法,并確保所有的線程實例都來自于定制的ThreadGroup類。
我們的框架基于J2SE1.3,采用了繼承ThreadGroup的方法:
private static class SwingThreadGroup extends ThreadGroup {
private ExceptionHandler handler;
public SwingThreadGroup(ExceptionHandler handler) {
super("Swing ThreadGroup");
this.handler = handler;
}
public void uncaughtException(Thread t, Throwable e) {
handler.handle(t, e);
}
}
上面代碼片斷中所示的SwingThreadGroup類,覆蓋了uncaughtException()方法,傳遞當前的線程實例并拋出Throwable到異常處理器中。
在我們提出控制客戶端的所有的游離異常之前,施點小小的魔法是必要的。為了計劃能夠實施,所有的線程必須要實例化自我們定制的SwingThreadGroup。這是通過產生一個新的主線程實例并傳遞SwingThreadGroup實例來實現的。所有的線程實例來自于這個主線程實例,并自動加入SwingThreadGroup實例,因此,當未經檢測的異常被拋出時使用我們新的異常處理器來處理。

框架在工具類SwingExceptionHandlerController中實現了該邏輯。應用提供了SwingMain接口的實現并傳遞異常控制器到其中。控制器必須被啟動,舊的主線程才能夠加入新的主線程并等待中斷信號。下面的代碼顯示了示例應用如何完成既定的任務。方法createAndShowGUI()構成了應用的實體,完成初始化Swing組件和傳送控制信息到用戶端的任務。
public DemoApp() {
SwingExceptionHandlerController.setHandler(new DefaultSwingExceptionHandler());
SwingExceptionHandlerController.setMain(new SwingMain() {
public Component getParentComponent() {
return frame;
}
public void run() {
createAndShowGUI();
}
});
SwingExceptionHandlerController.start();
SwingExceptionHandlerController.join();
}
現在是時候在Swing層建立最后的防線了,但我們仍需要提供有意義的信息給用戶。示例應用提供了最基本的實現,僅僅簡單地通過對話框顯示國際化的信息和唯一的請求ID作為有效的幫助。同時,異常信息通過唯一的請求ID被log4j記錄到日志中。
更完善的錯誤處理可能會發送電子郵件,SNMP信息,或者提供請求ID相關的技術支持等等。最重要的一點是客戶端和服務器端的日志都能夠被過濾通過唯一的請求ID,對每一個請求ID提供最精確的錯誤信息。
圖表6顯示了對于請求ID lcffeb4:feb53del38:-7ffb Swing客戶端和J2EE服務器日志提供的精確的跟蹤記錄,顯示了異常是在何處被拋出的。注意堆棧跟蹤包含的僅僅是服務器端異常的拋出信息。

向單獨的J2SE應用中加入異常處理與基于Web應用是不同的。
WAR異常處理器
在J2EE架構的眾多組件當中,Web應用組件是很幸運的,可以有他們自己的方式來設置異常處理器。通過配置描述文件web.xml,異常以及HTTP錯誤能夠被映射到由Servlet或JSP頁面做成的錯誤頁面中。參考下面示例的web.xml文件片斷:
ErrorHandlerServlet
dk.rhos.fw.rampart.util.errorhandling.ErrorHandlerServlet
ErrorHandlerServlet
/errorhandler
java.lang.Throwable
/errorhandler
這些標簽直接將所有未捕獲的異常定位到錯誤處理器,在這里被定位到ErrorHandlerServlet類。后者是一個目的明確的servlet,它的唯一角色就是Web組件與異常處理框架間的橋梁。當一個來自于Web應用的未捕獲異常到達servlet容器后,一套包含了異常信息的參數將被設置到HttpServletRequest實例中,然后傳遞給ErrorHandlerServlet類的service方法。下面的代碼示例了service()方法:
...
private static final String CONST_EXCEPTION =
"javax.servlet.error.exception";
...
protected void service( HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws ServletException, IOException
{
Throwable exception =
(Throwable)httpServletRequest.getAttribute(CONST_EXCEPTION);
ExceptionHandler handler = ConfigHelper.getWARExceptionHandler();
handler.handle(Thread.currentThread(), exception);
String responsePage = (String)ConfigHelper.getRequestContextFactory().
getRequestContext().
getAttribute(ExceptionConstants.CONST_RESPONSEPAGE);
if(responsePage == null) {
responsePage = "/error.jsp";
}
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
RequestDispatcher dispatcher =
httpServletRequest.getRequestDispatcher(responsePage);
try {
dispatcher.include(httpServletRequest, httpServletResponse);
} catch (Exception e) {
log.error("Failed to dispatch error to responsePage " + responsePage, e);
}
}
在service()方法中,第一,來自于HttpServletRequest實例的實際的異常是通過關鍵字javax.servlet.error.exception重新獲得的。第二,異常處理器實例是重新獲得的。在這個層面上,處理器被調用,HttpServletRequest實例被定位到關鍵字rampart.servlet.exception.responsepage所指定的頁面去。
類DefaultWARExceptionHandler查找異常信息中的國際化信息,并重定位輸出到JSP頁面error.jsp。然后該頁面自由的顯示信息給用戶,包括當前的請求ID作為支持參考。更完善的機制能夠很容易地通過擴展或替換處理器來實現。
封裝
通常情況下,異常處理并不是足夠的,因而調試和錯誤碼跟蹤變得復雜起來。因此,在系統開發開始時確定適當的策略和框架是至關重要的。雖然在這方面做事后補就是可行的,不過時間的花費是很驚人的。
本文僅是異常策略定義的一個起點,僅是向您介紹了一個簡單,易于擴展的未捕獲異常的繼承處理機制。通過一個示例的J2EE應用演示了你應該如何建立頂層的異常處理器來提供最后的防線。
快點下載源代碼,去嘗試一下吧,用心去體會,就會有收獲。
參考資料及源代碼
-
-
-