異常處理是 Java 開發中的一個重要部分,是爲了處理任何錯誤情況,好比資源不可訪問,非法輸入,空輸入等等。Java 提供了幾個異常處理特性,以try,catch 和 finally 關鍵字的形式內建於語言自身之中。Java 編程語言也容許建立新的自定義異常,並經過使用 throw 和 throws關鍵字拋出它們。在Java編程中,Java 的異常處理不僅僅是知道語法這麼簡單,它必須遵循標準的 JDK 庫,和處理錯誤和異常的開源代碼。java
這裏咱們將討論一些關於異常處理的 Java 最佳實踐。在咱們討論異常處理的最佳實踐以前,先讓咱們瞭解下幾個重要的概念,那就是什麼是異常以及異常的分類。數據庫
異常的英文單詞是 exception,異常本質上是程序上的錯誤,包括程序邏輯錯誤和系統錯誤。好比使用空的引用、數組下標越界、內存溢出錯誤等,這些都是意外的狀況,背離咱們程序自己的意圖。錯誤在咱們編寫程序的過程當中會常常發生,包括編譯期間和運行期間的錯誤,在編譯期間出現的錯誤有編譯器幫助咱們一塊兒修正,然而運行期間的錯誤便不是編譯器力所能及了,而且運行期間的錯誤每每是難以預料的。倘若程序在運行期間出現了錯誤,若是置之不理,程序便會終止或直接致使系統崩潰,顯然這不是咱們但願看到的結果。編程
如何對運行期間出現的錯誤進行處理和補救呢?Java 提供了異常機制來進行處理,經過異常機制來處理程序運行期間出現的錯誤。經過異常機制,咱們能夠更好地提高程序的健壯性。數組
Java 把異常看成對象來處理,並定義一個基類 java.lang.Throwable 做爲全部異常的超類。服務器
Java 包括三種類型的異常: 檢查性異常(checked exceptions)、非檢查性異常(unchecked Exceptions) 和錯誤(errors)。網絡
全部不是 Runtime Exception 的異常,統稱爲 Checked Exception,又被稱爲檢查性異常。這類異常的產生不是程序自己的問題,一般由外界因素形成的。爲了預防這些異常產生時,形成程序的中斷或獲得不正確的結果,Java 要求編寫可能產生這類異常的程序代碼時,必定要去作異常的處理。編程語言
Java 語言將派生於 RuntimeException 類或 Error 類的全部異常稱爲非檢查性異常。測試
Java 異常層次結構圖以下圖所示:線程
在瞭解了異常的基本概念以及分類後,如今讓咱們開始探索異常處理的最佳實踐吧。debug
catch (NoSuchMethodException e) { return null; }
雖然捕捉了異常可是卻沒有作任何處理,除非你確信這個異常能夠忽略,否則不該該這樣作。這樣會致使外面沒法知曉該方法發生了錯誤,沒法肯定定位錯誤緣由。
public void foo() throws Exception { //錯誤方式 }
必定要避免出現上面的代碼示例,它破壞了檢查性異常的目的。 聲明你的方法可能拋出的具體檢查性異常,若是隻有太多這樣的檢查性異常,你應該把它們包裝在你本身的異常中,並在異常消息中添加信息。 若是可能的話,你也能夠考慮代碼重構。
public void foo() throws SpecificException1, SpecificException2 { //正確方式 }
try { someMethod(); } catch (Exception e) { //錯誤方式 LOGGER.error("method has failed", e); }
捕獲異常的問題是,若是稍後調用的方法爲其方法聲明添加了新的檢查性異常,則開發人員的意圖是應該處理具體的新異常。若是你的代碼只是捕獲異常(或 Throwable),永遠不會知道這個變化,以及你的代碼如今是錯誤的,而且可能會在運行時的任什麼時候候中斷。
這是一個更嚴重的麻煩,由於 Java Error 也是 Throwable 的子類,Error 是 JVM 自己沒法處理的不可逆轉的條件,對於某些 JVM 的實現,JVM 可能實際上甚至不會在 Error 上調用 catch 子句。
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " + e.getMessage()); //錯誤方式 }
這破壞了原始異常的堆棧跟蹤,而且始終是錯誤的,正確的作法是:
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " , e); //正確方式 }
catch (NoSuchMethodException e) { //錯誤方式 LOGGER.error("Some information", e); throw e; }
正如上面的代碼中,記錄和拋出異常會在日誌文件中產生多條日誌消息,代碼中存在單個問題,而且對嘗試分析日誌的同事很不友好。
try { someMethod(); //Throws exceptionOne } finally { cleanUp(); //若是finally還拋出異常,那麼exceptionOne將永遠丟失 }
只要 cleanUp() 永遠不會拋出任何異常,上面的代碼沒有問題,可是若是 someMethod() 拋出一個異常,而且在 finally 塊中,cleanUp() 也拋出另外一個異常,那麼程序只會把第二個異常拋出來,原來的第一個異常(正確的緣由)將永遠丟失。若是在 finally 塊中調用的代碼可能會引起異常,請確保要麼處理它,要麼將其記錄下來。永遠不要讓它從 finally 塊中拋出來。
catch (NoSuchMethodException e) { throw e; //避免這種狀況,由於它沒有任何幫助 }
這是最重要的概念,不要爲了捕捉異常而捕捉,只有在想要處理異常時才捕捉異常,或者但願在該異常中提供其餘上下文信息。若是你不能在 catch 塊中處理它,那麼最好的建議就是不要只爲了從新拋出它而捕獲它。
完成代碼後,切勿忽略 printStackTrace(),最終別人可能會獲得這些堆棧,而且對於如何處理它徹底沒有任何方法,由於它不會附加任何上下文信息。
try { someMethod(); //Method 2 } finally { cleanUp(); //do cleanup here }
這是一個很好的作法,若是在你的方法中你正在訪問 Method 2,而 Method 2 拋出一些你不想在 Method 1 中處理的異常,可是仍然但願在發生異常時進行一些清理,而後在 finally 塊中進行清理,不要使用 catch 塊。
這多是關於異常處理最著名的原則,簡單說,應該儘快拋出(throw)異常,並儘量晚地捕獲(catch)它。應該等到有足夠的信息來妥善處理它。
這個原則隱含地說,你將更有可能把它放在低級方法中,在那裏你將檢查單個值是否爲空或不適合。並且你會讓異常堆棧跟蹤上升好幾個級別,直到達到足夠的抽象級別才能處理問題。
若是你正在使用數據庫鏈接或網絡鏈接等資源,請確保清除它們。若是你正在調用的 API 僅使用非檢查性異常,則仍應使用 try-finally 塊來清理資源。 在 try 模塊裏面訪問資源,在 finally 裏面最後關閉資源。即便在訪問資源時發生任何異常,資源也會優雅地關閉。
相關性對於保持應用程序清潔很是重要。一種嘗試讀取文件的方法,若是拋出 NullPointerException,那麼它不會給用戶任何相關的信息。相反,若是這種異常被包裹在自定義異常中,則會更好。NoSuchFileFoundException 則對該方法的用戶更有用。
不要在項目中出現使用異常來處理應用程序邏輯。永遠不要這樣作,它會使代碼很難閱讀和理解。
始終要在很是早的階段驗證用戶輸入,甚至在達到 controller 以前,它將幫助你把核心應用程序邏輯中的異常處理代碼量降到最低。若是用戶輸入出現錯誤,還能夠保證與應用程序一致。
例如:若是在用戶註冊應用程序中,遵循如下邏輯:
這是不正確的作法,它會使數據庫在各類狀況下處於不一致的狀態,應該首先驗證全部內容,而後將用戶數據置於 dao 層並進行數據庫更新。正確的作法是:
LOGGER.debug("Using cache sector A"); LOGGER.debug("Using retry sector B");
不要像上面這樣作,對多個 LOGGER.debug() 調用使用多行日誌消息可能在你的測試用例中看起來不錯,可是當它在具備 100 個並行運行的線程的應用程序服務器的日誌文件中顯示時,全部信息都輸出到相同的日誌文件,即便它們在實際代碼中爲先後行,可是在日誌文件中這兩個日誌消息可能會間隔 100 多行。應該這樣作:
LOGGER.debug("Using cache sector A, using retry sector B");
有用的異常消息和堆棧跟蹤很是重要,若是你的日誌不能定位異常位置,那要日誌有什麼用呢?
while (true) { try { Thread.sleep(100000); } catch (InterruptedException e) {} //別這樣作 doSomethingCool(); }
InterruptedException 異常提示應該中止程序正在作的事情,好比事務超時或線程池被關閉等。
應該盡最大努力完成正在作的事情,並完成當前執行的線程,而不是忽略 InterruptedException。修改後的程序以下:
while (true) { try { Thread.sleep(100000); } catch (InterruptedException e) { break; } } doSomethingCool();
在代碼中有許多相似的 catch 塊是無用的,只會增長代碼的重複性,針對這樣的問題可使用模板方法。
例如,在嘗試關閉數據庫鏈接時的異常處理。
class DBUtil{ public static void closeConnection(Connection conn){ try{ conn.close(); } catch(Exception ex){ //Log Exception - Cannot close connection } } }
這類的方法將在應用程序不少地方使用。不要把這塊代碼放的處處都是,而是定義上面的方法,而後像下面這樣使用它:
public void dataAccessCode() { Connection conn = null; try{ conn = getConnection(); .... } finally{ DBUtil.closeConnection(conn); } }
把用 JavaDoc 記錄運行時可能拋出的全部異常做爲一種習慣,其中也儘可能包括用戶應該遵循的操做,以防這些異常發生。
這篇文章首先介紹了什麼是異常,以及異常的三種分類,而後經過 20 個最佳實踐來討論如何處理異常,但願能在之後異常處理的時候有所改進及感悟。