處理Java異常的9個最佳實踐

銀河1號發表於2019-03-02

Java中的異常處理不是一個簡單的主題。初學者發現它很難理解,甚至有經驗的開發者也可以花幾個小時討論如何以及應該丟擲或處理哪些異常。

這就是為什麼大多數開發團隊都有自己的一套如何使用它們的規則。如果你是一個團隊的新手,你可能會驚訝這些規則與你之前使用的規則有多麼不同。
儘管如此,大多數團隊都使用了幾種最佳實踐。以下是幫助你入門或改進異常處理的9個最重要的內容。

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);
    }
}複製程式碼
問題是隻要沒有丟擲異常,這種方法似乎完全正常。try塊中的所有語句都將被執行,資源將被關閉。
但是你新增了try塊是有原因的。你呼叫一個或多個可能丟擲異常的方法,或者你自己丟擲異常。這意味著你可能無法到達try塊的末尾。因此,你將不會關閉資源。
因此,你應該將所有清理程式碼放入finally塊或使用try-with-resource語句。

使用Finally塊

與try塊的最後幾行相比,finally塊始終執行。這可以在成功執行try塊之後或在catch塊中處理異常之後發生。因此,你可以確保清理所有已開啟的資源。
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

另一種選擇是try-with-resource語句,我在Java異常處理的介紹中對此進行了更詳細的解釋。
如果資源實現AutoCloseable介面,則可以使用它。這就是大多數Java標準資源所做的事情。當你在try子句中開啟資源時,它將在try塊執行後自動關閉,或者處理異常。
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.特定異常

丟擲的異常越具體越好。請記住,不明白你程式碼的同事,或者你可能在幾個月後需要呼叫你的方法並處理異常。
因此,請務必提供儘可能多的資訊。這使你的API更易於理解。因此,你的方法的呼叫者將能夠更好地處理異常或通過額外的檢查來避免它
因此,總是嘗試找到最適合你的異常事件的類,例如丟擲NumberFormatException而不是IllegalArgumentException。並避免丟擲非特定的異常。
public void doNotDoThis() throws Exception {
    ...
}
public void doThis() throws NumberFormatException {
    ...
}複製程式碼

3.記錄你宣告的異常

無論何時在方法簽名中指定異常,都應該在Javadoc中記錄它。這與以前的最佳實踐具有相同的目標:為呼叫者提供儘可能多的資訊,以便他可以避免或處理異常。
因此,請確保向Javadoc 新增@throws宣告並描述可能導致異常的情況。
/**
 * This method does something extremely useful ...
 *
 * @param input
 * @throws MyBusinessException if ... happens
 */
public void doSomething(String input) throws MyBusinessException {
    ...
}複製程式碼

4.使用描述資訊丟擲異常

這種最佳實踐背後的想法類似於前兩種實踐。但是這次,你不向呼叫方提供有關方法的資訊。每個必須瞭解在日誌檔案或監視工具中丟擲異常時發生了什麼的人都會讀取異常的訊息。
因此,它應該儘可能準確地描述問題,並提供最相關的資訊來理解異常事件。
別誤會我的意思; 你不應該寫一段文字。但是你應該用1-2個簡短的句子來解釋這個例外的原因。這有助於你的運營團隊瞭解問題的嚴重性,還可以讓你更輕鬆地分析任何服務事件。
如果丟擲一個特定的異常,它的類名很可能已經描述了那種錯誤。因此,你無需提供大量其他資訊。一個很好的例子是NumberFormatException。它會被類java.lang.Long的建構函式丟擲,當你以錯誤的格式提供String引數。
try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
}複製程式碼
NumberFormatException類的名稱已經告訴你問題的型別。它的訊息只需要提供導致問題的輸入字串。如果異常類的名稱不具有表現力,則需要在訊息中提供所需的資訊。
17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException:
 For input string: "xyz"複製程式碼

5.優先捕獲最具體的異常

大多數IDE都可以幫助你實現這一最佳實踐。當你嘗試首先捕獲不太具體的異常時,它們提示無法訪問的程式碼塊。
問題是隻有匹配異常的第一個catch塊才會被執行。因此,如果首先捕獲IllegalArgumentException,則永遠不會到達應該處理更具體的NumberFormatException的catch塊,因為它是IllegalArgumentException的子類。
始終優先捕獲最具體的異常類,並將不太具體的catch塊新增到列表的末尾。
你可以在以下程式碼段中看到此類try-catch語句的示例。第一個catch塊處理所有的NumberFormatException,第二個處理所有不是NumberFormatException的IllegalArgumentException 異常。
public void catchMostSpecificExceptionFirst() {
 try {
        doSomething("A message");
    } catch (NumberFormatException e) {
        log.error(e);
    } catch (IllegalArgumentException e) {
        log.error(e)
    }
}複製程式碼

6.Don’t Catch Throwable

Throwable是所有異常和錯誤的超類。你可以在catch子句中使用它,但你永遠不應該這樣做!
如果在catch子句中使用Throwable,它不僅會捕獲所有異常; 它還會捕獲所有錯誤。JVM丟擲錯誤以指示應用程式無法處理的嚴重問題。典型的例子是OutOfMemoryErrorStackOverflowError。兩者都是由應用程式無法控制的情況引起的,無法處理。
所以,最好不要抓住Throwable,除非你完全確定你處於一個特殊情況,你可以或者需要處理錯誤。
public void doNotCatchThrowable() {
 try {
     // do something
    } catch (Throwable t) {
     // don't do this!
    }
}複製程式碼

7.Don’t Ignore Exceptions

你是否曾經分析過只有用例第一部分被執行的錯誤報告?
這通常是由忽略的異常引起的。開發人員可能非常確定它永遠不會被丟擲並新增了一個不處理或記錄它的catch塊。當你找到這個程式碼塊時,你很可能甚至會發現一個著名的“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.Don’t Log and Throw

這可能是此列表中最常被忽略的最佳做法。你可以找到許多程式碼片段,甚至是catch,log和重新throw異常的庫。
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.在沒有消費的情況下包裝異常

有時候捕獲標準異常並將其包裝成自定義異常會更好。此類異常的典型示例是應用程式或框架特定的業務異常。這允許你新增其他資訊,還可以為異常類實現特殊處理。
執行此操作時,請確保將原始異常設定為cause。該異常類提供了接受一個特定的構造方法的Throwable作為引數。否則,你將丟失原始異常的堆疊跟蹤和訊息,這將導致難以分析導致異常的異常事件。
public void wrapException(String input) throws MyBusinessException {
 try {
      // do something
    } catch (NumberFormatException e) {
      throw new MyBusinessException("A message that describes the error.", e);
    }
}複製程式碼

總結

正如所看到的,當你丟擲或捕獲異常時,你應該考慮許多不同的事情。其中大多數都旨在提高程式碼的可讀性或API的可用性。
異常通常同時是錯誤處理機制和通訊媒介。因此,您應該確保與同事討論要應用的最佳實踐和規則,以便每個人都能理解通用概念並以相同的方式使用它們。
更多文章歡迎訪問: http://www.apexyun.com 

公眾號:銀河系1號 

聯絡郵箱:public@space-explore.com 

(未經同意,請勿轉載) 


相關文章