Java異常處理最佳實(shí)踐

在 Java 中處理異常并不是一個簡單的事情。不僅僅初學(xué)者很難理解,即使一些有經(jīng)驗的開發(fā)者也需要花費(fèi)很多時間來思考如何處理異常,包括需要處理哪些異常,怎樣處理等等。這也是絕大多數(shù)開發(fā)團(tuán)隊都會制定一些規(guī)則來規(guī)范進(jìn)行異常處理的原因。而團(tuán)隊之間的這些規(guī)范往往是截然不同的。

本文給出幾個被很多團(tuán)隊使用的異常處理最佳實(shí)踐。

  • 在 finally 塊中清理資源或者使用 try-with-resource 語句
  • 優(yōu)先明確的異常
  • 對異常進(jìn)行文檔說明
  • 使用描述性消息拋出異常
  • 優(yōu)先捕獲最具體的異常
  • 不要捕獲 Throwable 類
  • 不要忽略異常
  • 不要記錄并拋出異常
  • 包裝異常時不要拋棄原始的異常
  • 不要使用異??刂瞥绦虻牧鞒?/li>
  • 使用標(biāo)準(zhǔn)異常
  • 異常會影響性能
  • 總結(jié)
  • 異常處理-阿里巴巴Java開發(fā)手冊

1. 在 finally 塊中清理資源或者使用 try-with-resource 語句

當(dāng)使用類似InputStream這種需要使用后關(guān)閉的資源時,一個常見的錯誤就是在try塊的最后關(guān)閉資源。

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 代碼塊內(nèi)代碼會正常執(zhí)行,并且資源可以正常關(guān)閉。但是,使用 try 代碼塊是有原因的,一般調(diào)用一個或多個可能拋出異常的方法,而且,你自己也可能會拋出一個異常,這意味著代碼可能不會執(zhí)行到 try 代碼塊的最后部分。結(jié)果就是,你并沒有關(guān)閉資源。

所以,你應(yīng)該把清理工作的代碼放到 finally 里去,或者使用 try-with-resource 特性。

1.1 使用 finally 代碼塊

與前面幾行 try 代碼塊不同,finally 代碼塊總是會被執(zhí)行。不管 try 代碼塊成功執(zhí)行之后還是你在 catch 代碼塊中處理完異常后都會執(zhí)行。因此,你可以確保你清理了所有打開的資源。

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

1.2 Java 7 的 try-with-resource 語法

如果你的資源實(shí)現(xiàn)了 AutoCloseable 接口,你可以使用這個語法。大多數(shù)的 Java 標(biāo)準(zhǔn)資源都繼承了這個接口。當(dāng)你在 try 子句中打開資源,資源會在 try 代碼塊執(zhí)行后或異常處理后自動關(guān)閉。

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. 優(yōu)先明確的異常

你拋出的異常越明確越好,永遠(yuǎn)記住,你的同事或者幾個月之后的你,將會調(diào)用你的方法并且處理異常。

因此需要保證提供給他們盡可能多的信息。這樣你的 API 更容易被理解。你的方法的調(diào)用者能夠更好的處理異常并且避免額外的檢查。

因此,總是嘗試尋找最適合你的異常事件的類,例如,拋出一個 NumberFormatException 來替換一個 IllegalArgumentException 。避免拋出一個不明確的異常。

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

3. 對異常進(jìn)行文檔說明

當(dāng)在方法上聲明拋出異常時,也需要進(jìn)行文檔說明。目的是為了給調(diào)用者提供盡可能多的信息,從而可以更好地避免或處理異常。
在 Javadoc 添加 @throws 聲明,并且描述拋出異常的場景。

/**

 * This method does something extremely useful ...

 *

 * @param input

 * @throws MyBusinessException if ... happens

 */
public void doSomething(String input) throws MyBusinessException {
    ...
}

4. 使用描述性消息拋出異常

在拋出異常時,需要盡可能精確地描述問題和相關(guān)信息,這樣無論是打印到日志中還是在監(jiān)控工具中,都能夠更容易被人閱讀,從而可以更好地定位具體錯誤信息、錯誤的嚴(yán)重程度等。

但這里并不是說要對錯誤信息長篇大論,因為本來 Exception 的類名就能夠反映錯誤的原因,因此只需要用一到兩句話描述即可。

如果拋出一個特定的異常,它的類名很可能已經(jīng)描述了這種錯誤。所以,你不需要提供很多額外的信息。一個很好的例子是 NumberFormatException 。當(dāng)你以錯誤的格式提供 String 時,它將被 java.lang.Long 類的構(gòu)造函數(shù)拋出。

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

5. 優(yōu)先捕獲最具體的異常

大多數(shù) IDE 都可以幫助你實(shí)現(xiàn)這個最佳實(shí)踐。當(dāng)你嘗試首先捕獲較不具體的異常時,它們會報告無法訪問的代碼塊。

但問題在于,只有匹配異常的第一個 catch 塊會被執(zhí)行。 因此,如果首先捕獲 IllegalArgumentException ,則永遠(yuǎn)不會到達(dá)應(yīng)該處理更具體的 NumberFormatException 的 catch 塊,因為它是 IllegalArgumentException 的子類。

