程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> EJB異常處理的最佳做法

EJB異常處理的最佳做法

編輯:關於JAVA

隨著 J2EE 成為企業開發平台之選,越來越多基於 J2EE 的應用程序將投入生產。J2EE 平台的重要組件之一是 Enterprise JavaBean(EJB)API。J2EE 和 EJB 技術一起提供了許多優點,但隨之而來的還有一些新的挑戰。特別是企業系統,其中的任何問題都必須快速得到解決。在本文中,企業 Java 編程老手 Srikanth Shenoy 展現了他在 EJB 異常處理方面的最佳做法,這些做法可以更快解決問題。

在 hello-world 情形中,異常處理非常簡單。每當碰到某個方法的異常時,就捕獲該異常並打印堆棧跟蹤或者聲明這個方法拋出異常。不幸的是,這種辦法不足以處理現實中出現的各種類型的異常。在生產系統中,當有異常拋出時,很可能是最終用戶無法處理他或她的請求。當發生這樣的異常時,最終用戶通常希望能這樣:

有一條清楚的消息表明已經發生了一個錯誤

有一個唯一的錯誤號,他可以據此訪問可方便獲得的客戶支持系統

問題快速得到解決,並且可以確信他的請求已經得到處理,或者將在設定的時間段內得到處理

理想情況下,企業級系統將不僅為客戶提供這些基本的服務,還將准備好一些必要的後端機制。舉例來說,客戶服務小組應該收到即時的錯誤通知,以便在客戶打電話求助之前服務代表就能意識到問題。此外,服務代表應該能夠交叉引用用戶的唯一錯誤號和產品日志,從而快速識別問題 ― 最好是能把問題定位到確切的行號或確切的方法。為了給最終用戶和支持小組提供他們需要的工具和服務,在構建一個系統時,您就必須對系統被部署後可能出問題的所有地方心中有數。

在本文中,我們將談談基於 EJB 的系統中的異常處理。我們將從回顧異常處理的基礎知識開始,包括日志實用程序的使用,然後,很快就轉入對 EJB 技術如何定義和管理不同類型的異常進行更詳細的討論。此後,我們將通過一些代碼示例來研究一些常見的異常處理解決方案的優缺點,我還將展示我自己在充分利用 EJB 異常處理方面的最佳做法。

請注意,本文假設您熟悉 J2EE 和 EJB 技術。您應理解實體 bean 和會話 bean 的差異。如果您對 bean 管理的持久性(bean-managed persistence(BMP))和容器管理的持久性(container-managed persistence(CMP))在實體 bean 上下文中是什麼意思稍有了解,也是有幫助的。請參閱 參考資料部分了解關於 J2EE 和 EJB 技術的更多信息。

異常處理基礎知識

解決系統錯誤的第一步是建立一個與生產系統具有相同構造的測試系統,然後跟蹤導致拋出異常的所有代碼,以及代碼中的所有不同分支。在分布式應用程序中,很可能是調試器不工作了,所以,您可能將用 System.out.println() 方法跟蹤異常。System.out.println 盡管很方便,但開銷巨大。在磁盤 I/O 期間, System.out.println 對 I/O 處理進行同步,這極大降低了吞吐量。在缺省情況下,堆棧跟蹤被記錄到控制台。但是,在生產系統中,浏覽控制台以查看異常跟蹤是行不通的。而且,不能保證堆棧跟蹤會顯示在生產系統中,因為,在 NT 上,系統管理員可以把 System.out 和 System.err 映射到 ' ' ,在 UNIX 上,可以映射到 dev/null 。此外,如果您把 J2EE 應用程序服務器作為 NT 服務運行,甚至不會有控制台。即使您把控制台日志重定向到一個輸出文件,當產品 J2EE 應用程序服務器重新啟動時,這個文件很可能也將被重寫。

異常處理的原則

以下是一些普遍接受的異常處理原則:

如果無法處理某個異常,那就不要捕獲它。

如果捕獲了一個異常,請不要胡亂處理它。

盡量在靠近異常被拋出的地方捕獲異常。

在捕獲異常的地方將它記錄到日志中,除非您打算將它重新拋出。

按照您的異常處理必須多精細來構造您的方法。

需要用幾種類型的異常就用幾種,尤其是對於應用程序異常。

