級(jí)別: 初級(jí)
Srikanth Shenoy, J2EE 顧問
2002 年 5 月 05 日
隨著 J2EE 成為企業(yè)開發(fā)平臺(tái)之選,越來越多基于 J2EE 的應(yīng)用程序?qū)⑼度肷a(chǎn)。J2EE 平臺(tái)的重要組件之一是 Enterprise JavaBean(EJB)API。J2EE 和 EJB 技術(shù)一起提供了許多優(yōu)點(diǎn),但隨之而來的還有一些新的挑戰(zhàn)。特別是企業(yè)系統(tǒng),其中的任何問題都必須快速得到解決。在本文中,企業(yè) Java 編程老手 Srikanth Shenoy 展現(xiàn)了他在 EJB 異常處理方面的最佳做法,這些做法可以更快解決問題。
在 hello-world 情形中,異常處理非常簡(jiǎn)單。每當(dāng)碰到某個(gè)方法的異常時(shí),就捕獲該異常并打印堆棧跟蹤或者聲明這個(gè)方法拋出異常。不幸的是,這種辦法不足以處理現(xiàn)實(shí)中出現(xiàn)的各種類型的異常。在生產(chǎn)系統(tǒng)中,當(dāng)有異常拋出時(shí),很可能是最終用戶無法處理他或她的請(qǐng)求。當(dāng)發(fā)生這樣的異常時(shí),最終用戶通常希望能這樣:
- 有一條清楚的消息表明已經(jīng)發(fā)生了一個(gè)錯(cuò)誤
- 有一個(gè)唯一的錯(cuò)誤號(hào),他可以據(jù)此訪問可方便獲得的客戶支持系統(tǒng)
- 問題快速得到解決,并且可以確信他的請(qǐng)求已經(jīng)得到處理,或者將在設(shè)定的時(shí)間段內(nèi)得到處理
理想情況下,企業(yè)級(jí)系統(tǒng)將不僅為客戶提供這些基本的服務(wù),還將準(zhǔn)備好一些必要的后端機(jī)制。舉例來說,客戶服務(wù)小組應(yīng)該收到即時(shí)的錯(cuò)誤通知,以便在客戶打電話求助之前服務(wù)代表就能意識(shí)到問題。此外,服務(wù)代表應(yīng)該能夠交叉引用用戶的唯一錯(cuò)誤號(hào)和產(chǎn)品日志,從而快速識(shí)別問題 ― 最好是能把問題定位到確切的行號(hào)或確切的方法。為了給最終用戶和支持小組提供他們需要的工具和服務(wù),在構(gòu)建一個(gè)系統(tǒng)時(shí),您就必須對(duì)系統(tǒng)被部署后可能出問題的所有地方心中有數(shù)。
在本文中,我們將談?wù)劵?EJB 的系統(tǒng)中的異常處理。我們將從回顧異常處理的基礎(chǔ)知識(shí)開始,包括日志實(shí)用程序的使用,然后,很快就轉(zhuǎn)入對(duì) EJB 技術(shù)如何定義和管理不同類型的異常進(jìn)行更詳細(xì)的討論。此后,我們將通過一些代碼示例來研究一些常見的異常處理解決方案的優(yōu)缺點(diǎn),我還將展示我自己在充分利用 EJB 異常處理方面的最佳做法。
請(qǐng)注意,本文假設(shè)您熟悉 J2EE 和 EJB 技術(shù)。您應(yīng)理解實(shí)體 bean 和會(huì)話 bean 的差異。如果您對(duì) bean 管理的持久性(bean-managed persistence(BMP))和容器管理的持久性(container-managed persistence(CMP))在實(shí)體 bean 上下文中是什么意思稍有了解,也是有幫助的。請(qǐng)參閱 參考資料部分了解關(guān)于 J2EE 和 EJB 技術(shù)的更多信息。
異常處理基礎(chǔ)知識(shí)
解決系統(tǒng)錯(cuò)誤的第一步是建立一個(gè)與生產(chǎn)系統(tǒng)具有相同構(gòu)造的測(cè)試系統(tǒng),然后跟蹤導(dǎo)致拋出異常的所有代碼,以及代碼中的所有不同分支。在分布式應(yīng)用程序中,很可能是調(diào)試器不工作了,所以,您可能將用 System.out.println() 方法跟蹤異常。 System.out.println 盡管很方便,但開銷巨大。在磁盤 I/O 期間, System.out.println 對(duì) I/O 處理進(jìn)行同步,這極大降低了吞吐量。在缺省情況下,堆棧跟蹤被記錄到控制臺(tái)。但是,在生產(chǎn)系統(tǒng)中,瀏覽控制臺(tái)以查看異常跟蹤是行不通的。而且,不能保證堆棧跟蹤會(huì)顯示在生產(chǎn)系統(tǒng)中,因?yàn)椋?NT 上,系統(tǒng)管理員可以把 System.out 和 System.err 映射到 ' ' ,在 UNIX 上,可以映射到 dev/null 。此外,如果您把 J2EE 應(yīng)用程序服務(wù)器作為 NT 服務(wù)運(yùn)行,甚至不會(huì)有控制臺(tái)。即使您把控制臺(tái)日志重定向到一個(gè)輸出文件,當(dāng)產(chǎn)品 J2EE 應(yīng)用程序服務(wù)器重新啟動(dòng)時(shí),這個(gè)文件很可能也將被重寫。
 |
