高效的java异常(Effective Java Exceptions)
摘要
Java開發人員可以做出的最重要的架構決策之一是如何使用Java異常模型。Java異常一直是社區爭論的主題。 有些人認為Java語言中的checked(受檢)異常是一個失敗的實驗。 本文認為,錯誤不在于Java模型,而在于Java庫設計者未能認知到方法失敗的兩個基本原因。 本文提倡一種思考異常情形性質的方法,并描述有助于您設計的設計模式。 最后,本文討論了異常處理作為面向切面編程模型中的橫切關注點。 正確使用Java異常是一個很大的好處。 本文將幫助您做到這一點。
為什么異常事關緊要
Java應用程序中的異常處理可以告訴您很多用于構建它的體系結構的強大。 架構是關于在應用程序的所有級別上一致地做出和遵循的決定。 要做出的最重要的決定之一是應用程序中的類,子系統或應用內的各個層的相互通信的方式。 Java異常是方法傳遞操作的替代結果的方式,因此在你的應用體系結構中值得特別關注。
衡量Java架構師技能和開發團隊紀律的一個好方法是查看其應用程序中的異常處理代碼。 首先要注意的是,有多少代碼用于捕獲異常,記錄異常,嘗試確定發生了什么情況以及將一個異常轉換為另一個異常。 清晰,緊湊和一致的(coherent)異常處理表明團隊具有一致的使用Java異常的方法。 當異常處理代碼的數量可能超過其他所有代碼時,您可以判斷團隊成員之間的交流已經崩潰(或者從一開始就不存在),并且每個人都以“他們自己的方式”處理異常。
臨時異常處理的結果是非常容易預測的。 如果你問團隊成員他們為什么在他們的代碼中的特定地方拋出、捕獲或忽略異常,回答通常是“我不知道還能做什么”。 如果你問他們如果他們代碼中的異常確實發生了會怎樣,那么他們就會皺眉頭了,你會得到一個類似于“我不知道。我們從未測試過它”的回答。
你可以通過查看Java組件的客戶端(client)代碼來判斷它是否有效地使用了Java異常。 如果它們包含大量邏輯以確定操作何時失敗,為什么失敗,原因幾乎總是因為組件的錯誤報告設計(error reporting design)有問題。 有缺陷的報告在客戶端產生大量“記錄和遺忘”(log and forget)代碼,很少有用。 最糟糕的是扭曲的邏輯路徑,嵌套的try/catch/finally塊,以及其它導致應用程序脆弱且難以管理的混亂。
將異常作為事后補救(或根本不解決它們)是導致軟件項目混亂和延期的主要原因。 異常處理是一個涉及設計的所有部分的問題。 建立異常的架構約定應該是項目需要做的第一個決策。 正確地使用Java異常模型將大大有助于保持應用程序的簡單性,可維護性和正確性。
挑戰異常標準
正確使用Java異常模型需要怎么做一直是爭論的主題。 Java不是第一種支持異常語義的編程語言; 但是,它是編譯器強制執行規則以管理某些異常的聲明和處理的第一種語言。 許多人認為編譯時異常檢查有助于精確的軟件設計,與其他語言特性很好地協調工作。 圖1顯示了Java異常層次結構。
通常,Java編譯器會強制一個拋出基于java.lang.Throwable的異常的方法,在方法聲明中有“throws”子句。 此外,編譯器驗證方法的客戶端是捕獲聲明的異常類型還是指定它們自己拋出該異常類型。 這些簡單的規則對全世界的Java開發人員產生了深遠的影響。
編譯器將Throwable繼承樹的兩個分支的異常檢查行為分開處理。 java.lang.Error和java.lang.RuntimeException的子類編譯時不用檢查。 在這兩者中,軟件設計者通常對運行時異常更感興趣。 術語“未經檢查”(unchecked)的異常即指運行時異常,它們區別于所有其它“已檢查”(checked)的異常。
我想,那些同樣重視Java強類型的人也接受了checked異常。畢竟,編譯器對數據類型施加的約束是對嚴格的編碼和精確的思考的一種鼓勵。編譯時類型檢查有助于防止在運行時出現令人討厭的意外情況。編譯時異常檢查的工作方式類似,提醒開發人員方法具有需要解決的潛在的其他結果。
在早期,建議盡可能使用已檢查的異常,以最大限度地利用編譯器提供的幫助來生產無錯誤的軟件。 Java庫的API設計者顯然訂閱了已檢查的異常標準,使用這些異常來廣泛地模擬庫方法中可能發生的任何意外事件。在J2SE 5.1 API規范中,已檢查的異常類型仍然超過未經檢查的類型的兩倍以上。
對于程序員來說,似乎Java庫類中的大多數常用方法都會為每個可能的失敗聲明checked異常。例如,java.io包嚴重依賴于checked異常IOException。至少有63個Java庫包直接或通過其中的幾個子類之一拋出此異常。
I/O失敗是一個嚴重但非常罕見的事件。最重要的是,您的代碼通常無法從一個IO失敗中恢復。 Java程序員發現自己在簡單的Java庫方法調用中被迫提供IOException和類似的不可恢復的事件。捕獲這些異常會給原本簡單的代碼增加混亂,因為在catch塊中幾乎做不了什么事情來幫助解決這個問題。沒有捕獲它們可能更糟,因為編譯器要求你將它們添加到方法拋出的異常列表中。這暴露了良好的面向對象設計自然的應該隱藏的實現細節。
這種沒有贏家的局面導致了大多數現在我們警告的臭名昭著的反模式異常處理。它還以正確的和錯誤的方法提出了許多建議來構建變通方法。
一些Java名人開始質疑Java的checked異常模型是否是一個失敗的實驗。有些事情確實失敗了,但它與在Java語言中包含異常檢查無關。失敗的原因在于Java API設計者認為大多數失敗情況都是相同的,并且可以通過相同的異常進行傳達。
故障和意外(Faults and Contingencies)
考慮一個虛構的銀行應用程序中的CheckingAccount類。 CheckingAccount屬于客戶,維護當前余額,并且能夠接受存款,接受支票上的止付訂單以及處理收到的支票。 CheckingAccount對象必須協調并發線程的訪問,其中任何一個線程都可能改變其狀態。CheckingAccount的processCheck()方法接受Check對象作為參數,通常從帳戶余額中扣除支票金額。但是調用processCheck()的檢查清除客戶端必須準備好應對兩個意外事件。首先,CheckingAccount可能有一個為支票注冊的止付訂單。其次,賬戶可能沒有足夠的資金來支付支票金額。
因此,processCheck()方法可以以三種可能的方式響應其調用者。正常的響應是檢查得到處理,方法簽名中聲明的結果返回給調用服務。這兩個意外響應代表了非常真實的銀行領域中需要傳達給支票清算客戶的情況。所有三個processCheck()響應都是有意設計的,用于模擬典型支票賬戶的行為。
在Java中表示意外響應的自然方式是定義兩個異常,比如StopPaymentException和InsufficientFundsException。客戶端忽略這些是不對的,因為它們肯定會被拋入應用程序的正常操作中。它們有助于表達方法的完整行為,與方法簽名一樣重要。
客戶端可以輕松處理這兩種異常。如果支票上的付款被停止,客戶端可以將支票路由到特殊處理的邏輯。如果資金不足,客戶端可以從客戶的儲蓄賬戶轉移資金以支付支票,然后再試一次。
意外事件以及使用CheckingAccount API的正常流程都被預測了。它們不代表軟件或運行環境的故障。將這些與由于與CheckingAccount類的內部實現細節相關的問題而可能出現的真正的失敗進行對比。
想象一下,CheckingAccount在數據庫中維護其持久狀態,并使用JDBC API來訪問它。由于與CheckingAccount的實現無關的原因,該API中幾乎每種數據庫訪問方法都有可能失敗。例如,有人可能忘記打開數據庫服務器,拔掉網絡電纜或更改訪問數據庫所需的密碼。
JDBC依賴于單個checked異常SQLException來報告可能出錯的所有內容。大多數可能出錯的地方都與配置數據庫,連接數據庫及其所在的硬件有關。processCheck()方法無法以有意義的方式處理這些情況。processCheck()至少知道它自己的實現。調用堆棧中的上游方法具有更小的解決問題的可能性。
CheckingAccount示例說明了方法執行無法返回其預期結果的兩個基本原因。它們值得一些描述性術語:
意外(Contingency)
方法可以用拋出異常的方式表達可能的異常(方法罕見的結果,但是是方法可能的返回值,只是不太常見,比如賬戶余額不足異常)。 該方法的調用者具有應對它們的策略。
故障(Fault)
一種出乎意料的情況,使得方法不能返回預期的值,如果不參考方法的內部實現,則無法對其進行描述。
使用此術語,停止付款訂單和透支是processCheck()方法的兩種可能的意外(Contingency)情況。 SQL問題表示可能的故障(Fault)情況。 processCheck()的調用者應該有一種方法來處理意外(Contigency)事件,但如果發生故障(Fault)這種情況,則無法合理地預期處理它。
匹配Java異常
在意外和故障方面考慮“可能出現的問題”將大大有助于在應用程序架構中建立Java異常約定。
| 被認為是 | 設計中的一部分 | 令人厭惡的"驚喜" |
| 是否被期望發生 | 希望很少發生 | 希望永不發生 |
| 誰關注它 | 調用方法的上游方法 | 需要處理問題的人 |
| 例子 | 替代返回模式(Alternative return modes) | 程序bug、硬件故障、配置錯誤、缺少文件、服務器訪問不了 |
| 最佳匹配 | checked異常 | unchecked異常 |
意外情況很好地映射到Java的checked異常。由于它們是方法語義契約的組成部分,因此有必要使用編譯器的幫助來確保它們得到解決。如果你發現編譯器強制你在不方便的情況下處理或聲明異常,那么你的設計需要重構一下了。這實際上是件好事。
人們對故障情況(Fault conditions)很感興趣,但是軟件邏輯卻不然。那些扮演“軟件直腸病學家”角色的人需要有關故障的信息來診斷和修復導致它們發生的任何事情。因此,未經檢查的Java異常是表示故障的完美方式。它們允許故障通知通過調用堆棧上的所有方法不受影響地滲透到專門設計用于捕獲它們的層面(level),捕獲它們包含的診斷信息,并為程序活動提供受控且優雅的結論。能夠產生故障的方法不需要在方法上聲明它,不需要上游方法來捕獲它們,并且方法的實現保持適當隱藏 - 所有這些都具有最少的代碼混亂。
較新的Java API(如Spring Framework和Java Data Objects庫)很少或根本不依賴于受檢(checked)異常。 Hibernate ORM框架從3.0版開始重新定義了主要部分,以消除checked異常的使用。這反映了這樣的認識,即這些框架所報告的絕大多數異常情況都是不可恢復的,這些異常情況源于方法調用的錯誤編碼或某些底層組件(如數據庫服務器)的故障。實際上,通過強制調用者捕獲或聲明此類異常幾乎沒有任何好處。
在你的架構中處理故障
在您的架構中有效處理故障的第一步是承認你需要這樣做。 對于那些以創造無可挑剔的軟件能力而自豪的工程師而言,很難接受這種認可。 下面是一些有幫助的理由。 首先,你的應用程序將花費大量時間進行開發,其中錯誤是司空見慣的。 提供程序員造成的故障將使你的團隊更容易診斷和修復它們。 其次,在Java庫中(過度)使用checked異常來處理故障情況將迫使你的代碼處理它們,即使你的調用順序完全正確。 如果沒有適當的故障處理框架,臨時異常處理會將熵注入你的應用程序。
一個成功的錯誤處理框架需要完成四個目標:
- 最小化代碼混亂
- 捕獲錯誤并且保存診斷信息
- 提醒正確的人
- 優雅地退出程序
錯誤會分散應用程序的真實目的。 因此,用于處理它們的代碼量應該是最小的,并且理想地,與應用程序的語義部分隔離。 故障處理必須滿足負責糾正它們的人的需要。 他們需要知道發生了故障,并獲得有助于他們弄清楚原因的信息。 即使根據定義,故障不可恢復,良好的故障處理也會嘗試以優雅的方式終止遇到故障的活動。
為故障情況使用unchecked異常
有許多理由使架構決策用unchecked異常來表示故障情況。 Java運行時通過拋出RuntimeException子類(如ArithmeticException和ClassCastException)來"獎勵"編程錯誤,為你的體系結構設置先例。unchecked異常通過讓上游方法不用處理和它意圖無關的錯誤而使得代碼更整潔。
你的故障處理策略應該認識到Java庫和其他API中的方法可能使用checked異常來表示應用程序上下文中的故障條件。在這種情況下,采用體系結構約定來捕獲發生的API異常,將其視為故障,并拋出unchecked異常以發出故障信號并捕獲診斷信息。
在這種情況下拋出的特定異常類型應該由你的體系結構定義。不要忘記,故障異常的主要目的是傳達將被記錄的診斷信息,以幫助人們找出問題所在。使用多個故障異常類型可能有點過分,因為你的體系結構將完全相同地處理它們。嵌入在單個故障異常類型中的良好描述性消息將在大多數情況下完成工作。使用Java的通用RuntimeException來表示你的故障情況很容易。從Java 1.4開始,RuntimeException與所有throwable一樣,支持異常鏈,允許你捕獲并報告引發故障的checked異常。
你可以選擇為故障報告定義自己的unchecked異常。如果你需要使用不支持異常鏈的Java 1.3或更早版本,則必須執行此操作。實現類似的鏈功能可以很容易地捕獲和轉換構成應用程序故障的checked異常。你的應用程序可能需要在故障報告異常中執行特殊操作。這將是為你的體系結構創建RuntimeException子類的另一個原因。
建立一個故障屏障(Establish a fault barrier)
決定拋出哪個異常以及何時拋出它是你的故障處理框架的重要決策。關于何時捕獲故障異常以及之后要做什么的問題同樣重要。這里的目標是使應用程序的功能部分免于處理故障的責任。關注點分離通常是件好事,負責處理故障的中央設施將在未來發揮作用。
在故障屏障模式中,任何應用程序組件都可以引發故障異常,但只有充當“故障屏障”的組件才能捕獲它們。采用這種模式消除了開發人員在本地插入處理故障的大量復雜代碼。故障屏障邏輯上位于調用堆棧的頂部,在觸發默認操作之前它停止向上傳播異常。默認操作根據應用程序類型的不同而不同。對于獨立Java應用程序,這意味著活動線程終止。對于由應用程序服務器托管的Web應用程序,這意味著應用程序服務器向瀏覽器發送不友好(且令人尷尬)的響應。
故障屏障組件的第一個職責是記錄故障異常中包含的信息,以便將來采取措施。到目前為止,應用程序日志是執行此操作的最佳位置。異常的鏈接消息,堆棧跟蹤等對于診斷人員來說都是有價值的信息。發送故障信息的最差位置是跨用戶界面。讓你的應用程序的客戶端參與調試過程對你或你的客戶來說幾乎沒有任何好處。如果你真的想要用診斷信息繪制用戶界面,則可能意味著你的日志記錄策略需要改進。
故障屏障的下一個責任是以受控方式關閉操作。這意味著什么取決于你的應用程序設計,但通常涉及生成對可能正在等待的客戶端的通用響應。如果應用程序是Web服務,則意味著使用soap:Server的<faultcode>和通用<faultstring>失敗消息在響應中構建SOAP <fault>元素。如果應用程序與Web瀏覽器通信,屏障將安排發送通用HTML響應,指示無法處理請求。
在Struts應用程序中,你的故障屏障可以采用全局異常處理器的形式,該處理器被配置為處理RuntimeException的任何子類。你的故障屏障類將擴展org.apache.struts.action.ExceptionHandler,根據需要覆蓋方法以實現你需要的自定義處理。這將處理在處理Struts操作期間明顯發現的無意生成的故障條件和故障情況。圖2顯示了意外事件和故障異常。
如果您正在使用Spring MVC框架,則可以通過擴展SimpleMappingExceptionResolver并將其配置為處理RuntimeException及其子類來輕松構建故障屏障。 通過重寫resolveException()方法,你可以在使用超類方法將請求路由到發送通用錯誤顯示的視圖之前添加所需的任何自定義處理。
當你的架構包含故障屏障并且開發人員意識到它時,編寫一次性故障異常處理代碼的誘惑就會大大減少。 結果是整個應用程序中的代碼更清晰,更易于維護。
你架構中的意外處理(Contingency Handling in Your Architecture)
隨著故障處理降級到屏障,主要組件之間的應急通信變得更加簡單。意外事件代表一種替代方法結果,與主要的返回結果同樣重要。因此,checked異常類型是傳達意外情況存在的良好工具,并提供處理意外所需的信息。這種做法需要Java編譯器的幫助,以提醒開發人員他們正在使用的API的所有方面以及需要提供所有方法結果。
通過單獨使用方法的返回類型可以傳達簡單的意外事件。例如,返回空引用而不是實際對象可以表示因為某種原因無法創建對象。 Java I/O方法通常返回整數值-1,而不是字節值或字節數,以表示文件結束的情況。如果你的方法的語義足夠簡單,那么替代返回值可能是最好的選擇,因為它們消除了異常帶來的開銷。缺點是方法調用者負責測試返回值以查看它是主要結果還是偶然結果。但是,編譯器不會強制方法調用者進行該測試。
如果方法具有void返回類型,則異常是表示發生意外事件的唯一方法。如果方法返回對象引用,則返回值可以表達的詞匯表限制為兩個值(null和non-null)。如果方法返回一個整數值,則可以通過選擇保證不與主要返回值沖突的值來表達幾個意外情況。但現在我們已經進入了錯誤代碼檢查的世界,開發了Java異常模型以避免這種情況。
提供一些有用的東西
定義不同的故障(fault)報告異常類型沒有多大意義,因為故障屏障對它們的處理方式完全相同。 意外(contingency)異常是完全不同的,因為它們旨在向方法調用者傳達不同的條件,你的體系結構可能會指定這些異常(指的是contingency exception)應該擴展java.lang.Exception或指定的基類。
不要忘記你的異常是完整的Java類型,它們可以包含專門的字段,方法,甚至可以為你的獨特目的而設計的構造函數。 例如,假想的CheckingAccount processCheck()方法拋出的InsufficientFundsException類型可以包含一個OverdraftProtection對象,該對象能夠幫助轉入所需的資金以彌補賬戶余額的不足。
記錄還是不記錄日志
記錄故障(fault)異常是有意義的,因為它們的目的是引起人們注意需要糾正的情況。 對于意外(contingency)異常,則不能這么說,因為它們可能代表相對罕見的事件,但預計它們中的每一個都會在你的應用程序生命周期中發生。 如果有的話,它們僅僅表示應用程序的運行方式與其工作方式相同罷了。 將記錄代碼添加到意外(contingency)捕獲塊會增加代碼的混亂,而沒有實際的好處。 如果意外事件代表重大事件,則在拋出意外事件異常以警告其調用者之前,生成記錄事件的日志的方法可能更好。
異常切面
在面向切面(也叫方面)編程(AOP)術語中,故障(fault)和意外(contingency)處理是橫切關注的問題。 例如,要實現故障屏障模式,所有參與的類必須遵循常見的約定:
- 故障屏障方法必須位于遍歷參與類的方法調用圖的頭部。
- 它們都必須使用unchecked異常來表示故障(fault)情況。
- 它們必須全部使用故障屏障期望接收的特定unchecked異常類型。
- 它們都必須從被認為是執行上下文中的錯誤發生的較低層方法中捕獲并轉換checked異常。
- 它們不得干擾故障異常在通往屏障的途中傳播。
這些擔憂跨越了其他無關類的界限。結果是一小部分分散的故障處理代碼和屏障類與參與者之間的隱式耦合(盡管仍然比完全不使用模式有很大改進!)。AOP允許將故障處理問題封裝在應用于參與類的公共Aspect中。諸如AspectJ和Spring AOP之類的Java AOP框架將異常處理識別為可以附加故障處理行為(或建議)的連接點。以這種方式,可以放寬綁定故障屏障模式中的參與者的約定。故障處理現在可以駐留在獨立的外部方面,從而無需將“屏障”方法放置在方法調用序列的頭部。
如果你在架構中使用AOP,則故障(fault)和意外(contingency)處理是適用于整個應用程序的方面的理想候選者。全面探討故障和意外處理如何在AOP世界中發揮作用將成為未來文章的一個有趣話題。
結論
雖然Java異常模型在其生命周期中產生了激烈的討論,但它在被正確應用時提供了極大的價值。 作為架構師,你需要建立從模型中獲得最大收益的約定。 在故障和意外事件方面考慮例外可以幫助你做出正確的選擇。 正確使用Java異常模型將使你的應用程序簡單,可維護和正確。 面向切面編程技術可以通過將故障和意外處理識別為橫切關注點來為你的架構提供一些明確的優勢。
原文鏈接:https://www.oracle.com/technetwork/java/effective-exceptions-092345.html
總結
以上是生活随笔為你收集整理的高效的java异常(Effective Java Exceptions)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android9怎么剪辑音频,音频剪辑铃
- 下一篇: WPF 悬浮键盘