第 1 點顯然與第 3 點相抵觸。實際的解決方案是以下兩者的折衷:您在距異常被拋出多近的地方將它捕獲;在完全丟失原始異常的意圖或內容之前,您可以讓異常落在多遠的地方。

注:盡管這些原則的應用遍及所有 EJB 異常處理機制,但它們並不是特別針對 EJB 異常處理的。

由於以上這些原因,把代碼組裝成產品並同時包含 System.out.println 並不是一種選擇。在測試期間使用 System.out.println ,然後在形成產品之前除去 System.out.println 也不是上策,因為這樣做意味著您的產品代碼與測試代碼運行得不盡相同。您需要的是一種聲明控制日志機制,以使您的測試代碼和產品代碼相同,並且當記錄日志以聲明方式關閉時,給產品帶來的性能開銷最小。

這裡的解決方案顯然是使用一個日志實用程序。采用恰當的編碼約定,日志實用程序將負責精確地記錄下任何類型的消息,不論是系統錯誤還是一些警告。所以,我們將在進一步講述之前談談日志實用程序。

日志領域:鳥瞰

每個大型應用程序在開發、測試及產品周期中都使用日志實用程序。在今天的日志領域中,有幾個角逐者,其中有兩個廣為人知。一個是 Log4J,它是來自 Apache 的 Jakarta 的一個開放源代碼的項目。另一個是 J2SE 1.4 捆綁提供的,它是最近剛加入到這個行列的。我們將使用 Log4J 說明本文所討論的最佳做法;但是,這些最佳做法並不特別依賴於 Log4J。

Log4J 有三個主要組件:layout、appender 和 category。Layou代表消息被記錄到日志中的格式。appender是消息將被記錄到的物理位置的別名。而 category則是有名稱的實體:您可以把它當作是日志的句柄。layout 和 appender 在 XML 配置文件中聲明。每個 category 帶有它自己的 layout 和 appender 定義。當您獲取了一個 category 並把消息記錄到它那裡時,消息在與該 category 相關聯的各個 appender 處結束,並且所有這些消息都將以 XML 配置文件中指定的 layout 格式表示。

Log4J 給消息指定四種優先級:它們是 ERROR、WARN、INFO 和 DEBUG。為便於本文的討論,所有異常都以具有 ERROR 優先級記錄。當記錄本文中的一個異常時,我們將能夠找到獲取 category(使用 Category.getInstance(String name) 方法)的代碼,然後調用方法 category.error() (它與具有 ERROR 優先級的消息相對應)。

盡管日志實用程序能幫助我們把消息記錄到適當的持久位置,但它們並不能根除問題。它們不能從產品日志中精確找出某個客戶的問題報告;這一便利技術留給您把它構建到您正在開發的系統中。

要了解關於 Log4J 日志實用程序或 J2SE 所帶的日志實用程序的更多信息,請參閱 參考資料部分。

異常的類別

異常的分類有不同方式。這裡,我們將討論從 EJB 的角度如何對異常進行分類。EJB 規范將異常大致分成三類:

JVM 異常:這種類型的異常由 JVM 拋出。OutOfMemoryError 就是 JVM 異常的一個常見示例。對 JVM 異常您無能為力。它們表明一種致命的情況。唯一得體的退出辦法是停止應用程序服務器(可能要增加硬件資源),然後重新啟動系統。

應用程序異常:應用程序異常是一種定制異常,由應用程序或第三方的庫拋出。這些本質上是受查異常(checked exception);它們預示了業務邏輯中的某個條件尚未滿足。在這樣的情況下,EJB 方法的調用者可以得體地處理這種局面並采用另一條備用途徑。

系統異常:在大多數情況下,系統異常由 JVM 作為 RuntimeException 的子類拋出。例如, NullPointerException 或 ArrayOutOfBoundsException 將因代碼中的錯誤而被拋出。另一種類型的系統異常在系統碰到配置不當的資源(例如,拼寫錯誤的 JNDI 查找(JNDI lookup))時發生。在這種情況下,系統就將拋出一個受查異常。捕獲這些受查系統異常並將它們作為非受查異常(unchecked exception)拋出頗有意義。最重要的規則是,如果您對某個異常無能為力,那麼它就是一個系統異常並且應當作為非受查異常拋出。

注: 受查異常是一個作為 java.lang.Exception 的子類的 Java 類。通過從 java.lang.Exception 派生子類,就強制您在編譯時捕獲這個異常。相反地, 非受查異常則是一個作為 java.lang.RuntimeException 的子類的 Java 類。從 java.lang.RuntimeException 派生子類確保了編譯器不會強制您捕獲這個異常。