總是優(yōu)先捕獲最具體的異常類,并將不太具體的 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. 不要捕獲 Throwable 類

Throwable 是所有異常和錯誤的超類。你可以在 catch 子句中使用它,但是你永遠(yuǎn)不應(yīng)該這樣做!

如果在 catch 子句中使用 Throwable ,它不僅會捕獲所有異常,也將捕獲所有的錯誤。JVM 拋出錯誤,指出不應(yīng)該由應(yīng)用程序處理的嚴(yán)重問題。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。兩者都是由應(yīng)用程序控制之外的情況引起的,無法處理。

所以,最好不要捕獲 Throwable ,除非你確定自己處于一種特殊的情況下能夠處理錯誤。

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

7. 不要忽略異常

很多時候,開發(fā)者很有自信不會拋出異常,因此寫了一個catch塊,但是沒有做任何處理或者記錄日志。

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

但現(xiàn)實(shí)是經(jīng)常會出現(xiàn)無法預(yù)料的異常,或者無法確定這里的代碼未來是不是會改動(刪除了阻止異常拋出的代碼),而此時由于異常被捕獲,使得無法拿到足夠的錯誤信息來定位問題。

合理的做法是至少要記錄異常的信息。

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

8. 不要記錄并拋出異常

這可能是本文中最常被忽略的最佳實(shí)踐??梢园l(fā)現(xiàn)很多代碼甚至類庫中都會有捕獲異常、記錄日志并再次拋出的邏輯。如下:

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

這個處理邏輯看著是合理的。但這經(jīng)常會給同一個異常輸出多條日志。如下:

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)

如上所示,后面的日志也沒有附加更有用的信息。如果想要提供更加有用的信息,那么可以將異常包裝為自定義異常。

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

因此,僅僅當(dāng)想要處理異常時才去捕獲,否則只需要在方法簽名中聲明讓調(diào)用者去處理。

9. 包裝異常時不要拋棄原始的異常

捕獲標(biāo)準(zhǔn)異常并包裝為自定義異常是一個很常見的做法。這樣可以添加更為具體的異常信息并能夠做針對的異常處理。
在你這樣做時,請確保將原始異常設(shè)置為原因(注:參考下方代碼 NumberFormatException e 中的原始異常 e )。Exception 類提供了特殊的構(gòu)造函數(shù)方法,它接受一個 Throwable 作為參數(shù)。否則,你將會丟失堆棧跟蹤和原始異常的消息,這將會使分析導(dǎo)致異常的異常事件變得困難。

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

10. 不要使用異常控制程序的流程

不應(yīng)該使用異??刂茟?yīng)用的執(zhí)行流程,例如,本應(yīng)該使用if語句進(jìn)行條件判斷的情況下,你卻使用異常處理,這是非常不好的習(xí)慣,會嚴(yán)重影響應(yīng)用的性能。

11. 使用標(biāo)準(zhǔn)異常

如果使用內(nèi)建的異??梢越鉀Q問題,就不要定義自己的異常。Java API 提供了上百種針對不同情況的異常類型,在開發(fā)中首先盡可能使用 Java API 提供的異常,如果標(biāo)準(zhǔn)的異常不能滿足你的要求,這時候創(chuàng)建自己的定制異常。盡可能得使用標(biāo)準(zhǔn)異常有利于新加入的開發(fā)者看懂項目代碼。

12. 異常會影響性能

異常處理的性能成本非常高,每個 Java 程序員在開發(fā)時都應(yīng)牢記這句話。創(chuàng)建一個異常非常慢,拋出一個異常又會消耗1~5ms,當(dāng)一個異常在應(yīng)用的多個層級之間傳遞時,會拖累整個應(yīng)用的性能。

  • 僅在異常情況下使用異常;
  • 在可恢復(fù)的異常情況下使用異常;

盡管使用異常有利于 Java 開發(fā),但是在應(yīng)用中最好不要捕獲太多的調(diào)用棧,因為在很多情況下都不需要打印調(diào)用棧就知道哪里出錯了。因此,異常消息應(yīng)該提供恰到好處的信息。

13. 總結(jié)

綜上所述,當(dāng)你拋出或捕獲異常的時候,有很多不同的情況需要考慮,而且大部分事情都是為了改善代碼的可讀性或者 API 的可用性。

異常不僅僅是一個錯誤控制機(jī)制,也是一個通信媒介。因此,為了和同事更好的合作,一個團(tuán)隊必須要制定出一個最佳實(shí)踐和規(guī)則,只有這樣,團(tuán)隊成員才能理解這些通用概念,同時在工作中使用它。

