J2EE中的異常管理及錯誤跟蹤
-為j2ee定制一個用來處理錯誤的異常處理框架
作者:Kåre Kjelstrøm/Jens Schjærff Byager
翻譯:xMatrix
版權聲明:可以任意轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
原文地址:
http://www.javaworld.com/javaworld/jw-07-2005/jw-0711-exception.Html
中文地址:
http://www.matrix.org.cn/resource/article/43/43744_J2EE_Exception.html
關鍵詞: J2EE Exception
摘要
回顧一下你上一個J2EE工程,是否遇到過類似錯誤沒有記入日志或者被多次記錄的情況?是否只是因為在某處代碼吃掉了異常導致你花費無數次時間來跟蹤一個bug?是否你的用戶直接看到了堆棧的跟蹤信息?如果這樣的話,你可能需要一種通用的異常管理的策略和一些補充的代碼。這篇文章為你提供了在J2EE項目中通過使用錯誤處理框架使用一些策略的基礎。(3100個英文單詞,2005年7月11日)
Java中關于異常處理的爭論可以被認為是一種信仰上的爭執:一方面,強制異常(checked exceptions)的支持者認為調用者應該處理他們調用代碼出現的異常;另一方面,非強制(unchecked exceptions)異常的追隨者認為強制異?;靵y了代碼,而且通??蛻舳瞬荒芰⒓刺幚?,那為什么還要檢查他呢。
作為初級工程師,我們首先信奉的是強制異常,但幾年后,在使用N久的try/catch/finally后,我們開始轉向非強制異常了。因為我們開始相信一些處理錯誤狀況的基本規則:
如果需要處理異常,那么就處理
如果處理不了,就拋出
如果拋不了,就用非強制的基類異常包裝后再拋出
但這些異常被拋到最頂層時會怎么樣呢?對這種情況,我們有一個底線確保錯誤信息被記錄并且用戶得到正確的提示。
本文提供了另外一種框架來處理異常,它擴展了“Create an application-Wide User session for J2EE”(JavaWorld, 2005年3月)所提出的企業應用session工具。使用此框架的J2EE應用將:
總是向用戶提供有意義的錯誤信息
記下未處理的錯誤環境,并且只記錄一次
在日志文件中用唯一的請求ID號對異常進行編號,以便進行高精度的調試
在各層中設置一個強壯的、可擴展的,而又簡單的策略來處理異常
為了搭建框架,我們運用了面向狀態編程(AOP,aspect-oriented PRogramming)、設計模式和使用XDoclet進行代碼生成。
你可以在資源中找到所有代碼及一個使用框架的J2EE應用。這些源程序組成了一個名為Rampart的完整框架,當初是為丹麥哥本哈根基于J2EE的電子保健系統應用(EHR, electronic healthcare records)而開發的。
為什么我們需要通用的錯誤處理方法
在項目的開始,我們會做一些關鍵性的系統架構決定,如:系統中的元素如何交互?會話狀態保存在哪兒?哪種通信協議會被使用等等。但這里并沒有包含錯誤處理。因而每個開發人員都可以任意決定如何定義、分類、建模和處理錯誤。作為一個開發人員,你可以想象在這種方式下的結果:
1. 臃腫的日志:每個try/catch都包含log語句,這導致被污染的代碼生成臃腫和多余的日志入口。
2. 多余的實現:同一類型的錯誤有不同的表示,這導致處理的復雜化。
3. 破碎的封裝:來自其他組件的異常被定義為方法標識的一部分,這導致接口和實現的分離被打破了。
4. 不明確的異常定義:方法簽名通常采用拋出java.lang.Exception,這導致客戶端不能明確得到方法錯誤的語義。
通常沒有定義異常處理策略的借口是:java已經提供了異常處理。這是事實,java也提供一貫的定義、通信、傳播及響應異常的工具。但開發人員需要決定如何在實際的項目中使用這些服務。幾個方面是必須要考慮的,如:
1. 檢查或不檢查異常:是否應該檢查或不檢查新異常類?
2. 異常的使用者:究竟是誰需要知道什么時候會發生未處理的異常及由誰來負責記錄及通知操作人員?
3. 基礎的異常層次:異常需要包含什么信息及異常層次需要反映什么語義?
4. 傳遞;是否未處理的異常會被定義或傳遞給別的異常類,及他們如何在分布式環境中傳遞?
5. 解釋:未處理的異常如何被解釋為可閱讀的,甚至支持多語言的信息?
在框架中封裝規則,要快!
我們給出的通用異常處理策略是基于如下的因素:
使用非強制的異常:使用強制異常,調用者要被迫處理他們幾乎不能處理的錯誤。非強制的異常則給調用者一個選擇。在使用第三方類庫時,你不能控制異常是強制或非強制的。這種情況下,你需要用非強制異常來包含強制異常。在使用非強制異常時,最大的讓步是你不能再強制調用者來處理異常了。然而作為接口定義的一部分,異常仍是約定的關鍵部分并且繼續成為Javadoc文檔的一部分。
封裝異常處理并在每一層的頂層提供處理器:你可以專注于只處理業務邏輯相關的異常。處理器可以為特定層剩余的異常執行標準操作:記錄日志、系統管理提示及轉換等等。
通過“簡單生活”方式來建模異常類層次:不要在發現新的錯誤類型時就創建新的異常類。首先問一下是否可以作為其他類型的變體來對待或者調用者確實需要捕獲。記住異常至少在某方面是可以用他的屬性來為不同的狀況建立變化模型的對象。較少的異常類在開始時是足夠的,但也僅在這種情況下可能需要用特定屬性來處理。
提供有意義的信息給使用者:未處理的異常代表不可預知的事件和問題。告訴用戶并且保存細節給技術支持人員。
雖然在不同的項目中需求、限制、異常層次及通知機制會有所不同,但許多元素還是一致的。因此為什么不完全地通過框架實現通用的策略呢?依據簡單使用原則的框架是強制使用策略的最好方法。通過jar文件與javadoc之類的可執行工件與開發人員對話比白紙和幻燈片更容易表示架構準則。
然而,你不能要求開發團隊直到異常處理策略及附加的框架支持準備完畢后才開始錯誤處理。錯誤處理必須在第一個源文件創建時確定。一個好的啟動方法是定義基礎的異常層次。
基礎異常層次
我們首要的任務是定義一個可以跨項目的通用異常層次。這里的非強制異常基類是UnrecoverableException,由于歷史原因,這個名字可能會有些誤導。你可以在自己的層次中使用更好的名字
當你不想使用強制異常時WrappedException可以提供一種簡單通用的傳送機制:包裹原來的異常并重新拋出。WrappedException保存原始異常作為內部引用,這使得當類需要原始異常時也可以可以正常工作。當這不重要時,你可以使用SerializableException,他類似于WrappedException,此外還可以在客戶端沒有對類庫作任何假設的情況下使用。
雖然我們偏好和推薦非強制異常,但你可以保留強制異常作為可選項。InstrumentedException是一個支持強制非強制異常的接口,他遵循一定屬性實現模式。他允許異常處理者一致地檢查來源頁不需要考慮是來自強制或非強制的異常。
下面的類圖顯示了我們基礎的異常層次。