異常處理的原則
以下是一些普遍接受的異常處理原則:
- 如果無法處理某個(gè)異常,那就不要捕獲它。
- 如果捕獲了一個(gè)異常,請(qǐng)不要胡亂處理它。
- 盡量在靠近異常被拋出的地方捕獲異常。
- 在捕獲異常的地方將它記錄到日志中,除非您打算將它重新拋出。
- 按照您的異常處理必須多精細(xì)來構(gòu)造您的方法。
- 需要用幾種類型的異常就用幾種,尤其是對(duì)于應(yīng)用程序異常。
第 1 點(diǎn)顯然與第 3 點(diǎn)相抵觸。實(shí)際的解決方案是以下兩者的折衷:您在距異常被拋出多近的地方將它捕獲;在完全丟失原始異常的意圖或內(nèi)容之前,您可以讓異常落在多遠(yuǎn)的地方。
注:盡管這些原則的應(yīng)用遍及所有 EJB 異常處理機(jī)制,但它們并不是特別針對(duì) EJB 異常處理的。 | |
由于以上這些原因,把代碼組裝成產(chǎn)品并同時(shí)包含 System.out.println 并不是一種選擇。在測(cè)試期間使用 System.out.println ,然后在形成產(chǎn)品之前除去 System.out.println 也不是上策,因?yàn)檫@樣做意味著您的產(chǎn)品代碼與測(cè)試代碼運(yùn)行得不盡相同。您需要的是一種聲明控制日志機(jī)制,以使您的測(cè)試代碼和產(chǎn)品代碼相同,并且當(dāng)記錄日志以聲明方式關(guān)閉時(shí),給產(chǎn)品帶來的性能開銷最小。
這里的解決方案顯然是使用一個(gè)日志實(shí)用程序。采用恰當(dāng)?shù)木幋a約定,日志實(shí)用程序?qū)⒇?fù)責(zé)精確地記錄下任何類型的消息,不論是系統(tǒng)錯(cuò)誤還是一些警告。所以,我們將在進(jìn)一步講述之前談?wù)勅罩緦?shí)用程序。
日志領(lǐng)域:鳥瞰
每個(gè)大型應(yīng)用程序在開發(fā)、測(cè)試及產(chǎn)品周期中都使用日志實(shí)用程序。在今天的日志領(lǐng)域中,有幾個(gè)角逐者,其中有兩個(gè)廣為人知。一個(gè)是 Log4J,它是來自 Apache 的 Jakarta 的一個(gè)開放源代碼的項(xiàng)目。另一個(gè)是 J2SE 1.4 捆綁提供的,它是最近剛加入到這個(gè)行列的。我們將使用 Log4J 說明本文所討論的最佳做法;但是,這些最佳做法并不特別依賴于 Log4J。
Log4J 有三個(gè)主要組件:layout、appender 和 category。 Layou代表消息被記錄到日志中的格式。 appender是消息將被記錄到的物理位置的別名。而 category則是有名稱的實(shí)體:您可以把它當(dāng)作是日志的句柄。layout 和 appender 在 XML 配置文件中聲明。每個(gè) category 帶有它自己的 layout 和 appender 定義。當(dāng)您獲取了一個(gè) category 并把消息記錄到它那里時(shí),消息在與該 category 相關(guān)聯(lián)的各個(gè) appender 處結(jié)束,并且所有這些消息都將以 XML 配置文件中指定的 layout 格式表示。
Log4J 給消息指定四種優(yōu)先級(jí):它們是 ERROR、WARN、INFO 和 DEBUG。為便于本文的討論,所有異常都以具有 ERROR 優(yōu)先級(jí)記錄。當(dāng)記錄本文中的一個(gè)異常時(shí),我們將能夠找到獲取 category(使用 Category.getInstance(String name) 方法)的代碼,然后調(diào)用方法 category.error() (它與具有 ERROR 優(yōu)先級(jí)的消息相對(duì)應(yīng))。
盡管日志實(shí)用程序能幫助我們把消息記錄到適當(dāng)?shù)某志梦恢茫鼈儾⒉荒芨龁栴}。它們不能從產(chǎn)品日志中精確找出某個(gè)客戶的問題報(bào)告;這一便利技術(shù)留給您把它構(gòu)建到您正在開發(fā)的系統(tǒng)中。
要了解關(guān)于 Log4J 日志實(shí)用程序或 J2SE 所帶的日志實(shí)用程序的更多信息,請(qǐng)參閱 參考資料部分。
異常的類別
異常的分類有不同方式。這里,我們將討論從 EJB 的角度如何對(duì)異常進(jìn)行分類。EJB 規(guī)范將異常大致分成三類:
- JVM 異常:這種類型的異常由 JVM 拋出。
OutOfMemoryError 就是 JVM 異常的一個(gè)常見示例。對(duì) JVM 異常您無能為力。它們表明一種致命的情況。唯一得體的退出辦法是停止應(yīng)用程序服務(wù)器(可能要增加硬件資源),然后重新啟動(dòng)系統(tǒng)。
- 應(yīng)用程序異常:應(yīng)用程序異常是一種定制異常,由應(yīng)用程序或第三方的庫(kù)拋出。這些本質(zhì)上是受查異常(checked exception);它們預(yù)示了業(yè)務(wù)邏輯中的某個(gè)條件尚未滿足。在這樣的情況下,EJB 方法的調(diào)用者可以得體地處理這種局面并采用另一條備用途徑。
- 系統(tǒng)異常:在大多數(shù)情況下,系統(tǒng)異常由 JVM 作為
RuntimeException 的子類拋出。例如, NullPointerException 或 ArrayOutOfBoundsException 將因代碼中的錯(cuò)誤而被拋出。另一種類型的系統(tǒng)異常在系統(tǒng)碰到配置不當(dāng)?shù)馁Y源(例如,拼寫錯(cuò)誤的 JNDI 查找(JNDI lookup))時(shí)發(fā)生。在這種情況下,系統(tǒng)就將拋出一個(gè)受查異常。捕獲這些受查系統(tǒng)異常并將它們作為非受查異常(unchecked exception)拋出頗有意義。最重要的規(guī)則是,如果您對(duì)某個(gè)異常無能為力,那么它就是一個(gè)系統(tǒng)異常并且應(yīng)當(dāng)作為非受查異常拋出。
注: 受查異常是一個(gè)作為 java.lang.Exception 的子類的 Java 類。通過從 java.lang.Exception 派生子類,就強(qiáng)制您在編譯時(shí)捕獲這個(gè)異常。相反地, 非受查異常則是一個(gè)作為 java.lang.RuntimeException 的子類的 Java 類。從 java.lang.RuntimeException 派生子類確保了編譯器不會(huì)強(qiáng)制您捕獲這個(gè)異常。
EJB 容器怎樣處理異常
EJB 容器攔截 EJB 組件上的每一個(gè)方法調(diào)用。結(jié)果,方法調(diào)用中發(fā)生的每一個(gè)異常也被 EJB 容器攔截到。EJB 規(guī)范只處理兩種類型的異常:應(yīng)用程序異常和系統(tǒng)異常。
EJB 規(guī)范把 應(yīng)用程序異常定義為在遠(yuǎn)程接口中的方法說明上聲明的任何異常(而不是 RemoteException )。應(yīng)用程序異常是業(yè)務(wù)工作流中的一種特殊情形。當(dāng)這種類型的異常被拋出時(shí),客戶機(jī)會(huì)得到一個(gè)恢復(fù)選項(xiàng),這個(gè)選項(xiàng)通常是要求以一種不同的方式處理請(qǐng)求。不過,這并不意味著任何在遠(yuǎn)程接口方法的 throws 子句中聲明的非受查異常都會(huì)被當(dāng)作應(yīng)用程序異常對(duì)待。EJB 規(guī)范明確指出,應(yīng)用程序異常不應(yīng)繼承 RuntimeException 或它的子類。
當(dāng)發(fā)生應(yīng)用程序異常時(shí),除非被顯式要求(通過調(diào)用關(guān)聯(lián)的 EJBContext 對(duì)象的 setRollbackOnly() 方法)回滾事務(wù),否則 EJB 容器就不會(huì)這樣做。事實(shí)上,應(yīng)用程序異常被保證以它原本的狀態(tài)傳送給客戶機(jī):EJB 容器絕不會(huì)以任何方式包裝或修改異常。
系統(tǒng)異常被定義為受查異常或非受查異常,EJB 方法不能從這種異常恢復(fù)。當(dāng) EJB 容器攔截到非受查異常時(shí),它會(huì)回滾事務(wù)并執(zhí)行任何必要的清理工作。接著,它把該非受查異常包裝到 RemoteException 中,然后拋給客戶機(jī)。這樣,EJB 容器就把所有非受查異常作為 RemoteException (或者作為其子類,例如 TransactionRolledbackException )提供給客戶機(jī)。
對(duì)于受查異常的情況,容器并不會(huì)自動(dòng)執(zhí)行上面所描述的內(nèi)務(wù)處理。要使用 EJB 容器的內(nèi)部?jī)?nèi)務(wù)處理,您將必須把受查異常作為非受查異常拋出。每當(dāng)發(fā)生受查系統(tǒng)異常(如 NamingException )時(shí),您都應(yīng)該通過包裝原始的異常拋出 javax.ejb.EJBException 或其子類。因?yàn)?EJBException 本身是非受查異常,所以不需要在方法的 throws 子句中聲明它。EJB 容器捕獲 EJBException 或其子類,把它包裝到 RemoteException 中,然后把 RemoteException 拋給客戶機(jī)。
雖然系統(tǒng)異常由應(yīng)用程序服務(wù)器記錄(這是 EJB 規(guī)范規(guī)定的),但記錄格式將因應(yīng)用程序服務(wù)器的不同而異。為了訪問所需的統(tǒng)計(jì)信息,企業(yè)常常需要對(duì)所生成的日志運(yùn)行 shell/Perl 腳本。為了確保記錄格式的統(tǒng)一,在您的代碼中記錄異常會(huì)更好些。
注:EJB 1.0 規(guī)范要求把受查系統(tǒng)異常作為 RemoteException 拋出。從 EJB 1.1 規(guī)范起規(guī)定 EJB 實(shí)現(xiàn)類絕不應(yīng)拋出 RemoteException 。
常見的異常處理策略
如果沒有異常處理策略,項(xiàng)目小組的不同開發(fā)者很可能會(huì)編寫以不同方式處理異常的代碼。由于同一個(gè)異常在系統(tǒng)的不同地方可能以不同的方式被描述和處理,所以,這至少會(huì)使產(chǎn)品支持小組感到迷惑。缺乏策略還會(huì)導(dǎo)致在整個(gè)系統(tǒng)的多個(gè)地方都有記錄。日志應(yīng)該集中起來或者分成幾個(gè)可管理的單元。理想的情況是,應(yīng)在盡可能少的地方記錄異常日志,同時(shí)不損失內(nèi)容。在這一部分及其后的幾個(gè)部分,我將展示可以在整個(gè)企業(yè)系統(tǒng)中以統(tǒng)一的方式實(shí)現(xiàn)的編碼策略。您可以從 參考資料部分下載本文開發(fā)的實(shí)用程序類。
清單 1 顯示了來自會(huì)話 EJB 組件的一個(gè)方法。這個(gè)方法刪除某個(gè)客戶在特定日期前所下的全部訂單。首先,它獲取 OrderEJB 的 Home 接口。接著,它取回某個(gè)特定客戶的所有訂單。當(dāng)它碰到在某個(gè)特定日期之前所下的訂單時(shí),就刪除所訂購(gòu)的商品,然后刪除訂單本身。請(qǐng)注意,拋出了三個(gè)異常,顯示了三種常見的異常處理做法。(為簡(jiǎn)單起見,假設(shè)編譯器優(yōu)化未被使用。) 清單 1. 三種常見的異常處理做法
100 try {
101 OrderHome homeObj = EJBHomeFactory.getInstance().getOrderHome();
102 Collection orderCollection = homeObj.findByCustomerId(id);
103 iterator orderItter = orderCollection.iterator();
104 while (orderIter.hasNext()) {
105 Order orderRemote = (OrderRemote) orderIter.getNext();
106 OrderValue orderVal = orderRemote.getValue();
107 if (orderVal.getDate() < "mm/dd/yyyy") {
108 OrderItemHome itemHome =
EJBHomeFactory.getInstance().getItemHome();
109 Collection itemCol = itemHome.findByOrderId(orderId)
110 Iterator itemIter = itemCol.iterator();
111 while (itemIter.hasNext()) {
112 OrderItem item = (OrderItem) itemIter.getNext();
113 item.remove();
114 }
115 orderRemote.remove();
116 }
117 }
118 } catch (NamingException ne) {
119 throw new EJBException("Naming Exception occurred");
120 } catch (FinderException fe) {
121 fe.printStackTrace();
122 throw new EJBException("Finder Exception occurred");
123 } catch (RemoteException re) {
124 re.printStackTrace();
125 //Some code to log the message
126 throw new EJBException(re);
127 }
|
現(xiàn)在,讓我們用上面所示的代碼來研究一下所展示的三種異常處理做法的缺點(diǎn)。
拋出/重拋出帶有出錯(cuò)消息的異常
NamingException 可能發(fā)生在行 101 或行 108。當(dāng)發(fā)生 NamingException 時(shí),這個(gè)方法的調(diào)用者就得到 RemoteException 并向后跟蹤該異常到行 119。調(diào)用者并不能告知 NamingException 實(shí)際是發(fā)生在行 101 還是行 108。由于異常內(nèi)容要直到被記錄了才能得到保護(hù),所以,這個(gè)問題的根源很難查出。在這種情形下,我們就說異常的內(nèi)容被“吞掉”了。正如這個(gè)示例所示,拋出或重拋出一個(gè)帶有消息的異常并不是一種好的異常處理解決辦法。
記錄到控制臺(tái)并拋出一個(gè)異常
FinderException 可能發(fā)生在行 102 或 109。不過,由于異常被記錄到控制臺(tái),所以僅當(dāng)控制臺(tái)可用時(shí)調(diào)用者才能向后跟蹤到行 102 或 109。這顯然不可行,所以異常只能被向后跟蹤到行 122。這里的推理同上。
包裝原始的異常以保護(hù)其內(nèi)容
RemoteException 可能發(fā)生在行 102、106、109、113 或 115。它在行 123 的 catch 塊被捕獲。接著,這個(gè)異常被包裝到 EJBException 中,所以,不論調(diào)用者在哪里記錄它,它都能保持完整。這種辦法比前面兩種辦法更好,同時(shí)演示了沒有日志策略的情況。如果 deleteOldOrders() 方法的調(diào)用者記錄該異常,那么將導(dǎo)致重復(fù)記錄。而且,盡管有了日志記錄,但當(dāng)客戶報(bào)告某個(gè)問題時(shí),產(chǎn)品日志或控制臺(tái)并不能被交叉引用。
EJB 異常處理探試法
EJB 組件應(yīng)拋出哪些異常?您應(yīng)將它們記錄到系統(tǒng)中的什么地方?這兩個(gè)問題盤根錯(cuò)結(jié)、相互聯(lián)系,應(yīng)該一起解決。解決辦法取決于以下因素:
- 您的 EJB 系統(tǒng)設(shè)計(jì):在良好的 EJB 設(shè)計(jì)中,客戶機(jī)絕不調(diào)用實(shí)體 EJB 組件上的方法。多數(shù)實(shí)體 EJB 方法調(diào)用發(fā)生在會(huì)話 EJB 組件中。如果您的設(shè)計(jì)遵循這些準(zhǔn)則,則您應(yīng)該用會(huì)話 EJB 組件來記錄異常。如果客戶機(jī)直接調(diào)用了實(shí)體 EJB 方法,則您還應(yīng)該把消息記錄到實(shí)體 EJB 組件中。然而,存在一個(gè)難題:相同的實(shí)體 EJB 方法可能也會(huì)被會(huì)話 EJB 組件調(diào)用。在這種情形下,如何避免重復(fù)記錄呢?類似地,當(dāng)一個(gè)會(huì)話 EJB 組件調(diào)用其它實(shí)體 EJB 方法時(shí),您如何避免重復(fù)記錄呢?很快我們就將探討一種處理這兩種情況的通用解決方案。(請(qǐng)注意,EJB 1.1 并未從體系結(jié)構(gòu)上阻止客戶機(jī)調(diào)用實(shí)體 EJB 組件上的方法。在 EJB 2.0 中,您可以通過為實(shí)體 EJB 組件定義本地接口規(guī)定這種限制。)
- 計(jì)劃的代碼重用范圍:這里的問題是您是打算把日志代碼添加到多個(gè)地方,還是打算重新設(shè)計(jì)、重新構(gòu)造代碼來減少日志代碼。
- 您要為之服務(wù)的客戶機(jī)的類型:考慮您是將為 J2EE Web 層、單機(jī) Java 應(yīng)用程序、PDA 還是將為其它客戶機(jī)服務(wù)是很重要的。Web 層設(shè)計(jì)有各種形狀和大小。如果您在使用命令(Command)模式,在這個(gè)模式中,Web 層通過每次傳入一個(gè)不同的命令調(diào)用 EJB 層中的相同方法,那么,把異常記錄到命令在其中執(zhí)行的 EJB 組件中是很有用的。在多數(shù)其它的 Web 層設(shè)計(jì)中,把異常記錄到 Web 層本身要更容易,也更好,因?yàn)槟枰旬惓H罩敬a添加到更少的地方。如果您的 Web 層和 EJB 層在同一地方并且不需要支持任何其它類型的客戶機(jī),那么就應(yīng)該考慮后一種選擇。
- 您將處理的異常的類型(應(yīng)用程序或系統(tǒng)):處理應(yīng)用程序異常與處理系統(tǒng)異常有很大不同。系統(tǒng)異常的發(fā)生不受 EJB 開發(fā)者意圖的控制。因?yàn)橄到y(tǒng)異常的含義不清楚,所以內(nèi)容應(yīng)指明異常的上下文。您已經(jīng)看到了,通過對(duì)原始異常進(jìn)行包裝使這個(gè)問題得到了最好的處理。另一方面,應(yīng)用程序異常是由 EJB 開發(fā)者顯式拋出的,通常包裝有一條消息。因?yàn)閼?yīng)用程序異常的含義清楚,所以沒有理由要保護(hù)它的上下文。這種類型的異常不必記錄到 EJB 層或客戶機(jī)層;它應(yīng)該以一種有意義的方式提供給最終用戶,帶上指向所提供的解決方案的另一條備用途徑。系統(tǒng)異常消息沒必要對(duì)最終用戶很有意義。
 |
