天天看點

關于JAVA異常處理的20個最佳實踐

關于JAVA異常處理的20個最佳實踐

原文位址

在我們深入了解異常處理最佳實踐的深層概念之前,讓我們從一個最重要的概念開始,那就是了解在JAVA中有三種一般類型的可抛類: 檢查性異常(checked exceptions)、非檢查性異常(unchecked Exceptions) 和 錯誤(errors)。

異常類型

關于JAVA異常處理的20個最佳實踐

檢查性異常(checked exceptions) 是必須在在方法的

throws

子句中聲明的異常。它們擴充了異常,旨在成為一種“在你面前”的異常類型。JAVA希望你能夠處理它們,因為它們以某種方式依賴于程式之外的外部因素。檢查的異常表示在正常系統操作期間可能發生的預期問題。## 标題 ## 當你嘗試通過網絡或檔案系統使用外部系統時,通常會發生這些異常。 大多數情況下,對檢查性異常的正确響應應該是稍後重試,或者提示使用者修改其輸入。

非檢查性異常(unchecked Exceptions) 是不需要在throws子句中聲明的異常。 由于程式錯誤,JVM并不會強制你處理它們,因為它們大多數是在運作時生成的。 它們擴充了

RuntimeException

。 最常見的例子是

NullPointerException

[相當可怕..是不是?]。 未經檢查的異常可能不應該重試,正确的操作通常應該是什麼都不做,并讓它從你的方法和執行堆棧中出來。 在高層次的執行中,應該記錄這種類型的異常。

錯誤(errors) 是嚴重的運作時環境問題,幾乎肯定無法恢複。 一些示例是

OutOfMemoryError

LinkageError

StackOverflowError

。 它們通常會讓程式崩潰或程式的一部分。 隻有良好的日志練習才能幫助你确定錯誤的确切原因。

使用者自定義異常

任何時候,當使用者覺得他出于某種原因想要使用自己的特定于應用程式的異常時,他可以建立一個新的類來适當的擴充超類(主要是它的Exception.java)并開始在适當的地方使用它。 這些使用者定義的異常可以以兩種方式使用:

1) 當應用程式出現問題時,直接抛出自定義異常

throw new DaoObjectNotFoundException("Couldn't find dao with id " + id);
           

2) 或者将自定義異常中的原始異常包裝并抛出

catch (NoSuchMethodException e) {
  throw new DaoObjectNotFoundException("Couldn't find dao with id " + id, e);
}
           

包裝異常可以通過添加自己的消息/上下文資訊來為使用者提供額外資訊,同時仍保留原始異常的堆棧跟蹤和消息。 它還允許你隐藏代碼的實作細節,這是封裝異常的最重要原因。

現在讓我們開始探索遵循行業聰明的異常處理的最佳實踐。

你必須考慮并遵循的最佳做法

1) 永遠不要吞下catch塊中的異常

catch (NoSuchMethodException e) {
   return null;
}
           

2) 在你的方法裡抛出定義具體的檢查性異常

public void foo() throws Exception { //錯誤方式
}
           

一定要避免出現上面的代碼示例。 它簡單地破壞了檢查性異常的整個目的。 聲明你的方法可能抛出的具體檢查性異常。 如果隻有太多這樣的檢查性異常,你應該把它們包裝在你自己的異常中,并在異常消息中添加資訊。 如果可能的話,你也可以考慮代碼重構。

public void foo() throws SpecificException1, SpecificException2 { //正确方式
}
           

3) 捕獲具體的子類而不是捕獲Exception類

try {
   someMethod();
} catch (Exception e) { //錯誤方式
   LOGGER.error("method has failed", e);
}
           

捕獲異常的問題是,如果稍後調用的方法為其方法聲明添加了新的檢查性異常,則開發人員的意圖是應該處理具體的新異常。 如果你的代碼隻是捕獲異常(或Throwable),你永遠不會知道這個變化,以及你的代碼現在是錯誤的,并且可能會在運作時的任何時候中斷。

4) 永遠不要捕獲Throwable類

這是一個更嚴重的麻煩。 因為java錯誤也是Throwable的子類。 錯誤是JVM本身無法處理的不可逆轉的條件。 對于某些JVM的實作,JVM可能實際上甚至不會在錯誤上調用catch子句。

5) 始終正确包裝自定義異常中的異常,以便堆棧跟蹤不會丢失

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " + e.getMessage());  //錯誤方式
}
           

這破壞了原始異常的堆棧跟蹤,并且始終是錯誤的。 正确的做法是:

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " , e);  //正确方式
}
           

6) 要麼記錄異常要麼抛出異常,但不要一起執行

catch (NoSuchMethodException e) {  
//錯誤方式 
   LOGGER.error("Some information", e);
   throw e;
}
           

正如在上面的示例代碼中,記錄和抛出異常會在日志檔案中産生多條日志消息,代碼中存在單個問題,并且讓嘗試挖掘日志的工程師生活得很糟糕。

7) finally塊中永遠不要抛出任何異常

try {
  someMethod();  //Throws exceptionOne
} finally {
  cleanUp();    //如果finally還抛出異常,那麼exceptionOne将永遠丢失
}
           

隻要cleanUp()永遠不會抛出任何異常,上面的代碼沒有問題。 但是如果someMethod()抛出一個異常,并且在finally塊中,cleanUp()也抛出另一個異常,那麼程式隻會把第二個異常抛出來,原來的第一個異常(正确的原因)将永遠丢失。 如果你在finally塊中調用的代碼可能會引發異常,請確定你要麼處理它,要麼将其記錄下來。 永遠不要讓它從finally塊中抛出來。

