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

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

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

一些支持使用未經(jīng)檢測異常的人都認(rèn)為Sun應(yīng)該在所有的基于J2EE框架的EJB容器中加入“鉤子”。這樣就會允許定制錯誤處理、安全等方面的信息,而不必依靠信任度不高的供應(yīng)商提供的框架。遺憾的是,Sun在EJB規(guī)范中并沒有提供這樣的機(jī)制。所以我們只有選用AOP框架來完成這方面的任務(wù):
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;
}
}
現(xiàn)行的處理器是通過ConfigHelper類來完成配制和維護(hù)工作的。如果業(yè)務(wù)邏輯Bean運(yùn)行期間拋出運(yùn)行時(shí)異常或錯誤時(shí),處理器將會被要求來處理異常。
類DefaultEJBExceptionHandler將來自于Sun核心包以外的異常堆棧信息序列化入SerializableException,一方面客戶端不存在的類的異常堆棧信息能夠被傳遞出去,另一方面原始的異常信息會在傳遞的過程中丟失。
EJB容器能夠如實(shí)地將產(chǎn)生的運(yùn)行時(shí)異常(RuntimeException)和錯誤捕獲,并將它們封裝入java.rmi.RemoteException,如果客戶端是遠(yuǎn)程的,則將它們封裝入javax.ejb.EJBException。為了能夠準(zhǔn)確把握起因和跟蹤堆棧信息,框架在客戶端業(yè)務(wù)代理(BusinessDelegate)內(nèi)部剝離了傳遞過來的無用的異常信息,然后重新拋出最原始的錯誤信息。
“堡壘”中的BusinessDelegate類將“不可知的”EJB接口暴露給客戶端,同時(shí)將EJB本地和遠(yuǎn)程接口封裝在內(nèi)。BusinessDelegate類從EJB實(shí)現(xiàn)類中產(chǎn)生不同的XDoclet,按照下面的UML結(jié)構(gòu)圖:

類BusinessDelegate暴露出EJB實(shí)現(xiàn)類的所有業(yè)務(wù)方法,并將它們委派給相應(yīng)的LocalProxy類和RemoteProxy類。通過這樣兩個(gè)代理類來處理EJB相關(guān)的異常,因此屏蔽了調(diào)用BusinessDelegate類的實(shí)現(xiàn)細(xì)節(jié)。下面所示的代碼顯示了LocalProxy類的一些方法:
public java.lang.String someOtherMethod() {
try {
return serviceInterface.someOtherMethod();
} catch (EJBException e) {
BusinessDelegateUtil.throwActualException(e);
}
return null; // Statement is never reached
}
變量serviceInterface存放EJB本地接口的引用。任何一個(gè)由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;
}
}
實(shí)際的異常信息被重新提取并拋出到頂級客戶端異常處理器。當(dāng)異常到傳遞到處理器時(shí),堆棧信息將是服務(wù)器端所拋出的實(shí)際的錯誤信息。沒有多余的客戶端的附加信息被添加到里面。
Swing異常處理器
JVM為每一個(gè)控制線程提供缺省的頂級異常處理器。當(dāng)運(yùn)行的時(shí)候,該處理器將錯誤信息和異常信息堆砌到System.err,并殺掉該線程!對于用戶而言,上面的那些信息是無用的,從調(diào)試的角度來講,也帶來了很大的麻煩。我們需要這樣一種機(jī)制,當(dāng)我們?yōu)榱艘院蟮恼{(diào)試而保留堆棧信息和唯一的請求ID時(shí),允許我們給用戶做出提示。在“為J2EE創(chuàng)建應(yīng)用級別的用戶會話”一文中講述了在各層次的應(yīng)用中怎樣使請求ID變得有價(jià)值。
對于J2SE1.4來說,在線程實(shí)例中,未被捕獲的異常,會引起線程組自身的uncaughtException()方法被執(zhí)行。在應(yīng)用程式中控制異常處理的簡單方法就是擴(kuò)展類ThreadGroup,覆蓋uncaughtException()方法,并確保所有的線程實(shí)例都來自于定制的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()方法,傳遞當(dāng)前的線程實(shí)例并拋出Throwable到異常處理器中。
在我們提出控制客戶端的所有的游離異常之前,施點(diǎn)小小的魔法是必要的。為了計(jì)劃能夠?qū)嵤械木€程必須要實(shí)例化自我們定制的SwingThreadGroup。這是通過產(chǎn)生一個(gè)新的主線程實(shí)例并傳遞SwingThreadGroup實(shí)例來實(shí)現(xiàn)的。所有的線程實(shí)例來自于這個(gè)主線程實(shí)例,并自動加入SwingThreadGroup實(shí)例,因此,當(dāng)未經(jīng)檢測的異常被拋出時(shí)使用我們新的異常處理器來處理。

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

向單獨(dú)的J2SE應(yīng)用中加入異常處理與基于Web應(yīng)用是不同的。
WAR異常處理器
在J2EE架構(gòu)的眾多組件當(dāng)中,Web應(yīng)用組件是很幸運(yùn)的,可以有他們自己的方式來設(shè)置異常處理器。通過配置描述文件web.xml,異常以及HTTP錯誤能夠被映射到由Servlet或JSP頁面做成的錯誤頁面中。參考下面示例的web.xml文件片斷:
ErrorHandlerServlet
dk.rhos.fw.rampart.util.errorhandling.ErrorHandlerServlet
ErrorHandlerServlet
/errorhandler
java.lang.Throwable
/errorhandler
這些標(biāo)簽直接將所有未捕獲的異常定位到錯誤處理器,在這里被定位到ErrorHandlerServlet類。后者是一個(gè)目的明確的servlet,它的唯一角色就是Web組件與異常處理框架間的橋梁。當(dāng)一個(gè)來自于Web應(yīng)用的未捕獲異常到達(dá)servlet容器后,一套包含了異常信息的參數(shù)將被設(shè)置到HttpServletRequest實(shí)例中,然后傳遞給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實(shí)例的實(shí)際的異常是通過關(guān)鍵字javax.servlet.error.exception重新獲得的。第二,異常處理器實(shí)例是重新獲得的。在這個(gè)層面上,處理器被調(diào)用,HttpServletRequest實(shí)例被定位到關(guān)鍵字rampart.servlet.exception.responsepage所指定的頁面去。
類DefaultWARExceptionHandler查找異常信息中的國際化信息,并重定位輸出到JSP頁面error.jsp。然后該頁面自由的顯示信息給用戶,包括當(dāng)前的請求ID作為支持參考。更完善的機(jī)制能夠很容易地通過擴(kuò)展或替換處理器來實(shí)現(xiàn)。
封裝
通常情況下,異常處理并不是足夠的,因而調(diào)試和錯誤碼跟蹤變得復(fù)雜起來。因此,在系統(tǒng)開發(fā)開始時(shí)確定適當(dāng)?shù)牟呗院涂蚣苁侵陵P(guān)重要的。雖然在這方面做事后補(bǔ)就是可行的,不過時(shí)間的花費(fèi)是很驚人的。
本文僅是異常策略定義的一個(gè)起點(diǎn),僅是向您介紹了一個(gè)簡單,易于擴(kuò)展的未捕獲異常的繼承處理機(jī)制。通過一個(gè)示例的J2EE應(yīng)用演示了你應(yīng)該如何建立頂層的異常處理器來提供最后的防線。
快點(diǎn)下載源代碼,去嘗試一下吧,用心去體會,就會有收獲。
參考資料及源代碼
-
-
-