異常處理-阿里巴巴Java開發(fā)手冊

  1. 【強(qiáng)制】Java 類庫中定義的可以通過預(yù)檢查方式規(guī)避的RuntimeException異常不應(yīng)該通過catch 的方式來處理,比如:NullPointerException,IndexOutOfBoundsException等等。 說明:無法通過預(yù)檢查的異常除外,比如,在解析字符串形式的數(shù)字時,不得不通過catch NumberFormatException來實(shí)現(xiàn)。 正例:if (obj != null) {...} 反例:try { obj.method(); } catch (NullPointerException e) {…}

  2. 【強(qiáng)制】異常不要用來做流程控制,條件控制。 說明:異常設(shè)計的初衷是解決程序運(yùn)行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。

  3. 【強(qiáng)制】catch時請分清穩(wěn)定代碼和非穩(wěn)定代碼,穩(wěn)定代碼指的是無論如何不會出錯的代碼。對于非穩(wěn)定代碼的catch盡可能進(jìn)行區(qū)分異常類型,再做對應(yīng)的異常處理。 說明:對大段代碼進(jìn)行try-catch,使程序無法根據(jù)不同的異常做出正確的應(yīng)激反應(yīng),也不利于定位問題,這是一種不負(fù)責(zé)任的表現(xiàn)。 正例:用戶注冊的場景中,如果用戶輸入非法字符,或用戶名稱已存在,或用戶輸入密碼過于簡單,在程序上作出分門別類的判斷,并提示給用戶。

  4. 【強(qiáng)制】捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調(diào)用者。最外層的業(yè)務(wù)使用者,必須處理異常,將其轉(zhuǎn)化為用戶可以理解的內(nèi)容。

  5. 【強(qiáng)制】有try塊放到了事務(wù)代碼中,catch異常后,如果需要回滾事務(wù),一定要注意手動回滾事務(wù)。

  6. 【強(qiáng)制】finally塊必須對資源對象、流對象進(jìn)行關(guān)閉,有異常也要做try-catch。 說明:如果JDK7及以上,可以使用try-with-resources方式。

  7. 【強(qiáng)制】不要在 finally塊中使用 return。
    說明: finally塊中的 return返回后方法結(jié)束執(zhí)行,不會再執(zhí)行 try塊中的 return語句。

  8. 【強(qiáng)制】捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。 說明:如果預(yù)期對方拋的是繡球,實(shí)際接到的是鉛球,就會產(chǎn)生意外情況。

  9. 【推薦】方法的返回值可以為null,不強(qiáng)制返回空集合,或者空對象等,必須添加注釋充分說明什么情況下會返回null值。 說明:本手冊明確防止NPE是調(diào)用者的責(zé)任。即使被調(diào)用方法返回空集合或者空對象,對調(diào)用者來說,也并非高枕無憂,必須考慮到遠(yuǎn)程調(diào)用失敗、序列化失敗、運(yùn)行時異常等場景返回null的情況。

  10. 【推薦】防止NPE,是程序員的基本修養(yǎng),注意NPE產(chǎn)生的場景:

    1)返回類型為基本數(shù)據(jù)類型,return包裝數(shù)據(jù)類型的對象時,自動拆箱有可能產(chǎn)生NPE。 反例:public int f() { return Integer對象}, 如果為null,自動解箱拋NPE。

    2) 數(shù)據(jù)庫的查詢結(jié)果可能為null。

    3) 集合里的元素即使isNotEmpty,取出的數(shù)據(jù)元素也可能為null。

    4) 遠(yuǎn)程調(diào)用返回對象時,一律要求進(jìn)行空指針判斷,防止NPE。

    5) 對于Session中獲取的數(shù)據(jù),建議NPE檢查,避免空指針。

    6) 級聯(lián)調(diào)用obj.getA().getB().getC();一連串調(diào)用,易產(chǎn)生NPE。
    正例:使用JDK8的Optional類來防止NPE問題。

  11. 【推薦】定義時區(qū)分unchecked / checked 異常,避免直接拋出new RuntimeException(),更不允許拋出Exception或者Throwable,應(yīng)使用有業(yè)務(wù)含義的自定義異常。推薦業(yè)界已定義過的自定義異常,如:DAOException / ServiceException等。

  12. 【參考】對于公司外的http/api開放接口必須使用“錯誤碼”;而應(yīng)用內(nèi)部推薦異常拋出;跨應(yīng)用間RPC調(diào)用優(yōu)先考慮使用Result方式,封裝isSuccess()方法、“錯誤碼”、“錯誤簡短信息”。 說明:關(guān)于RPC方法返回方式使用Result方式的理由:

    1)使用拋異常返回方式,調(diào)用方如果沒有捕獲到就會產(chǎn)生運(yùn)行時錯誤。 
    
    2)如果不加棧信息,只是new自定義異常,加入自己的理解的error message,對于調(diào)用端解決問題的幫助不會太多。如果加了棧信息,在頻繁調(diào)用出錯的情況下,數(shù)據(jù)序列化和傳輸?shù)男阅軗p耗也是問題。
    
  13. 【參考】避免出現(xiàn)重復(fù)的代碼(Don’t Repeat Yourself),即DRY原則。 說明:隨意復(fù)制和粘貼代碼,必然會導(dǎo)致代碼的重復(fù),在以后需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。 正例:一個類中有多個public方法,都需要進(jìn)行數(shù)行相同的參數(shù)校驗操作,這個時候請抽取:
    private boolean checkParam(DTO dto) {...}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容