EJB 容器怎樣處理異常

EJB 容器攔截 EJB 組件上的每一個方法調用。結果,方法調用中發生的每一個異常也被 EJB 容器攔截到。EJB 規范只處理兩種類型的異常:應用程序異常和系統異常。

EJB 規范把 應用程序異常定義為在遠程接口中的方法說明上聲明的任何異常(而不是 RemoteException )。應用程序異常是業務工作流中的一種特殊情形。當這種類型的異常被拋出時,客戶機會得到一個恢復選項,這個選項通常是要求以一種不同的方式處理請求。不過,這並不意味著任何在遠程接口方法的 throws 子句中聲明的非受查異常都會被當作應用程序異常對待。EJB 規范明確指出,應用程序異常不應繼承 RuntimeException 或它的子類。

當發生應用程序異常時,除非被顯式要求(通過調用關聯的 EJBContext 對象的 setRollbackOnly() 方法)回滾事務,否則 EJB 容器就不會這樣做。事實上,應用程序異常被保證以它原本的狀態傳送給客戶機:EJB 容器絕不會以任何方式包裝或修改異常。

系統異常被定義為受查異常或非受查異常,EJB 方法不能從這種異常恢復。當 EJB 容器攔截到非受查異常時,它會回滾事務並執行任何必要的清理工作。接著,它把該非受查異常包裝到 RemoteException 中,然後拋給客戶機。這樣,EJB 容器就把所有非受查異常作為 RemoteException (或者作為其子類,例如 TransactionRolledbackException )提供給客戶機。

對於受查異常的情況,容器並不會自動執行上面所描述的內務處理。要使用 EJB 容器的內部內務處理,您將必須把受查異常作為非受查異常拋出。每當發生受查系統異常(如 NamingException )時,您都應該通過包裝原始的異常拋出 javax.ejb.EJBException 或其子類。因為 EJBException 本身是非受查異常,所以不需要在方法的 throws 子句中聲明它。EJB 容器捕獲 EJBException 或其子類,把它包裝到 RemoteException 中,然後把 RemoteException 拋給客戶機。

雖然系統異常由應用程序服務器記錄(這是 EJB 規范規定的),但記錄格式將因應用程序服務器的不同而異。為了訪問所需的統計信息,企業常常需要對所生成的日志運行 shell/Perl 腳本。為了確保記錄格式的統一,在您的代碼中記錄異常會更好些。

注:EJB 1.0 規范要求把受查系統異常作為 RemoteException 拋出。從 EJB 1.1 規范起規定 EJB 實現類絕不應拋出 RemoteException 。

常見的異常處理策略

如果沒有異常處理策略,項目小組的不同開發者很可能會編寫以不同方式處理異常的代碼。由於同一個異常在系統的不同地方可能以不同的方式被描述和處理,所以,這至少會使產品支持小組感到迷惑。缺乏策略還會導致在整個系統的多個地方都有記錄。日志應該集中起來或者分成幾個可管理的單元。理想的情況是,應在盡可能少的地方記錄異常日志,同時不損失內容。在這一部分及其後的幾個部分,我將展示可以在整個企業系統中以統一的方式實現的編碼策略。您可以從 參考資料部分下載本文開發的實用程序類。

清單 1 顯示了來自會話 EJB 組件的一個方法。這個方法刪除某個客戶在特定日期前所下的全部訂單。首先,它獲取 OrderEJB 的 Home 接口。接著,它取回某個特定客戶的所有訂單。當它碰到在某個特定日期之前所下的訂單時,就刪除所訂購的商品,然後刪除訂單本身。請注意,拋出了三個異常,顯示了三種常見的異常處理做法。(為簡單起見,假設編譯器優化未被使用。)

清單 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 }

現在,讓我們用上面所示的代碼來研究一下所展示的三種異常處理做法的缺點。

拋出/重拋出帶有出錯消息的異常

NamingException 可能發生在行 101 或行 108。當發生 NamingException 時,這個方法的調用者就得到 RemoteException 並向後跟蹤該異常到行 119。調用者並不能告知 NamingException 實際是發生在行 101 還是行 108。由於異常內容要直到被記錄了才能得到保護,所以,這個問題的根源很難查出。在這種情形下,我們就說異常的內容被“吞掉”了。正如這個示例所示,拋出或重拋出一個帶有消息的異常並不是一種好的異常處理解決辦法。

