概述
一般情況下,企業級應用都對應著複雜的業務邏輯,為了保證系統的健壯,必然需要面對各種系統業務異常和執行時異常。
不好的異常處理方式容易造成應用程式邏輯混亂,脆弱而難於管理。應用程式中充斥著零散的異常處理程式碼,使程式程式碼晦澀難懂、可讀性差,並且難於維護。
一個好的異常處理框架能為應用程式的異常處理提供統一的處理檢視,把異常處理從程式正常執行邏輯分離出來,以至於提供更加結構化以及可讀性的程式架構。另外,一個好的異常處理框架具備可擴充套件性,很容易根據具體的異常處理需求,擴充套件出特定的異常處理邏輯。
另外,異常處理框架從一定程度上依賴並體現系統架構層次。系統架構決定了系統中各個子系統,各個層次之間的互動,而異常處理框架則統一體現這種架構中的各種互動所發生的錯誤、異常。因此,異常處理框架是系統架構時就應該考慮的問題。
本文將對異常相關方面做一些討論,並進而探討一些關於構建穩健且可擴充套件的異常處理框架方面的視角或設計原則。由於本文引入一部分 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) 異常兩個概念。然後,著重介紹了對於一個應用系統從使用者和開發者兩個角度如何去看應用系統所發生的異常;通過多視角看應用系統異常對於設計一個合理的系統異常框架可以提供較好的設計原則。最後介紹了一個通用可擴充套件的異常處理框架,包括設計原則,異常層次結構的定義以及異常轉譯方面的考慮。
尤其對於比較大的軟體系統,異常處理框架是軟體系統體系結構需要考量的很重要的一方面。好的異常處理結構既能條理清晰地處理異常,又能保證異常處理的可擴充套件性與可用性,最後還需要保證系統的效能不受額外的損失。