異常以及異常處理框架探析
概述
一般情況下,企業級應用都對應著復雜的業務邏輯,為了保證系統的健壯,必然需要面對各種系統業務異常和運行時異常。
不好的異常處理方式容易造成應用程序邏輯混亂,脆弱而難于管理。應用程序中充斥著零散的異常處理代碼,使程序代碼晦澀難懂、可讀性差,并且難于維護。
一個好的異常處理框架能為應用程序的異常處理提供統一的處理視圖,把異常處理從程序正常運行邏輯分離出來,以至于提供更加結構化以及可讀性的程序架構。另外,一個好的異常處理框架具備可擴展性,很容易根據具體的異常處理需求,擴展出特定的異常處理邏輯。
另外,異常處理框架從一定程度上依賴并體現系統架構層次。系統架構決定了系統中各個子系統,各個層次之間的交互,而異常處理框架則統一體現這種架構中的各種交互所發生的錯誤、異常。因此,異常處理框架是系統架構時就應該考慮的問題。
本文將對異常相關方面做一些討論,并進而探討一些關于構建穩健且可擴展的異常處理框架方面的視角或設計原則。由于本文引入一部分 Java 語言中異常相關的概念,因此本文假設您熟悉 Java 相關基礎知識以及了解 Java EE 和 EJB 相關技術。
Java 異常基本概念
在 Java 程序設計語言中,使用一種異常處理的錯誤捕獲機制。當程序運行過程中發生一些異常情況,程序有可能被中斷、或導致錯誤的結果出現。在這種情況下,程序不會返回任何值,而是拋出封裝了錯誤信息的對象。Java 語言提供了專門的異常處理機制去處理這些異常。如圖 1 所示為 Java 異常體系結構:
圖 1. Java 異常體系結構
檢查 (Checked) 異常與非檢查 (Unchecked) 異常
Java 語言規范將派生于 Error 類或 RuntimeException 類的所有異常都稱為非檢查異常。除“非檢查異常”以外的所有異常都稱為檢查異常。檢查異常對方法調用者來說屬于必須處理的異常,當一個應用系統定義了大量或者容易產生很多檢查異常的方法調用,程序中會有很多的異常處理代碼。
如果一個異常是致命的且不可恢復并且對于捕獲該異常的方法根本不知如何處理時,或者捕獲這類異常無任何益處,筆者認為應該定義這類異常為非檢查異常,由頂層專門的異常處理程序處理;像數據庫連接錯誤、網絡連接錯誤或者文件打不開等之類的異常一般均屬于非檢查異常。這類異常一般與外部環境相關,一旦出現,基本無法有效地處理。
而對于一些具備可以回避異常或預料內可以恢復并存在相應的處理方法的異常,可以定義該類異常為檢查異常。像一般由輸入不合法數據引起的異常或者與業務相關的一些異常,基本上屬于檢查異常。當出現這類異常,一般可以經過有效處理或通過重試可以恢復正常狀態。
由于檢查異常屬于必須處理的異常,在存在大量的檢查異常的程序中,意味著很多的異常處理代碼。另外,檢查異常也導致破壞接口方法。如果一個接口上的某個方法已被多處使用,當為這個方法添加一個檢查異常時,導致所有調用此方法的代碼都需要修改處理該異常。當然,存在合適數量的檢查異常,無疑是比較優雅的,有助于避免許多潛在的錯誤。
到底何時使用檢查異常,何時使用非檢查異常,并沒有一個絕對的標準,需要依具體情況而定。很多情況,在我們的程序中需要將檢查異常包裝成非檢查異常拋給頂層程序統一處理;而有些情況,需要將非檢查異常包裝成檢查異常統一拋出。
多視角理解異常
從應用系統最終用戶的角度來看,用戶所面對的是系統中所提供的各種業務功能以及系統本身的管理功能。用戶并不理解系統內部是如何實現以及如何運行的,與系統開發者存在天然的鴻溝,系統運行對用戶來說如同一個黑盒一樣。對用戶而言,系統所出現的任何異常或錯誤,都屬于系統運行時異常。對于這些異常,有些異常是用戶可以理解并能解決的;而另外一些異常是用戶無法理解和解決的。當一個系統錯誤出現時,系統本身需要反饋給用戶一種可理解的業務相近的信息,從而用戶可以根據這些信息去盡可能解決問題。另一方面,有一類錯誤屬于系統內部運行異常或錯誤,用戶對此類錯誤根本無能為力。而這類異常同樣需要提供足夠詳細的信息,系統管理員可根據這類異常盡可能解決。一般情況下,如果異常面向系統用戶,以系統異常呈現更好。
從系統開發者角度來看,更多的是從系統內部邏輯來看異常。有一部分異常需要內部截獲處理,而另外一部分異常對于異常產生源而言無法進行有效處理,從而需要向外拋出異常以待合適的調用者進行處理。對于開發者而言,需要預見異常,并且需要考慮何時處理異常,何時拋出異常,必要時以某種方式記錄或通知異常。總而言之,開發者需要通過對系統運行時可能出現的異常盡可能地處理以保證系統的正常運行,并對于無法處理的異常以一種合適的方式記錄、通知、呈現以便找到發生異常的原因,從而解決或避免異常。
圖 2. 異常視圖
異常管理與異常框架
基本異常處理結構
如圖 3 所示,為一個常見的一般性異常處理代碼結構。其中,try 語句塊代表要運行的代碼并受異常監控,其中代碼發生異常時,會創建一個異常對象并拋出。
catch 語句塊會捕獲 try 代碼塊中發生的異常,并與自己的異常類型進行匹配,若匹配,則在其 catch 代碼塊中進行異常處理。catch 語句塊可以有多個,當 try 語句塊中拋出一個異常時,會針對每個 catch 塊進行匹配,一旦與某個 catch 塊匹配,就進入該 catch 塊處理并不再與其他 catch 塊匹配。
finally 語句塊是緊跟 catch 語句后的語句塊,該語句塊總會在方法返回前執行,無論 try 語句塊是否發生異常。
圖 3. 異常處理代碼結構
前面說過,一般當程序發生異常時,通常異常處理可能需要做一些通用處理,如異常日志記錄、異常通知,重定向到一個統一的錯誤頁面(如 Web 應用)等。如果這些通用異常處理放置于 catch 塊中,將導致大量的重復代碼,從而可能引起日志冗余、同一異常的實現多樣化等問題。另外,大量異常處理程序放置于 catch 塊中造成程序的高耦合性。為了解決此類問題,有必要分離出異常處理程序、統一異常處理風格、降低耦合性、增強異常處理模塊的復用程度。通常的異常處理模式包括業務委托模式(Business Delegate)、前端控制器模式(Front Controller)、攔截過濾器模式(Intercepting Filter)、AOP 模式、模板方法模式等。
一般性異常處理框架
為了解決基本異常處理結構所帶來的問題,不妨把異常相關處理委托給一個專用 Service 代理,從而分離出異常處理業務,以一種統一的方式和邏輯進行處理,如圖 4 所示。異常 Service 主要處理兩個方面:一方面是要按照實際系統要求調用通用處理程序處理異常,如日志記錄、異常界面展示、異常通知等;另外一方面,需要通過過濾所接受到的異常類型,找到定制的異常處理程序進行異常處理。對于異常 Service 的應用一般可以通過在系統的頂層進行異常自動攔截(一般多層系統中尤為普遍,如放置于前端控制器 Front Controller),或者主動調用異常 Service 進行處理。
圖 4. 一般性異常處理框架
如圖 5 所示類圖顯示了一個具體的異常處理框架:
圖 5. 異常處理框架類圖樣例
該框架主要包括三部分:異常 Service、異常處理過濾器、系統異常層次定義。
異常 Service:整個異常框架的核心,通常用于主動攔截異常或被動調用處理異常。根據具體業務需要,調用通用服務程序進行一般化異常處理,如日志服務、異常消息通知服務等;另外,異常 Service 最主要目的用于攔截并處理異常,其需要維護定制的異常處理器鏈,用于特定類型異常的特定處理。
異常處理過濾器:維護系統中各種異常處理器,包括增加異常處理器、刪除異常處理器、查找異常處理器操作等。其中最主要的功能是接收特定異常并找到與之匹配的異常處理器進行處理。異常處理過濾器具體實現可以通過一個配置文件維護所有異常與異常處理器的映射,另外可以通過另外一個配置文件維護系統中所有已定義的異常處理器。從而,異常處理過濾器可以通過配置文件進行初始化操作。
異常層次定義:異常層次定義應用系統的異常基礎結構,是異常處理過濾器所處理的目標異常類型集合。
異常層次定義
異常層次結構應該以一種普遍通用的原則定義。為此,我們可以利用面向對象語言具備多態的性質,隱藏異常的實際實現。對于異常 Service 而言,只需要捕獲最基本的應用程序異常 AppException,異常處理過濾器會自動過濾實際異常類型并找到相應的異常處理器。另外,在方法的 throws 語句中勿需放入大量的檢查異常;對方法調用者也不會出現混亂的 catch 塊,最多可能只存在一個用于處理基本應用程序異常 AppException(委托給異常 Service 處理)。
前面的章節講過,應用系統異常可以從用戶和開發者兩個視角去考慮。因此,我們可以把異常劃分為業務操作異常和系統內部運行時異常兩種類型。拋出業務級異常或系統運行時異常的決策,需要與應用系統本身的架構層次相結合,考慮所要處理異常的層次。如圖 6 所示為一個典型的異常層次結構:
圖 6. 異常層次類圖樣例
其中,BussinessException 屬于基本業務操作異常,所有業務操作異常都繼承于該類。例如,通常 UI 層或 Web 層是由系統最終用戶執行業務操作驅動,因此最好拋出業務類異常。ServiceException 一般屬于中間服務層異常,該層操作引起的異常一般包裝成基本 ServiceException。DAOException 屬于數據訪問相關的基本異常。
對于多層系統,每一層都有該層的基本異常。在層與層之間的信息傳遞與方法調用時候,一旦在某層發生異常,傳遞到上一層的時候,一般包裝成該層異常,直至與用戶最接近的 UI 層,從而轉化成用戶友好的錯誤信息。
異常轉譯以及異常鏈
前面關于檢查異常和非檢查異常的論述中提到,在存在大量的檢查異常的程序中,意味著很多的異常處理代碼,導致晦澀的異常處理,并且檢查異常容易破壞接口方法。為了解決檢查異常帶來的缺陷,我們可以利用異常轉譯的方法,將檢查異常轉化為非檢查異常,由異常 Service 攔截處理。
異常轉譯就是將一種異常轉換為另一種異常。異常轉譯針對所有繼承 Throwable 超類的異常類而言的。如下圖 7 中代碼所示展示了異常轉譯的一個例子:
圖 7. 異常轉譯代碼樣例
對于任何一個應用系統而言,系統運行過程中所發生的任何異常或錯誤都應該以合適的方式通知用戶或記錄;由于異常源可能來自很多方面,其所拋出的異常大多不能為系統用戶所理解,此時就必須將各種類型的異常轉化成各種用戶可理解的異常。這也是異常框架所需要關注和解決的方面。
在異常的層層轉譯過程中,就形成一個異常鏈。整個異常鏈保存了每一層的異常原因。通過遞歸調用 getCause() 方法可以遍歷所有的異常原因。需要注意的是,在形成異常鏈的過程中,會消耗較多的資源,導致系統性能降低。這里涉及異常原理,在此不必多說,有興趣可查閱相關資料。在本文提出的異常框架中,異常 service 可以截獲來自系統各層的異常,而勿需異常層層轉譯。
結束語
本文首先簡要介紹了異常的基本概念以及 Java 語言中基本異常體系結構,重點介紹了 Java 異常中的檢查 (checked) 異常和非檢查 (unchecked) 異常兩個概念。然后,著重介紹了對于一個應用系統從用戶和開發者兩個角度如何去看應用系統所發生的異常;通過多視角看應用系統異常對于設計一個合理的系統異常框架可以提供較好的設計原則。最后介紹了一個通用可擴展的異常處理框架,包括設計原則,異常層次結構的定義以及異常轉譯方面的考慮。
尤其對于比較大的軟件系統,異常處理框架是軟件系統體系結構需要考量的很重要的一方面。好的異常處理結構既能條理清晰地處理異常,又能保證異常處理的可擴展性與可用性,最后還需要保證系統的性能不受額外的損失。
關于作者
王建光為 IBM 一名軟件工程師,目前正參與開發 IBM System Director 產品。他還曾經領導過大型連鎖系統項目,開發過 C++、Java 產品。目前對軟件架構、軟件設計模式等領域非常感興趣。