記錄到控制台並拋出一個異常

FinderException 可能發生在行 102 或 109。不過,由於異常被記錄到控制台,所以僅當控制台可用時調用者才能向後跟蹤到行 102 或 109。這顯然不可行,所以異常只能被向後跟蹤到行 122。這裡的推理同上。

包裝原始的異常以保護其內容

RemoteException 可能發生在行 102、106、109、113 或 115。它在行 123 的 catch 塊被捕獲。接著,這個異常被包裝到 EJBException 中,所以,不論調用者在哪裡記錄它,它都能保持完整。這種辦法比前面兩種辦法更好,同時演示了沒有日志策略的情況。如果 deleteOldOrders() 方法的調用者記錄該異常,那麼將導致重復記錄。而且,盡管有了日志記錄,但當客戶報告某個問題時,產品日志或控制台並不能被交叉引用。

EJB 異常處理探試法

EJB 組件應拋出哪些異常?您應將它們記錄到系統中的什麼地方?這兩個問題盤根錯結、相互聯系,應該一起解決。解決辦法取決於以下因素:

您的 EJB 系統設計:在良好的 EJB 設計中,客戶機絕不調用實體 EJB 組件上的方法。多數實體 EJB 方法調用發生在會話 EJB 組件中。如果您的設計遵循這些准則,則您應該用會話 EJB 組件來記錄異常。如果客戶機直接調用了實體 EJB 方法,則您還應該把消息記錄到實體 EJB 組件中。然而,存在一個難題:相同的實體 EJB 方法可能也會被會話 EJB 組件調用。在這種情形下,如何避免重復記錄呢?類似地,當一個會話 EJB 組件調用其它實體 EJB 方法時,您如何避免重復記錄呢?很快我們就將探討一種處理這兩種情況的通用解決方案。(請注意,EJB 1.1 並未從體系結構上阻止客戶機調用實體 EJB 組件上的方法。在 EJB 2.0 中,您可以通過為實體 EJB 組件定義本地接口規定這種限制。)

計劃的代碼重用范圍:這裡的問題是您是打算把日志代碼添加到多個地方,還是打算重新設計、重新構造代碼來減少日志代碼。

您要為之服務的客戶機的類型:考慮您是將為 J2EE Web 層、單機 Java 應用程序、PDA 還是將為其它客戶機服務是很重要的。Web 層設計有各種形狀和大小。如果您在使用命令(Command)模式,在這個模式中,Web 層通過每次傳入一個不同的命令調用 EJB 層中的相同方法,那麼,把異常記錄到命令在其中執行的 EJB 組件中是很有用的。在多數其它的 Web 層設計中,把異常記錄到 Web 層本身要更容易,也更好,因為您需要把異常日志代碼添加到更少的地方。如果您的 Web 層和 EJB 層在同一地方並且不需要支持任何其它類型的客戶機,那麼就應該考慮後一種選擇。

您將處理的異常的類型(應用程序或系統):處理應用程序異常與處理系統異常有很大不同。系統異常的發生不受 EJB 開發者意圖的控制。因為系統異常的含義不清楚,所以內容應指明異常的上下文。您已經看到了,通過對原始異常進行包裝使這個問題得到了最好的處理。另一方面,應用程序異常是由 EJB 開發者顯式拋出的,通常包裝有一條消息。因為應用程序異常的含義清楚,所以沒有理由要保護它的上下文。這種類型的異常不必記錄到 EJB 層或客戶機層;它應該以一種有意義的方式提供給最終用戶,帶上指向所提供的解決方案的另一條備用途徑。系統異常消息沒必要對最終用戶很有意義。

處理應用程序異常

在這一部分及其後的幾個部分中,我們將更仔細地研究用 EJB 異常處理應用程序異常和系統異常,以及 Web 層設計。作為這個討論的一部分,我們將探討處理從會話和實體 EJB 組件拋出的異常的不同方式。

實體 EJB 組件中的應用程序異常

清單 2 顯示了實體 EJB 的一個 ejbCreate() 方法。這個方法的調用者傳入一個 OrderItemValue 並請求創建一個 OrderItem 實體。因為 OrderItemValue 沒有名稱,所以拋出了 CreateException 。