8) 始終隻捕獲實際可處理的異常

catch (NoSuchMethodException e) {
   throw e; //避免這種情況,因為它沒有任何幫助
}
           

這是最重要的概念。 不要為了捕捉異常而捕捉,隻有在想要處理異常時才捕捉異常,或者希望在該異常中提供其他上下文資訊。 如果你不能在catch塊中處理它,那麼最好的建議就是不要隻為了重新抛出它而捕獲它。

9) 不要使用printStackTrace()語句或類似的方法

完成代碼後,切勿忽略printStackTrace()。 你的同僚可能會最終得到這些堆棧,并且對于如何處理它完全沒有任何知識,因為它不會附加任何上下文資訊。

10) 如果你不打算處理異常,請使用finally塊而不是catch塊

try {
  someMethod();  //Method 2
} finally {
  cleanUp();    //do cleanup here
}
           

這也是一個很好的做法。 如果在你的方法中你正在通路Method 2,而Method 2抛出一些你不想在method 1中處理的異常,但是仍然希望在發生異常時進行一些清理,然後在finally塊中進行清理。 不要使用catch塊。

11) 記住“早throw晚catch”原則

這可能是關于異常處理最著名的原則。 它基本上說,你應該盡快抛出(throw)異常,并盡可能晚地捕獲(catch)它。 你應該等到你有足夠的資訊來妥善處理它。

這個原則隐含地說,你将更有可能把它放在低級方法中,在那裡你将檢查單個值是否為空或不适合。 而且你會讓異常堆棧跟蹤上升好幾個級别,直到達到足夠的抽象級别才能處理問題。

12) 清理總是在處理異常之後

如果你正在使用資料庫連接配接或網絡連接配接等資源,請確定清除它們。 如果你正在調用的API僅使用非檢查性異常,則仍應使用try-finally塊來清理資源。 在try子產品裡面通路資源,在finally裡面最後關閉資源。 即使在通路資源時發生任何異常,資源也會優雅地關閉。

13) 隻從方法中抛出相關異常

相關性對于保持應用程式清潔非常重要。 一種嘗試讀取檔案的方法; 如果抛出NullPointerException,那麼它不會給使用者任何相關的資訊。 相反,如果這種異常被包裹在自定義異常中,則會更好。 NoSuchFileFoundException則對該方法的使用者更有用。

14) 切勿在程式中使用流程控制異常

我們已經閱讀過很多次,但有時我們還是會在項目中看到開發人員嘗試為應用程式邏輯而使用異常的代碼。 永遠不要這樣做。 它使代碼很難閱讀,了解和醜陋。

15) 驗證使用者輸入以在請求處理的早期捕獲不利條件

始終要在非常早的階段驗證使用者輸入,甚至在達到實際controller之前。 它将幫助你把核心應用程式邏輯中的異常處理代碼量降到最低。 如果使用者輸入出現錯誤,它還可以幫助你使與應用程式保持一緻。

例如:如果在使用者注冊應用程式中,你遵循以下邏輯:

1)驗證使用者

2)插入使用者

3)驗證位址

4)插入位址

5)如果出問題復原一切

這是非常不正确的做法。 它會使資料庫在各種情況下處于不一緻的狀态。 首先驗證所有内容,然後将使用者資料置于dao層并進行資料庫更新。 正确的做法是:

1)驗證使用者

2)驗證位址

3)插入使用者

4)插入位址

5)如果問題復原一切

16) 一個異常隻能包含在一個日志中

LOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");
           

不要這樣做。

對多個LOGGER.debug()調用使用多行日志消息可能在你的測試用例中看起來不錯,但是當它在具有400個并行運作的線程的應用程式伺服器的日志檔案中顯示時,所有轉儲資訊都是相同的日志檔案,你的兩個日志消息最終可能會在日志檔案中間隔1000行,即使它們出現在代碼的後續行中。

像這樣做:

LOGGER.debug("Using cache sector A, using retry sector B");
           

17) 将所有相關資訊盡可能地傳遞給異常

有用且資訊豐富的異常消息和堆棧跟蹤也非常重要。 如果你的日志不能确定任何事情(有效内容不全或很難确定問題原因),

那要日志有什麼用? 這類的日志隻是你代碼中的裝飾品。

18) 終止掉被中斷線程

while (true) {
  try {
    Thread.sleep();
  } catch (InterruptedException e) {} //别這樣做
  doSomethingCool();
}
           

InterruptedException是你的代碼的一個提示,它應該停止它正在做的事情。 線程中斷的一些常見用例是active事務逾時或線程池關閉。 你的代碼應該盡最大努力完成它正在做的事情,并且完成目前的執行線程,而不是忽略InterruptedException。 是以要糾正上面的例子:

while (true) {
  try {
    Thread.sleep();
  } catch (InterruptedException e) {
    break;
  }
}
doSomethingCool();
           

19) 使用模闆方法重複try-catch

在你的代碼中有100個類似的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);
    }
}
           

20) 在JavaDoc中記錄應用程式中的所有異常

把注釋(javadoc)運作時可能抛出的所有異常作為一種習慣。

也要盡可能包括可行的方案,使用者應該關注這些異常發生的情況。

這就是我現在所想的。 如果你發現任何遺漏或你與我的觀點不一緻,請發表評論。 我會很樂意讨論。

繼續閱讀