在 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ā)手冊
【強(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) {…}
【強(qiáng)制】異常不要用來做流程控制,條件控制。 說明:異常設(shè)計的初衷是解決程序運(yùn)行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多。
【強(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)。 正例:用戶注冊的場景中,如果用戶輸入非法字符,或用戶名稱已存在,或用戶輸入密碼過于簡單,在程序上作出分門別類的判斷,并提示給用戶。
【強(qiáng)制】捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調(diào)用者。最外層的業(yè)務(wù)使用者,必須處理異常,將其轉(zhuǎn)化為用戶可以理解的內(nèi)容。
【強(qiáng)制】有try塊放到了事務(wù)代碼中,catch異常后,如果需要回滾事務(wù),一定要注意手動回滾事務(wù)。
【強(qiáng)制】finally塊必須對資源對象、流對象進(jìn)行關(guān)閉,有異常也要做try-catch。 說明:如果JDK7及以上,可以使用try-with-resources方式。
【強(qiáng)制】不要在 finally塊中使用 return。
說明: finally塊中的 return返回后方法結(jié)束執(zhí)行,不會再執(zhí)行 try塊中的 return語句。【強(qiáng)制】捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。 說明:如果預(yù)期對方拋的是繡球,實(shí)際接到的是鉛球,就會產(chǎn)生意外情況。
【推薦】方法的返回值可以為null,不強(qiáng)制返回空集合,或者空對象等,必須添加注釋充分說明什么情況下會返回null值。 說明:本手冊明確防止NPE是調(diào)用者的責(zé)任。即使被調(diào)用方法返回空集合或者空對象,對調(diào)用者來說,也并非高枕無憂,必須考慮到遠(yuǎn)程調(diào)用失敗、序列化失敗、運(yùn)行時異常等場景返回null的情況。
-
【推薦】防止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問題。 【推薦】定義時區(qū)分unchecked / checked 異常,避免直接拋出new RuntimeException(),更不允許拋出Exception或者Throwable,應(yīng)使用有業(yè)務(wù)含義的自定義異常。推薦業(yè)界已定義過的自定義異常,如:DAOException / ServiceException等。
-
【參考】對于公司外的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耗也是問題。 【參考】避免出現(xiàn)重復(fù)的代碼(Don’t Repeat Yourself),即DRY原則。 說明:隨意復(fù)制和粘貼代碼,必然會導(dǎo)致代碼的重復(fù),在以后需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。 正例:一個類中有多個public方法,都需要進(jìn)行數(shù)行相同的參數(shù)校驗操作,這個時候請抽取:
private boolean checkParam(DTO dto) {...}