清單 2. 實體 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 的一個很典型的用法。類似地,如果方法的輸入參數的值不正確,則查找程序方法將拋出 FinderException 。

然而,如果您在使用容器管理的持久性(CMP),則開發者無法控制查找程序方法,從而 FinderException 永遠不會被 CMP 實現拋出。盡管如此,在 Home 接口的查找程序方法的 throws 子句中聲明 FinderException 還是要更好一些。RemoveException 是另一個應用程序異常,它在實體被刪除時被拋出。

從實體 EJB 組件拋出的應用程序異常基本上限定為這三種類型( CreateException 、 FinderException 和 RemoveException )及它們的子類。多數應用程序異常都來源於會話 EJB 組件,因為那裡是作出智能決策的地方。實體 EJB 組件一般是啞類,它們的唯一職責就是創建和取回數據。

會話 EJB 組件中的應用程序異常

清單 3 顯示了來自會話 EJB 組件的一個方法。這個方法的調用者設法訂購 n 件某特定類型的某商品。SessionEJB() 方法計算出倉庫中的數量不夠,於是拋出 NotEnoughStockException 。NotEnoughStockException 適用於特定於業務的場合;當拋出了這個異常時,調用者會得到采用另一個備用途徑的建議,讓他訂購更少數量的商品。

清單 3. 會話 EJB 組件中的樣本容器回調方法

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);
   }
}

處理系統異常

系統異常處理是比應用程序異常處理更為復雜的論題。由於會話 EJB 組件和實體 EJB 組件處理系統異常的方式相似,所以,對於本部分的所有示例,我們都將著重於實體 EJB 組件,不過請記住,其中的大部分示例也適用於處理會話 EJB 組件。

當引用其它 EJB 遠程接口時,實體 EJB 組件會碰到 RemoteException ,而查找其它 EJB 組件時,則會碰到 NamingException ,如果使用 bean 管理的持久性(BMP),則會碰到 SQLException 。與這些類似的受查系統異常應該被捕獲並作為 EJBException 或它的一個子類拋出。原始的異常應被包裝起來。清單 4 顯示了一種處理系統異常的辦法,這種辦法與處理系統異常的 EJB 容器的行為一致。通過包裝原始的異常並在實體 EJB 組件中將它重新拋出,您就確保了能夠在想記錄它的時候訪問該異常。

清單 4. 處理系統異常的一種常見方式

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);
}

避免重復記錄

通常,異常記錄發生在會話 EJB 組件中。但如果直接從 EJB 層外部訪問實體 EJB 組件,又會怎麼樣呢?要是這樣,您就不得不在實體 EJB 組件中記錄異常並拋出它。這裡的問題是,調用者沒辦法知道異常是否已經被記錄,因而很可能再次記錄它,從而導致重復記錄。更重要的是,調用者沒辦法訪問初始記錄時所生成的唯一的標識。任何沒有交叉引用機制的記錄都是毫無用處的。

請考慮這種最糟糕的情形:單機 Java 應用程序訪問了實體 EJB 組件中的一個方法 foo() 。在一個名為 bar() 的會話 EJB 方法中也訪問了同一個方法。一個 Web 層客戶機調用會話 EJB 組件的方法 bar() 並也記錄了該異常。如果當從 Web 層調用會話 EJB 方法 bar() 時在實體 EJB 方法 foo() 中發生了一個異常,則該異常將被記錄到三個地方:先是在實體 EJB 組件,然後是在會話 EJB 組件,最後是在 Web 層。而且,沒有一個堆棧跟蹤可以被交叉引用!

幸運的是,解決這些問題用常規辦法就可以很容易地做到。您所需要的只是一種機制,使調用者能夠:

訪問唯一的標識

查明異常是否已經被記錄了

您可以派生 EJBException 的子類來存儲這樣的信息。清單 5 顯示了 LoggableEJBException 子類:

清單 5. LoggableEJBException ― EJBException 的一個子類

public class LoggableEJBException extends EJBException {
   protected boolean isLogged;
   protected String uniqueID;
   public LoggableEJBException(Exception exc) {
   super(exc);
   isLogged = false;
   uniqueID = ExceptionIDGenerator.getExceptionID();
   }
   ..
   ..
}