這時候我們已經擁有了一個策略及相應的一組可以被拋出的異?!,F在是時候建立安全網了。
防守的底線
“創建應用范圍的用戶會話”這篇文章描述了Rampart,一個使用了由企業信息系統層,基于無狀態會話bean的業務層及基于網頁和標準J2SE客戶端的客戶層的分層架構。異常可以從任意層次拋出,可以在線處理或者延遲到調用鏈的最終端。J2SE和J2EE應用服務器都可以通過捕獲未處理的Errors和RuntimeExceptions來抵御侵入性的行為,通過輸出棧信息、記入日志或者執行其他默認的操作。在任何情況下,用戶都不應該看到輸出信息,通常是沒有意義的甚至影響程序穩定性的錯誤。因此我們必須構建自己的壁壘來提供更好的異常處理機制來維持這一防守的底線
看一下圖2:

異??赡馨l生在EJB層的服務端和網頁層,甚至獨立的客戶端。在第一種情況下,異常停留在同一VM中,也可能被傳送到網頁層。這兒就是我們要安裝的頂層異常處理器的地方。
在后一種情況下,異常發生在EJB容器的邊緣并且通過RMI連接傳遞到客戶端。必須注意不要傳送任何屬于服務端類的異常(如來自對象關系映射框架這類的)到客戶端。而由EJB異常處理器通過使用SerializableException作為中介來處理這個問題。在客戶端,頂層的Swing異常處理器捕獲其他未處理的錯誤并采取相應措施。
異常處理框架
在Rampart框架中異常處理器是一個實現了ExceptionHandler接口的類。這個接口僅有一個包含兩個參數(待處理的Throwable和當前的Thread)的方法。方便起見,框架提供了包含基本的實現類ExceptionHandlerBase,他辨別Throwable并將其代理給RuntimeException, Error, Throwable和Rampart框架的Unrecoverable的特定的抽象方法來處理。子類提供這些方法的實現并區別處理。
下面的類圖顯示了異常處理器的層次和三個缺省的異常處理器。
點擊查看大圖
許多人認為SUN應該在每應用的基礎上給J2EE框架內置插入所有容器的鉤子。這樣就允許自定義錯誤處理方案、安全及更多可安裝的功能,而不需要依賴特定廠商的方案和框架。不幸地是,SUN并沒有在EJB規范中提供這樣的機制。既然如此,我們只有拿出AOP這個強有力的工具來增加異常處理。我們選擇的AspectWerkz框架,可以如下使用方面:
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類來配置和獲取的。如果RuntimeException 或者Error在業務邏輯處理過程被拋出時,處理器就會被請求處理了。
DefaultEJBExceptionHandler序列化任何并非來自SUN核心包異常的堆棧信息到專門的SerializableException中,這從好的方面來看,可以將在遠程客戶端不存在的類的異常的堆棧以任意方式傳播,另一方面,這會丟失原始的異常。
如果客戶端是遠程的,EJB容器忠實地捕獲RuntimeException或Error并將他包在java.rmi.RemoteException中,否則使用javax.ejb.EJBException。為了在最低程度保持來源的精確性及堆棧信息,框架在BusinessDelegates剝離傳送異常并重新拋出原始異常。
Rampart框架的BusinessDelegate類提供一個EJB無關的接口給客戶端,而在內部包含本地或遠程的EJB接口。BusinessDelegate類從EJB實現類中用XDoclet生成的,他遵循圖4中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本地接口。任何被容器拋出的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;
}
}
actual異常被摘出并重新被拋出給頂層的客戶端異常處理器。當異常到達處理器時,堆棧信息會是來自服務端且包含實際錯誤的原始異常。沒有多余的客戶端信息被附加。
Swing異常處理器
JVM為每一個控制線程提供了缺省的頂層異常處理器。在異常發生時,處理器輸出Error或RuntimeException的堆棧信息到System.err并且結束線程。這種處理行為與用戶的要求相差很遠而且從調試的觀點來看也不是很優雅。我們需要一種機制在保存堆棧信息和為以后調試準備的唯一請求ID的同時允許通知用戶?!皠摻ɑ贘2EE的應用范圍用戶會話”描述了如何在所有層都可以形成這樣的請求ID。
J2SE1.4以前的版本,在Thread實例中未捕獲的異常將導致其所在的ThreadGroup的uncaughtException()方法被執行。在應用中控制異常處理的簡單方法是繼承ThreadGroup類,重寫uncaughtException()方法,并且確認所有Thread在自定義的ThreadGroup類的實例中啟動。
J2SE5提供了一種更方便的機制允許在Thread類的實例中安裝UncaughtExceptionHandler實現。處理器在未捕獲的異常到達Thread實例的運行方法中通過回調機制起作用。我們的框架基于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()方法并傳遞Thread實例及拋出Throwable給配置的異常處理器。
在我們在客戶端層控制所有未處理的異常之前還需要做些技巧性的工作。為了使用這個方案有效,所有線程必須與我們的SwingThreadGroup實例關聯。這可以通過生成一個主Thread實例并且通過Runnable實現傳遞SwingThreadGroup實例,這樣就可以執行整個程序了。所有來自這個新的主Thread實例的Thread實例自動加入SwingThreadGroup實例,因此當非強制異常被拋出時會觸發新的異常處理器。
點擊查看大圖
如圖5中框架在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。一個更復雜的錯誤處理器可以發送email、SNMP信息或者包含請求ID的技術支持。關鍵的是客戶端及服務端日志可以用請求ID來過濾從而使基于每個請求的定位更精確。