處理應(yīng)用程序異常
在這一部分及其后的幾個(gè)部分中,我們將更仔細(xì)地研究用 EJB 異常處理應(yīng)用程序異常和系統(tǒng)異常,以及 Web 層設(shè)計(jì)。作為這個(gè)討論的一部分,我們將探討處理從會(huì)話和實(shí)體 EJB 組件拋出的異常的不同方式。
實(shí)體 EJB 組件中的應(yīng)用程序異常 清單 2 顯示了實(shí)體 EJB 的一個(gè) ejbCreate() 方法。這個(gè)方法的調(diào)用者傳入一個(gè) OrderItemValue 并請(qǐng)求創(chuàng)建一個(gè) OrderItem 實(shí)體。因?yàn)?OrderItemValue 沒有名稱,所以拋出了 CreateException 。 清單 2. 實(shí)體 EJB 組件中的樣本 ejbCreate() 方法
public Integer ejbCreate(OrderItemValue value) throws CreateException {
if (value.getItemName() == null) {
throw new CreateException("Cannot create Order without a name");
}
..
..
return null;
}
|
清單 2 顯示了 CreateException 的一個(gè)很典型的用法。類似地,如果方法的輸入?yún)?shù)的值不正確,則查找程序方法將拋出 FinderException 。
然而,如果您在使用容器管理的持久性(CMP),則開發(fā)者無法控制查找程序方法,從而 FinderException 永遠(yuǎn)不會(huì)被 CMP 實(shí)現(xiàn)拋出。盡管如此,在 Home 接口的查找程序方法的 throws 子句中聲明 FinderException 還是要更好一些。 RemoveException 是另一個(gè)應(yīng)用程序異常,它在實(shí)體被刪除時(shí)被拋出。
從實(shí)體 EJB 組件拋出的應(yīng)用程序異常基本上限定為這三種類型( CreateException 、 FinderException 和 RemoveException )及它們的子類。多數(shù)應(yīng)用程序異常都來源于會(huì)話 EJB 組件,因?yàn)槟抢锸亲鞒鲋悄軟Q策的地方。實(shí)體 EJB 組件一般是啞類,它們的唯一職責(zé)就是創(chuàng)建和取回?cái)?shù)據(jù)。
會(huì)話 EJB 組件中的應(yīng)用程序異常 清單 3 顯示了來自會(huì)話 EJB 組件的一個(gè)方法。這個(gè)方法的調(diào)用者設(shè)法訂購(gòu) n 件某特定類型的某商品。 SessionEJB() 方法計(jì)算出倉(cāng)庫(kù)中的數(shù)量不夠,于是拋出 NotEnoughStockException 。 NotEnoughStockException 適用于特定于業(yè)務(wù)的場(chǎng)合;當(dāng)拋出了這個(gè)異常時(shí),調(diào)用者會(huì)得到采用另一個(gè)備用途徑的建議,讓他訂購(gòu)更少數(shù)量的商品。 清單 3. 會(huì)話 EJB 組件中的樣本容器回調(diào)方法
public ItemValueObject[] placeOrder(int n, ItemType itemType) throws
NotEnoughStockException {
//Check Inventory.
Collection orders = ItemHome.findByItemType(itemType);
if (orders.size() < n) {
throw NotEnoughStockException("Insufficient stock for " + itemType);
}
}
|
處理系統(tǒng)異常
系統(tǒng)異常處理是比應(yīng)用程序異常處理更為復(fù)雜的論題。由于會(huì)話 EJB 組件和實(shí)體 EJB 組件處理系統(tǒng)異常的方式相似,所以,對(duì)于本部分的所有示例,我們都將著重于實(shí)體 EJB 組件,不過請(qǐng)記住,其中的大部分示例也適用于處理會(huì)話 EJB 組件。
當(dāng)引用其它 EJB 遠(yuǎn)程接口時(shí),實(shí)體 EJB 組件會(huì)碰到 RemoteException ,而查找其它 EJB 組件時(shí),則會(huì)碰到 NamingException ,如果使用 bean 管理的持久性(BMP),則會(huì)碰到 SQLException 。與這些類似的受查系統(tǒng)異常應(yīng)該被捕獲并作為 EJBException 或它的一個(gè)子類拋出。原始的異常應(yīng)被包裝起來。清單 4 顯示了一種處理系統(tǒng)異常的辦法,這種辦法與處理系統(tǒng)異常的 EJB 容器的行為一致。通過包裝原始的異常并在實(shí)體 EJB 組件中將它重新拋出,您就確保了能夠在想記錄它的時(shí)候訪問該異常。 清單 4. 處理系統(tǒng)異常的一種常見方式
try {
OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
throw new EJBException(ne);
} catch (SQLException se) {
throw new EJBException(se);
} catch (RemoteException re) {
throw new EJBException(re);
}
|
避免重復(fù)記錄
通常,異常記錄發(fā)生在會(huì)話 EJB 組件中。但如果直接從 EJB 層外部訪問實(shí)體 EJB 組件,又會(huì)怎么樣呢?要是這樣,您就不得不在實(shí)體 EJB 組件中記錄異常并拋出它。這里的問題是,調(diào)用者沒辦法知道異常是否已經(jīng)被記錄,因而很可能再次記錄它,從而導(dǎo)致重復(fù)記錄。更重要的是,調(diào)用者沒辦法訪問初始記錄時(shí)所生成的唯一的標(biāo)識(shí)。任何沒有交叉引用機(jī)制的記錄都是毫無用處的。
請(qǐng)考慮這種最糟糕的情形:?jiǎn)螜C(jī) Java 應(yīng)用程序訪問了實(shí)體 EJB 組件中的一個(gè)方法 foo() 。在一個(gè)名為 bar() 的會(huì)話 EJB 方法中也訪問了同一個(gè)方法。一個(gè) Web 層客戶機(jī)調(diào)用會(huì)話 EJB 組件的方法 bar() 并也記錄了該異常。如果當(dāng)從 Web 層調(diào)用會(huì)話 EJB 方法 bar() 時(shí)在實(shí)體 EJB 方法 foo() 中發(fā)生了一個(gè)異常,則該異常將被記錄到三個(gè)地方:先是在實(shí)體 EJB 組件,然后是在會(huì)話 EJB 組件,最后是在 Web 層。而且,沒有一個(gè)堆棧跟蹤可以被交叉引用!
幸運(yùn)的是,解決這些問題用常規(guī)辦法就可以很容易地做到。您所需要的只是一種機(jī)制,使調(diào)用者能夠:
- 訪問唯一的標(biāo)識(shí)
- 查明異常是否已經(jīng)被記錄了
您可以派生 EJBException 的子類來存儲(chǔ)這樣的信息。清單 5 顯示了 LoggableEJBException 子類: 清單 5. LoggableEJBException ― EJBException 的一個(gè)子類
public class LoggableEJBException extends EJBException {
protected boolean isLogged;
protected String uniqueID;
public LoggableEJBException(Exception exc) {
super(exc);
isLogged = false;
uniqueID = ExceptionIDGenerator.getExceptionID();
}
..
..
}
|
類 LoggableEJBException 有一個(gè)指示符標(biāo)志( isLogged ),用于檢查異常是否已經(jīng)被記錄了。每當(dāng)捕獲一個(gè) LoggableEJBException 時(shí),看一下該異常是否已經(jīng)被記錄了( isLogged == false )。如果 isLogged 為 false,則記錄該異常并把標(biāo)志設(shè)置為 true 。
ExceptionIDGenerator 類用當(dāng)前時(shí)間和機(jī)器的主機(jī)名為異常生成唯一的標(biāo)識(shí)。如果您喜歡,也可以用有想象力的算法來生成這個(gè)唯一的標(biāo)識(shí)。如果您在實(shí)體 EJB 組件中記錄了異常,則這個(gè)異常將不會(huì)在別的地方被記錄。如果您沒有記錄就在實(shí)體 EJB 組件中拋出了 LoggableEJBException ,則這個(gè)異常將被記錄到會(huì)話 EJB 組件中,但不記錄到 Web 層中。
單 6 顯示了使用這一技術(shù)重寫后的清單 4。您還可以繼承 LoggableException 以適合于您的需要(通過給異常指定錯(cuò)誤代碼等)。 清單 6. 使用 LoggableEJBException 的異常處理
try {
OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
throw new LoggableEJBException(ne);
} catch (SQLException se) {
throw new LoggableEJBException(se);
} catch (RemoteException re) {
Throwable t = re.detail;
if (t != null && t instanceof Exception) {
throw new LoggableEJBException((Exception) re.detail);
} else {
throw new LoggableEJBException(re);
}
}
|
記錄 RemoteException
從清單 6 中,您可以看到 naming 和 SQL 異常在被拋出前被包裝到了 LoggableEJBException 中。但 RemoteException 是以一種稍有不同 ― 而且要稍微花點(diǎn)氣力 ― 的方式處理的。
 |