類 LoggableEJBException 有一個指示符標志( isLogged ),用於檢查異常是否已經被記錄了。每當捕獲一個 LoggableEJBException 時,看一下該異常是否已經被記錄了( isLogged == false )。如果 isLogged 為 false,則記錄該異常並把標志設置為 true 。

ExceptionIDGenerator 類用當前時間和機器的主機名為異常生成唯一的標識。如果您喜歡,也可以用有想象力的算法來生成這個唯一的標識。如果您在實體 EJB 組件中記錄了異常,則這個異常將不會在別的地方被記錄。如果您沒有記錄就在實體 EJB 組件中拋出了 LoggableEJBException ,則這個異常將被記錄到會話 EJB 組件中,但不記錄到 Web 層中。

單 6 顯示了使用這一技術重寫後的清單 4。您還可以繼承 LoggableException 以適合於您的需要(通過給異常指定錯誤代碼等)。

清單 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

會話 EJB 組件中的系統異常

如果您決定記錄會話 EJB 異常,請使用 清單 7所示的記錄代碼;否則,請拋出異常,如 清單 6所示。您應該注意到,會話 EJB 組件處理異常可有一種與實體 EJB 組件不同的方式:因為大多數 EJB 系統都只能從 Web 層訪問,而且會話 EJB 可以作為 EJB 層的虛包,所以,把會話 EJB 異常的記錄推遲到 Web 層實際上是有可能做到的。

從清單 6 中,您可以看到 naming 和 SQL 異常在被拋出前被包裝到了 LoggableEJBException 中。但 RemoteException 是以一種稍有不同 ― 而且要稍微花點氣力 ― 的方式處理的。它之所以不同,是因為在 RemoteException 中,實際的異常將被存儲到一個稱為 detail (它是 Throwable 類型的)的公共屬性中。在大多數情況下,這個公共屬性保存有一個異常。如果您調用 RemoteException 的 printStackTrace ,則除打印 detail 的堆棧跟蹤之外,它還會打印異常本身的堆棧跟蹤。您不需要像這樣的 RemoteException 的堆棧跟蹤。

為了把您的應用程序代碼從錯綜復雜的代碼(例如 RemoteException 的代碼)中分離出來,這些行被重新構造成一個稱為 ExceptionLogUtil 的類。有了這個類,您所要做的只是每當需要創建 LoggableEJBException 時調用 ExceptionLogUtil.createLoggableEJBException(e) 。請注意,在清單 6 中,實體 EJB 組件並沒有記錄異常;不過,即便您決定在實體 EJB 組件中記錄異常,這個解決方案仍然行得通。清單 7 顯示了實體 EJB 組件中的異常記錄:

清單 7. 實體 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 中看到的是一個非常簡單明了的異常記錄機制。一旦捕獲受查系統異常就創建一個新的 LoggableEJBException 。接著,使用類 StackTraceUtil 獲取 LoggableEJBException 的堆棧跟蹤,把它作為一個字符串。然後,使用 Log4J category 把該字符串作為一個錯誤加以記錄。

StackTraceUtil 類的工作原理

在清單 7 中,您看到了一個新的稱為 StackTraceUtil 的類。因為 Log4J 只能記錄 String 消息,所以這個類負責解決把堆棧跟蹤轉換成 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() 方法把出錯消息記錄到 System.err 。Throwable 還有一個重載的 printStackTrace() 方法,它把出錯消息記錄到 PrintWriter 或 PrintStream 。上面的 StackTraceUtil 中的方法把 StringWriter 包裝到 PrintWriter 中。當 PrintWriter 包含有堆棧跟蹤時,它只是調用 StringWriter 的 toString() ,以獲取該堆棧跟蹤的 String 表示。

Web 層的 EJB 異常處理

在 Web 層設計中,把異常記錄機制放到客戶機端往往更容易也更高效。要能做到這一點,Web 層就必須是 EJB 層的唯一客戶機。此外,Web 層必須建立在以下模式或框架之一的基礎上:

模式:業務委派(Business Delegate)、FrontController 或攔截過濾器(Intercepting Filter)

框架:Struts 或任何包含層次結構的類似於 MVC 框架的框架

