在咱們深刻了解異常處理最佳實踐的深層概念以前,讓咱們從一個最重要的概念開始,那就是理解在JAVA中有三種通常類型的可拋類: 檢查性異常(checked exceptions)、非檢查性異常(unchecked Exceptions) 和 錯誤(errors)。java
檢查性異常(checked exceptions) 是必須在在方法的throws
子句中聲明的異常。它們擴展了異常,旨在成爲一種「在你面前」的異常類型。JAVA但願你可以處理它們,由於它們以某種方式依賴於程序以外的外部因素。檢查的異常表示在正常系統操做期間可能發生的預期問題。 當你嘗試經過網絡或文件系統使用外部系統時,一般會發生這些異常。 大多數狀況下,對檢查性異常的正確響應應該是稍後重試,或者提示用戶修改其輸入。
非檢查性異常(unchecked Exceptions) 是不須要在throws子句中聲明的異常。 因爲程序錯誤,JVM並不會強制你處理它們,由於它們大多數是在運行時生成的。 它們擴展了RuntimeException
。 最多見的例子是NullPointerException
[至關可怕..是否是?]。 未經檢查的異常可能不該該重試,正確的操做一般應該是什麼都不作,並讓它從你的方法和執行堆棧中出來。 在高層次的執行中,應該記錄這種類型的異常。
錯誤(errors) 是嚴重的運行時環境問題,幾乎確定沒法恢復。 例如OutOfMemoryError
,LinkageError
和StackOverflowError
, 它們一般會讓程序崩潰或程序的一部分。 只有良好的日誌練習才能幫助你肯定錯誤的確切緣由。數據庫
任什麼時候候,當用戶以爲他出於某種緣由想要使用本身的特定於應用程序的異常時,他能夠建立一個新的類來適當的擴展超類(主要是它的Exception.java)並開始在適當的地方使用它。 這些用戶定義的異常能夠以兩種方式使用:
1) 當應用程序出現問題時,直接拋出自定義異常服務器
throw new DaoObjectNotFoundException("Couldn't find dao with id " + id);
2) 或者將自定義異常中的原始異常包裝並拋出網絡
catch (NoSuchMethodException e) { throw new DaoObjectNotFoundException("Couldn't find dao with id " + id, e); }
包裝異常能夠經過添加本身的消息/上下文信息來爲用戶提供額外信息,同時仍保留原始異常的堆棧跟蹤和消息。 它還容許你隱藏代碼的實現細節,這是封裝異常的最重要緣由。 測試
如今讓咱們開始探索遵循行業聰明的異常處理的最佳實踐。spa
catch (NoSuchMethodException e) { return null; }
public void foo() throws Exception { //錯誤方式 }
必定要避免出現上面的代碼示例。 它簡單地破壞了檢查性異常的整個目的。 聲明你的方法可能拋出的具體檢查性異常。 若是隻有太多這樣的檢查性異常,你應該把它們包裝在你本身的異常中,並在異常消息中添加信息。 若是可能的話,你也能夠考慮代碼重構。線程
public void foo() throws SpecificException1, SpecificException2 { //正確方式 }
try { someMethod(); } catch (Exception e) { //錯誤方式 LOGGER.error("method has failed", e); }
捕獲異常的問題是,若是稍後調用的方法爲其方法聲明添加了新的檢查性異常,則開發人員的意圖是應該處理具體的新異常。 若是你的代碼只是捕獲異常(或Throwable),你永遠不會知道這個變化,以及你的代碼如今是錯誤的,而且可能會在運行時的任什麼時候候中斷。debug
這是一個更嚴重的麻煩。 由於java錯誤也是Throwable的子類。 錯誤是JVM自己沒法處理的不可逆轉的條件。 對於某些JVM的實現,JVM可能實際上甚至不會在錯誤上調用catch子句。日誌
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " + e.getMessage()); //錯誤方式 }
這破壞了原始異常的堆棧跟蹤,而且始終是錯誤的。 正確的作法是:code
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " , e); //正確方式 }
catch (NoSuchMethodException e) { //錯誤方式 LOGGER.error("Some information", e); throw e; }
正如在上面的示例代碼中,記錄和拋出異常會在日誌文件中產生多條日誌消息,代碼中存在單個問題,而且讓嘗試挖掘日誌的工程師生活得很糟糕。
try { someMethod(); //Throws exceptionOne } finally { cleanUp(); //若是finally還拋出異常,那麼exceptionOne將永遠丟失 }
只要cleanUp()永遠不會拋出任何異常,上面的代碼沒有問題。 可是若是someMethod()拋出一個異常,而且在finally塊中,cleanUp()也拋出另外一個異常,那麼程序只會把第二個異常拋出來,原來的第一個異常(正確的緣由)將永遠丟失。 若是你在finally塊中調用的代碼可能會引起異常,請確保你要麼處理它,要麼將其記錄下來。 永遠不要讓它從finally塊中拋出來。
catch (NoSuchMethodException e) { throw e; //避免這種狀況,由於它沒有任何幫助 }
這是最重要的概念。 不要爲了捕捉異常而捕捉,只有在想要處理異常時才捕捉異常,或者但願在該異常中提供其餘上下文信息。 若是你不能在catch塊中處理它,那麼最好的建議就是不要只爲了從新拋出它而捕獲它。
完成代碼後,切勿忽略printStackTrace()。 你的同事可能會最終獲得這些堆棧,而且對於如何處理它徹底沒有任何知識,由於它不會附加任何上下文信息。
try { someMethod(); //Method 2 } finally { cleanUp(); //do cleanup here }
這也是一個很好的作法。 若是在你的方法中你正在訪問Method 2,而Method 2拋出一些你不想在method 1中處理的異常,可是仍然但願在發生異常時進行一些清理,而後在finally塊中進行清理。 不要使用catch塊。
這多是關於異常處理最著名的原則。 它基本上說,你應該儘快拋出(throw)異常,並儘量晚地捕獲(catch)它。 你應該等到你有足夠的信息來妥善處理它。
這個原則隱含地說,你將更有可能把它放在低級方法中,在那裏你將檢查單個值是否爲空或不適合。 並且你會讓異常堆棧跟蹤上升好幾個級別,直到達到足夠的抽象級別才能處理問題。
若是你正在使用數據庫鏈接或網絡鏈接等資源,請確保清除它們。 若是你正在調用的API僅使用非檢查性異常,則仍應使用try-finally塊來清理資源。 在try模塊裏面訪問資源,在finally裏面最後關閉資源。 即便在訪問資源時發生任何異常,資源也會優雅地關閉。
相關性對於保持應用程序清潔很是重要。 一種嘗試讀取文件的方法; 若是拋出NullPointerException,那麼它不會給用戶任何相關的信息。 相反,若是這種異常被包裹在自定義異常中,則會更好。 NoSuchFileFoundException則對該方法的用戶更有用。
咱們已經閱讀過不少次,但有時咱們仍是會在項目中看到開發人員嘗試爲應用程序邏輯而使用異常的代碼。 永遠不要這樣作。 它使代碼很難閱讀,理解和醜陋。
始終要在很是早的階段驗證用戶輸入,甚至在達到實際controller以前。 它將幫助你把核心應用程序邏輯中的異常處理代碼量降到最低。 若是用戶輸入出現錯誤,它還能夠幫助你使與應用程序保持一致。
例如:若是在用戶註冊應用程序中,你遵循如下邏輯:
1)驗證用戶
2)插入用戶
3)驗證地址
4)插入地址
5)若是出問題回滾一切
這是很是不正確的作法。 它會使數據庫在各類狀況下處於不一致的狀態。 首先驗證全部內容,而後將用戶數據置於dao層並進行數據庫更新。 正確的作法是:
1)驗證用戶
2)驗證地址
3)插入用戶
4)插入地址
5)若是問題回滾一切
LOGGER.debug("Using cache sector A"); LOGGER.debug("Using retry sector B");
不要這樣作。
對多個LOGGER.debug()調用使用多行日誌消息可能在你的測試用例中看起來不錯,可是當它在具備400個並行運行的線程的應用程序服務器的日誌文件中顯示時,全部轉儲信息都是相同的日誌文件,你的兩個日誌消息最終可能會在日誌文件中間隔1000行,即便它們出如今代碼的後續行中。
像這樣作:
LOGGER.debug("Using cache sector A, using retry sector B");
有用且信息豐富的異常消息和堆棧跟蹤也很是重要。 若是你的日誌不能肯定任何事情(有效內容不全或很難肯定問題緣由),
那要日誌有什麼用? 這類的日誌只是你代碼中的裝飾品。
while (true) { try { Thread.sleep(100000); } catch (InterruptedException e) {} //別這樣作 doSomethingCool(); }
InterruptedException是你的代碼的一個提示,它應該中止它正在作的事情。 線程中斷的一些常見用例是active事務超時或線程池關閉。 你的代碼應該盡最大努力完成它正在作的事情,而且完成當前的執行線程,而不是忽略InterruptedException。 因此要糾正上面的例子:
while (true) { try { Thread.sleep(100000); } catch (InterruptedException e) { break; } } doSomethingCool();
在你的代碼中有100個相似的catch塊是沒有用的。 它增長代碼的重複性並且沒有任何的幫助。 對這種狀況要使用模板方法。
例如,下面的代碼嘗試關閉數據庫鏈接。
class DBUtil{ public static void closeConnection(Connection conn){ try{ conn.close(); } catch(Exception ex){ //Log Exception - Cannot close connection } } }
這種類型的方法將在你的應用程序的成千上萬個地方使用。 不要把這塊代碼放的處處都是,而是定義頂層的方法,並在下層的任何地方使用它:
public void dataAccessCode() { Connection conn = null; try{ conn = getConnection(); .... } finally{ DBUtil.closeConnection(conn); } }
把註釋(javadoc)運行時可能拋出的全部異常做爲一種習慣。
也要儘量包括可行的方案,用戶應該關注這些異常發生的狀況。
這就是我如今所想的。 若是你發現任何遺漏或你與個人觀點不一致,請發表評論。 我會很樂意討論。