會(huì)話 EJB 組件中的系統(tǒng)異常
如果您決定記錄會(huì)話 EJB 異常,請(qǐng)使用 清單 7所示的記錄代碼;否則,請(qǐng)拋出異常,如 清單 6所示。您應(yīng)該注意到,會(huì)話 EJB 組件處理異常可有一種與實(shí)體 EJB 組件不同的方式:因?yàn)榇蠖鄶?shù) EJB 系統(tǒng)都只能從 Web 層訪問,而且會(huì)話 EJB 可以作為 EJB 層的虛包,所以,把會(huì)話 EJB 異常的記錄推遲到 Web 層實(shí)際上是有可能做到的。 | | 它之所以不同,是因?yàn)樵?RemoteException 中,實(shí)際的異常將被存儲(chǔ)到一個(gè)稱為 detail (它是 Throwable 類型的)的公共屬性中。在大多數(shù)情況下,這個(gè)公共屬性保存有一個(gè)異常。如果您調(diào)用 RemoteException 的 printStackTrace ,則除打印 detail 的堆棧跟蹤之外,它還會(huì)打印異常本身的堆棧跟蹤。您不需要像這樣的 RemoteException 的堆棧跟蹤。
為了把您的應(yīng)用程序代碼從錯(cuò)綜復(fù)雜的代碼(例如 RemoteException 的代碼)中分離出來,這些行被重新構(gòu)造成一個(gè)稱為 ExceptionLogUtil 的類。有了這個(gè)類,您所要做的只是每當(dāng)需要?jiǎng)?chuàng)建 LoggableEJBException 時(shí)調(diào)用 ExceptionLogUtil.createLoggableEJBException(e) 。請(qǐng)注意,在清單 6 中,實(shí)體 EJB 組件并沒有記錄異常;不過,即便您決定在實(shí)體 EJB 組件中記錄異常,這個(gè)解決方案仍然行得通。清單 7 顯示了實(shí)體 EJB 組件中的異常記錄: 清單 7. 實(shí)體 EJB 組件中的異常記錄
try {
OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
Order order = orderHome.findByPrimaryKey(Integer id);
} catch (RemoteException re) {
LoggableEJBException le =
ExceptionLogUtil.createLoggableEJBException(re);
String traceStr = StackTraceUtil.getStackTrace(le);
Category.getInstance(getClass().getName()).error(le.getUniqueID() +
":" + traceStr);
le.setLogged(true);
throw le;
}
|
您在清單 7 中看到的是一個(gè)非常簡(jiǎn)單明了的異常記錄機(jī)制。一旦捕獲受查系統(tǒng)異常就創(chuàng)建一個(gè)新的 LoggableEJBException 。接著,使用類 StackTraceUtil 獲取 LoggableEJBException 的堆棧跟蹤,把它作為一個(gè)字符串。然后,使用 Log4J category 把該字符串作為一個(gè)錯(cuò)誤加以記錄。
StackTraceUtil 類的工作原理
在清單 7 中,您看到了一個(gè)新的稱為 StackTraceUtil 的類。因?yàn)?Log4J 只能記錄 String 消息,所以這個(gè)類負(fù)責(zé)解決把堆棧跟蹤轉(zhuǎn)換成 String 的問題。清單 8 說明了 StackTraceUtil 類的工作原理: 清單 8. StackTraceUtil 類
public class StackTraceUtil {
public static String getStackTrace(Exception e)
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
return sw.toString();
}
..
..
}
|
java.lang.Throwable 中缺省的 printStackTrace() 方法把出錯(cuò)消息記錄到 System.err 。 Throwable 還有一個(gè)重載的 printStackTrace() 方法,它把出錯(cuò)消息記錄到 PrintWriter 或 PrintStream 。上面的 StackTraceUtil 中的方法把 StringWriter 包裝到 PrintWriter 中。當(dāng) PrintWriter 包含有堆棧跟蹤時(shí),它只是調(diào)用 StringWriter 的 toString() ,以獲取該堆棧跟蹤的 String 表示。
Web 層的 EJB 異常處理
在 Web 層設(shè)計(jì)中,把異常記錄機(jī)制放到客戶機(jī)端往往更容易也更高效。要能做到這一點(diǎn),Web 層就必須是 EJB 層的唯一客戶機(jī)。此外,Web 層必須建立在以下模式或框架之一的基礎(chǔ)上:
- 模式:業(yè)務(wù)委派(Business Delegate)、FrontController 或攔截過濾器(Intercepting Filter)
- 框架:Struts 或任何包含層次結(jié)構(gòu)的類似于 MVC 框架的框架
為什么異常記錄應(yīng)該在客戶機(jī)端上發(fā)生呢?嗯,首先,控制尚未傳到應(yīng)用程序服務(wù)器之外。所謂的客戶機(jī)層在 J2EE 應(yīng)用程序服務(wù)器本身上運(yùn)行,它由 JSP 頁(yè)、servlet 或它們的助手類組成。其次,在設(shè)計(jì)良好的 Web 層中的類有一個(gè)層次結(jié)構(gòu)(例如:在業(yè)務(wù)委派(Business Delegate)類、攔截過濾器(Intercepting Filter)類、http 請(qǐng)求處理程序(http request handler)類和 JSP 基類(JSP base class)中,或者在 Struts Action 類中),或者 FrontController servlet 形式的單點(diǎn)調(diào)用。這些層次結(jié)構(gòu)的基類或者 Controller 類中的中央點(diǎn)可能包含有異常記錄代碼。對(duì)于基于會(huì)話 EJB 記錄的情況,EJB 組件中的每一個(gè)方法都必須具有記錄代碼。隨著業(yè)務(wù)邏輯的增加,會(huì)話 EJB 方法的數(shù)量也會(huì)增加,記錄代碼的數(shù)量也會(huì)增加。Web 層系統(tǒng)將需要更少的記錄代碼。如果您的 Web 層和 EJB 層在同一地方并且不需要支持任何其它類型的客戶機(jī),那么您應(yīng)該考慮這一備用方案。不管怎樣,記錄機(jī)制不會(huì)改變;您可以使用與前面的部分所描述的相同技術(shù)。
真實(shí)世界的復(fù)雜性
到現(xiàn)在為止,您已經(jīng)看到了簡(jiǎn)單情形的會(huì)話和實(shí)體 EJB 組件的異常處理技術(shù)。然而,應(yīng)用程序異常的某些組合可能會(huì)更令人費(fèi)解,并且有多種解釋。清單 9 顯示了一個(gè)示例。 OrderEJB 的 ejbCreate() 方法試圖獲取 CustomerEJB 的一個(gè)遠(yuǎn)程引用,這會(huì)導(dǎo)致 FinderException 。 OrderEJB 和 CustomerEJB 都是實(shí)體 EJB 組件。您應(yīng)該如何解釋 ejbCreate() 中的這個(gè) FinderException 呢?是把它當(dāng)作應(yīng)用程序異常對(duì)待呢(因?yàn)?EJB 規(guī)范把它定義為標(biāo)準(zhǔn)應(yīng)用程序異常),還是當(dāng)作系統(tǒng)異常對(duì)待? 清單 9. ejbCreate() 方法中的 FinderException
public Object ejbCreate(OrderValue val) throws CreateException {
try {
if (value.getItemName() == null) {
throw new CreateException("Cannot create Order without a name");
}
String custId = val.getCustomerId();
Customer cust = customerHome.fingByPrimaryKey(custId);
this.customer = cust;
} catch (FinderException ne) {
//How do you handle this Exception ?
} catch (RemoteException re) {
//This is clearly a System Exception
throw ExceptionLogUtil.createLoggableEJBException(re);
}
return null;
}
|
雖然沒有什么東西阻止您把 FinderException 當(dāng)應(yīng)用程序異常對(duì)待,但把它當(dāng)系統(tǒng)異常對(duì)待會(huì)更好。原因是:EJB 客戶機(jī)傾向于把 EJB 組件當(dāng)黑箱對(duì)待。如果 createOrder() 方法的調(diào)用者獲得了一個(gè) FinderException ,這對(duì)調(diào)用者并沒有任何意義。 OrderEJB 正試圖設(shè)置客戶遠(yuǎn)程引用這件事對(duì)調(diào)用者來說是透明的。從客戶機(jī)的角度看,失敗僅僅意味著該訂單無法創(chuàng)建。
這類情形的另一個(gè)示例是,會(huì)話 EJB 組件試圖創(chuàng)建另一個(gè)會(huì)話 EJB,因而導(dǎo)致了一個(gè) CreateException 。一種類似的情形是,實(shí)體 EJB 方法試圖創(chuàng)建一個(gè)會(huì)話 EJB 組件,因而導(dǎo)致了一個(gè) CreateException 。這兩個(gè)異常都應(yīng)該當(dāng)作系統(tǒng)異常對(duì)待。
另一個(gè)可能碰到的挑戰(zhàn)是會(huì)話 EJB 組件在它的某個(gè)容器回調(diào)方法中獲得了一個(gè) FinderException 。您必須逐例處理這類情況。您可能要決定是把 FinderException 當(dāng)應(yīng)用程序異常還是系統(tǒng)異常對(duì)待。請(qǐng)考慮清單 1 的情況,其中調(diào)用者調(diào)用了會(huì)話 EJB 組件的 deleteOldOrder 方法。如果我們不是捕獲 FinderException ,而是將它拋出,會(huì)怎么樣呢?在這一特定情況中,把 FinderException 當(dāng)系統(tǒng)異常對(duì)待似乎是符合邏輯的。這里的理由是,會(huì)話 EJB 組件傾向于在它們的方法中做許多工作,因?yàn)樗鼈兲幚砉ぷ髁髑樾危⑶宜鼈儗?duì)調(diào)用者而言是黑箱。
另一方面,請(qǐng)考慮會(huì)話 EJB 正在處理下訂單的情形。要下一個(gè)訂單,用戶必須有一個(gè)簡(jiǎn)檔 ― 但這個(gè)特定用戶卻還沒有。業(yè)務(wù)邏輯可能希望會(huì)話 EJB 顯式地通知用戶她的簡(jiǎn)檔丟失了。丟失的簡(jiǎn)檔很可能表現(xiàn)為會(huì)話 EJB 組件中的 javax.ejb.ObjectNotFoundException ( FinderException 的一個(gè)子類)。在這種情況下,最好的辦法是在會(huì)話 EJB 組件中捕獲 ObjectNotFoundException 并拋出一個(gè)應(yīng)用程序異常,讓用戶知道她的簡(jiǎn)檔丟失了。
即使是有了很好的異常處理策略,另一個(gè)問題還是經(jīng)常會(huì)在測(cè)試中出現(xiàn),而且在產(chǎn)品中也更加重要。編譯器和運(yùn)行時(shí)優(yōu)化會(huì)改變一個(gè)類的整體結(jié)構(gòu),這會(huì)限制您使用堆棧跟蹤實(shí)用程序來跟蹤異常的能力。這就是您需要代碼重構(gòu)的幫助的地方。您應(yīng)該把大的方法調(diào)用分割為更小的、更易于管理的塊。而且,只要有可能,異常類型需要多少就劃分為多少;每次您捕獲一個(gè)異常,都應(yīng)該捕獲已規(guī)定好類型的異常,而不是捕獲所有類型的異常。
結(jié)束語
我們已經(jīng)在本文討論了很多東西,您可能想知道我們已經(jīng)討論的主要設(shè)計(jì)是否都物有所值。我的經(jīng)驗(yàn)是,即便是在中小型項(xiàng)目中,在開發(fā)周期中,您的付出就已經(jīng)能看到回報(bào),更不用說測(cè)試和產(chǎn)品周期了。此外,在宕機(jī)對(duì)業(yè)務(wù)具有毀滅性影響的生產(chǎn)系統(tǒng)中,良好的異常處理體系結(jié)構(gòu)的重要性再怎么強(qiáng)調(diào)也不過分。
我希望本文所展示的最佳做法對(duì)您有益。要深入理解這里提供的某些信息,請(qǐng)參看 參考資料部分中的清單。
參考資料
 |
關(guān)于作者
 |

|
Srikanth Shenoy 專門從事大型 J2EE 和 EAI 項(xiàng)目的體系結(jié)構(gòu)、設(shè)計(jì)、開發(fā)和部署工作。他在 Java 平臺(tái)一出現(xiàn)時(shí)就迷上了它,從此便全心投入。Srikanth 已經(jīng)幫他的制造業(yè)、物流業(yè)和金融業(yè)客戶實(shí)現(xiàn)了 Java 平臺(tái)“一次編寫,隨處運(yùn)行”的夢(mèng)想。您可以通過 srikanth@srikanth.org與他聯(lián)系。 | |