圖6顯示合并的Swing客戶端及J2EE服務端日志為請求ID為1cffeb4:feb53del38:-7ff6提供精確的定位。注意堆棧信息僅包含來自服務端的信息,而異常就是來自那兒。
雖然為獨立的J2SE應用增加異常處理的底層框架是基礎的,但當我們移植到基于網頁客戶端時還是需要作些改變。
WAR異常處理器
網頁應用在J2EE開發中是比較幸運的,擁有自己安裝異常處理的能力。通過web.xml配置描述文檔,異常和HTTP錯誤可以在servlets或jsp中映射到錯誤頁面。看一下下面的來自web.xml文檔中的示例片斷:
<servlet>
<servlet-name>ErrorHandlerServlet</servlet-name>
<servlet-class>dk.rhos.fw.rampart.util.errorhandling.ErrorHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ErrorHandlerServlet</servlet-name>
<url-pattern>/errorhandler</url-pattern>
</servlet-mapping>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/errorhandler</location>
</error-page>
這些標記指示所有未處理的異常會轉到/errorhandler這個URL去,在這里就是映射到ErrorHandlerServlet類。這是一個專門的servlet用來作為網頁組件與異常處理框架之間的橋梁。當來自網頁應用的未處理異常到達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類查找異常信息所對應的國際化信息并重定向輸出到/error.jsp。然后這個頁面顯示信息給用戶,包括當前的請求ID。更復雜的機制可以通過簡單地擴展或代替這個處理器來實現。
小結
通常異常處理并沒有被迅速地處理掉,因而復雜的調試和錯誤信息在許多時候破壞了整個用戶感受。因此在系統開發啟動之前準備好異常處理的策略和框架是重要的。在開發完成后再進行修補也是可行的,但代價要昂貴的多。
這篇文章給了你定義異常策略的起點,并且介紹給你一個簡單但可擴展的非強制異常層次。我們已經通覽了一個示例J2EE架構的業務和客戶端層,并且告訴你如何安裝頂層的異常處理器來提供一個防衛的底線??蚣艽a也告訴你一種通過附加給異常和日志入口的唯一請求ID的方式來確定基于每用戶請求的確切錯誤。
你可以下載這個框架,嘗試一下,并根據你的需求來更改并在你控制下獲取異常。
關于作者
Jens Byager是斯堪的納維亞北歐地區最大的IT服務公司TietoEnator保健部門的首席顧問。他也有在銀行、電信、后勤方面作為軟件開發人員及架構師的背景。他擁有Niels Brock Copenhagen商業大學計算機科學的博士士學位和丹麥Aalborg大學計算機科學碩士學位。
Kåre Kjelstrøm是一家丹麥顧問公司Silverbullet的創始人,主要幫助設計和構建企業級應用。他曾經在Trifork從事有關EAS J2EE應用服務器開發和作為J2EE顧問為當地的商業和保健業工作。這些年,他居住在硅谷,從事電子商務過程自動化、Web services管理及J2EE應用服務器相關工作。他擁有丹麥Aarhus大學計算機科學學士和碩士學位。他的Weblog是Drops of a Vapor Trail。
資源
·下載本文示例代碼:
http://www.javaworld.com/javaworld/jw-07-2005/exception/jw-0711-exception.zip
核心J2EE模式:業務代理(SUN開發網絡):
http://java.sun.com/blueprints/corej2eepatterns/Patterns/BusinessDelegate.html
· XDoclet:
http://xdoclet.sourceforge.net/xdoclet/index.html
·AspectWerkz:
http://aspectwerkz.codehaus.org/
·Maven:
http://maven.apache.org/
·Servlet規范:
http://java.sun.com/prodUCts/servlet/download.html
·Rampart框架:
http://213.237.179.87/projects/rampart/index.html
·"創建J2EE應用范圍用戶會話" Kåre Kjelstrøm (JavaWorld, March 2005):
http://www.javaworld.com/javaworld/jw-03-2005/jw-0314-usersession.html
·更多在JavaWorld中的異常處理文章:
o"Patch an Exception-Handling Framework," Niranjan R. Kamath (March 2005)為異常處理框架打補丁:
http://www.javaworld.com/javaworld/jw-03-2005/jw-0321-exception.html
o在“異常設計”(JavaWorld, July 1998)中,Bill Venners解釋了如何使用強制和非強制異常來支持契約設計:
http://www.javaworld.com/javaworld/jw-07-1998/jw-07-techniques.html
o“異常實踐”,Brian Goetz
§第一部分:在程序中有效使用異常(August 2001):
http://www.javaworld.com/javaworld/jw-08-2001/jw-0803-exceptions.html?
§第二部分:用異常鏈來保存調試信息(September 2001):
http://www.javaworld.com/javaworld/jw-09-2001/jw-0914-exceptions.html?
§第三部分:: 用信息目錄簡化本地化(December 2001):
http://www.javaworld.com/javaworld/jw-12-2001/jw-1221-exceptions.html?
o“異常:不要為丟棄而處理異?!盩ony Sintes (February 2002):
http://www.javaworld.com/javaworld/javaqa/2002-02/01-qa-0208-exceptional.html
o“java提示124:不要在捕獲異常時把網灑得太大” Dave Schweisguth (February 2003):
http://www.javaworld.com/javaworld/javatips/jw-javatip134.html?
o通用異常的危險:“小心通用異常的危險” Paul Philion (October 2003): http://www.javaworld.com/javaworld/jw-10-2003/jw-1003-generics.html?
·在JavaWorld的j2EE部分有更多有關J2EE的文章
http://www.javaworld.com/channel_content/jw-j2ee-index.shtml?
·在JavaWorld的EJB部分有更多有關J2EE的文章
http://www.javaworld.com/channel_content/jw-ejbs-index.shtml?
·在JavaWorld的Testing部分有更多參考
http://www.javaworld.com/channel_content/jw-testing-index.shtml進入討論組討論。
(出處:http://m.survivalescaperooms.com)