1. 程式人生 > >Java 異常處理最佳實踐

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);
    }
}

原文連結