Java 異常處理最佳實踐

1. 在Finally語句塊中釋放資源或者使用Try-With-Resource語句

比如,在Try語句中使用InputStream輸入流,並且試圖在Try語句塊中關閉資源,比如下面的代碼就不是推薦做法。

public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        // use the inputStream to read a file
        // do NOT do this
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

正確做法是在Finally語句塊中執行資源釋放操作,比如,下面的代碼就是推薦做法:

public void closeResourceInFinally() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
}

或者使用Java 7引入的try-with-resource語句,如果資源實現了AutoCloseable,資源將自動釋放。

public void automaticallyCloseResource() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
        // use the inputStream to read a file
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

2. 指定異常而非通用異常

最佳實踐:在拋出異常時,最好異常應儘可能的準確,現實中你不可能一個人單打獨鬥,你需要與他人進行合作、交互、甚至聯調,對於一個陌生人抑或甚至你團隊中的其他人,他們也不大可能熟悉你內部的代碼邏輯或者你本人,那麼你的代碼邏輯所拋出的異常應儘可能的準確以便他們可以正確處理。所以,推薦做法是儘可能的告知對方可能需要知道的準確信息,接口應能準確的表達實際含義,這樣的話,方法調用方纔可以很好的處理異常或者通過額外檢查來避免這種異常。所以,應儘可能的拋出符合實際的異常信息,比如:拋出NumberFormatException異常而非 IllegalArgumentException,同時應避免代碼中直接拋出Exception異常。

public void doNotDoThis() throws Exception {
    ...
}
public void doThis() throws NumberFormatException {
    ...
}

3. 應對拋出的異常作必要的說明

如果方法拋出異常,那麼Javadoc中最好給出有關這個異常的說明,理由與最佳實踐2一樣,就是給方法調用者儘可能多的信息,以便調用方可以處理或者避免該異常。


/**
 * This method does something extremely useful ...
 *
 * @param input
 * @throws MyBusinessException if ... happens
 */
public void doSomething(String input) throws MyBusinessException {
    ...
}

4. 拋出的異常應包含足夠的描述性的信息

該最佳實踐的理念與前兩者類似,但與前兩者不同的是,該最佳實踐並不調用方方法信息,此最佳實踐的目標在於當異常發生時,異常關聯方可以通過異常信息來獲知問題所在,因此,異常中所包含的描述信息應儘可能準確,但,千萬不能誤解此實踐含義,儘可能準確的意思並不是說你需要一大段的描述來說明異常,只需要簡單的一兩句話說明異常發生的原因,以便運維同事可以快速定位問題原因,同時也能幫助你來確認服務事故。

如果拋出特定類型異常,異常的名稱最好是能描述異常的種類或者類型,這樣做的目的是避免其他不必要的額外信息,比如我們常見的NumberFormatException就是一個很好的例子。


try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
}

拋出的異常完全可以說明問題原因,因而,你根本無需其他額外補充信息。

17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"

5. 首先捕獲最準確異常

如今,絕大部分IDEs都可以做到這一點,也就是說,如果由多個異常捕獲語句,那麼最裏面的異常應是最“精確”的異常,異常捕獲應從“小”到“大”,從“精確”到“模糊”。如果,你首先捕獲範圍最大的那個異常,那麼IDE會告訴你,其餘異常將不可達unreachable,比如,下面的代碼段中,首先處理的是NumberFormatException

public void catchMostSpecificExceptionFirst() {
    try {
        doSomething("A message");
    } catch (NumberFormatException e) {
        log.error(e);
    } catch (IllegalArgumentException e) {
        log.error(e)
    }
}

6. 不要使用Throwable

Throwable是所有異常和錯誤的父類,你可以在catch語句中定義使用,但建議不要這麼做。一旦你這麼做,不僅代碼不僅會捕獲所有異常,而且同樣也會捕獲所有的錯誤,比如由JVM拋出的,不應由程序本身來處理的錯誤都會被程序“吃”掉,例如,我們常見的OutOfMemoryError StackOverflowError,當發生這種類型的異常時,錯誤已經超過應用本身可控範圍。


public void doNotCatchThrowable() {
    try {
        // do something
    } catch (Throwable t) {
        // don't do this!
    }
}

7. 不要忽略任何異常

你是否遇見過這種情況,用例剛開始執行就出現BUG?這種錯誤通常是由“被忽略的”異常所導致的,可能在代碼最開始的時候,開發人員篤信某段代碼不會發生異常,所以使用了一個catch語句,但語句塊中沒有任何異常處理邏輯或者日誌記錄,即便當你去REVIEW代碼,你也會發現類似這種的註釋:

This will never happen

public void doNotIgnoreExceptions() {
    try {
        // do something
    } catch (NumberFormatException e) {
        // this will never happen
    }
}

但,代碼可能在不斷的演變,你可能無法知道未來代碼所處的上下文會變成什麼樣子,比如某天項目組的某個人刪除了某些校驗條件,原本不會拋出任何異常的代碼可能會拋出多種異常,所以正確的做法,至少你應該以日誌記錄的方式告訴大家,未預期的異常發生了,這裏需要有人去校驗。

public void logAnException() {
    try {
        // do something
    } catch (NumberFormatException e) {
        log.error("This should never happen: " + e);
    }
}

8. 不要同時記錄然後再拋出異常

這可能是我們最容易忽略的編碼實踐,可能你會經常看到過類似的代碼段,甚至在某些Java庫中也有類似的編碼習慣,異常被捕獲,然後記錄,然後在重新拋出。

try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}

對於方法調用方來說,當異常發生時,可以很方便的通過日誌記錄來分析異常,但這種做法帶來的問題是針對同一個錯誤重複記錄多條錯誤信息,而且多餘的信息並沒有提供其他額外有用的信息。

17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)

參照最佳實踐4,異常信息應儘可能準確的描述異常發生的地點、方法以及代碼行,如果你需要添加額外的信息,推薦的做法是通過自定義異常的方式來封裝實現,封裝異常的前提是遵循最佳實踐9.

public void wrapException(String input) throws MyBusinessException {
    try {
        // do something
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e);
    }
}

9. 自定義封裝異常但不要隱藏處理最初異常

有時候,我們需要捕獲標準異常,並且將其封裝爲一個自定義異常,常見的場景是應用或者框架將標準異常封裝爲特定業務異常,通過自定義封裝異常,我們可以增加一些其他附加信息,並實現特定的異常處理類。

但記住一點,確認最初原始異常爲錯誤原因,Exception類包含有一個特定的構造函數,函數可以接受一個Throwable類作爲參數,否則的話,自定義封裝將隱藏原始錯誤信息,最初也會導致異常無法正確分析。

public void wrapException(String input) throws MyBusinessException {
    try {
        // do something
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e);
    }
}

原文鏈接

https://dzone.com/articles/9-best-practices-to-handle-exceptions-in-java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章