為什麼異常記錄應該在客戶機端上發生呢?嗯,首先,控制尚未傳到應用程序服務器之外。所謂的客戶機層在 J2EE 應用程序服務器本身上運行,它由 JSP 頁、servlet 或它們的助手類組成。其次,在設計良好的 Web 層中的類有一個層次結構(例如:在業務委派(Business Delegate)類、攔截過濾器(Intercepting Filter)類、http 請求處理程序(http request handler)類和 JSP 基類(JSP base class)中,或者在 Struts Action 類中),或者 FrontController servlet 形式的單點調用。這些層次結構的基類或者 Controller 類中的中央點可能包含有異常記錄代碼。對於基於會話 EJB 記錄的情況,EJB 組件中的每一個方法都必須具有記錄代碼。隨著業務邏輯的增加,會話 EJB 方法的數量也會增加,記錄代碼的數量也會增加。Web 層系統將需要更少的記錄代碼。如果您的 Web 層和 EJB 層在同一地方並且不需要支持任何其它類型的客戶機,那麼您應該考慮這一備用方案。不管怎樣,記錄機制不會改變;您可以使用與前面的部分所描述的相同技術。

真實世界的復雜性

到現在為止,您已經看到了簡單情形的會話和實體 EJB 組件的異常處理技術。然而,應用程序異常的某些組合可能會更令人費解,並且有多種解釋。清單 9 顯示了一個示例。OrderEJB 的 ejbCreate() 方法試圖獲取 CustomerEJB 的一個遠程引用,這會導致 FinderException 。OrderEJB 和 CustomerEJB 都是實體 EJB 組件。您應該如何解釋 ejbCreate() 中的這個 FinderException 呢?是把它當作應用程序異常對待呢(因為 EJB 規范把它定義為標准應用程序異常),還是當作系統異常對待?

清單 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 當應用程序異常對待,但把它當系統異常對待會更好。原因是:EJB 客戶機傾向於把 EJB 組件當黑箱對待。如果 createOrder() 方法的調用者獲得了一個 FinderException ,這對調用者並沒有任何意義。OrderEJB 正試圖設置客戶遠程引用這件事對調用者來說是透明的。從客戶機的角度看,失敗僅僅意味著該訂單無法創建。

這類情形的另一個示例是,會話 EJB 組件試圖創建另一個會話 EJB,因而導致了一個 CreateException 。一種類似的情形是,實體 EJB 方法試圖創建一個會話 EJB 組件,因而導致了一個 CreateException 。這兩個異常都應該當作系統異常對待。

另一個可能碰到的挑戰是會話 EJB 組件在它的某個容器回調方法中獲得了一個 FinderException 。您必須逐例處理這類情況。您可能要決定是把 FinderException 當應用程序異常還是系統異常對待。請考慮清單 1 的情況,其中調用者調用了會話 EJB 組件的 deleteOldOrder 方法。如果我們不是捕獲 FinderException ,而是將它拋出,會怎麼樣呢?在這一特定情況中,把 FinderException 當系統異常對待似乎是符合邏輯的。這裡的理由是,會話 EJB 組件傾向於在它們的方法中做許多工作,因為它們處理工作流情形,並且它們對調用者而言是黑箱。

另一方面,請考慮會話 EJB 正在處理下訂單的情形。要下一個訂單,用戶必須有一個簡檔 ― 但這個特定用戶卻還沒有。業務邏輯可能希望會話 EJB 顯式地通知用戶她的簡檔丟失了。丟失的簡檔很可能表現為會話 EJB 組件中的 javax.ejb.ObjectNotFoundException ( FinderException 的一個子類)。在這種情況下,最好的辦法是在會話 EJB 組件中捕獲 ObjectNotFoundException 並拋出一個應用程序異常,讓用戶知道她的簡檔丟失了。

即使是有了很好的異常處理策略,另一個問題還是經常會在測試中出現,而且在產品中也更加重要。編譯器和運行時優化會改變一個類的整體結構,這會限制您使用堆棧跟蹤實用程序來跟蹤異常的能力。這就是您需要代碼重構的幫助的地方。您應該把大的方法調用分割為更小的、更易於管理的塊。而且,只要有可能,異常類型需要多少就劃分為多少;每次您捕獲一個異常,都應該捕獲已規定好類型的異常,而不是捕獲所有類型的異常。

結束語

我們已經在本文討論了很多東西,您可能想知道我們已經討論的主要設計是否都物有所值。我的經驗是,即便是在中小型項目中,在開發周期中,您的付出就已經能看到回報,更不用說測試和產品周期了。此外,在宕機對業務具有毀滅性影響的生產系統中,良好的異常處理體系結構的重要性再怎麼強調也不過分。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved