Java 異常處理的 20 個最佳實踐,你知道幾個?

来源:https://www.cnblogs.com/wupeixuan/archive/2019/10/26/11746117.html
-Advertisement-
Play Games

異常處理是 Java 開發中的一個重要部分,是為了處理任何錯誤狀況,比如資源不可訪問,非法輸入,空輸入等等。Java 提供了幾個異常處理特性,以try,catch 和 finally 關鍵字的形式內建於語言自身之中。Java 編程語言也允許創建新的自定義異常,並通過使用 throw 和 throws ...


異常處理是 Java 開發中的一個重要部分,是為了處理任何錯誤狀況,比如資源不可訪問,非法輸入,空輸入等等。Java 提供了幾個異常處理特性,以try,catch 和 finally 關鍵字的形式內建於語言自身之中。Java 編程語言也允許創建新的自定義異常,並通過使用 throw 和 throws關鍵字拋出它們。在Java編程中,Java 的異常處理不單單是知道語法這麼簡單,它必須遵循標準的 JDK 庫,和處理錯誤和異常的開源代碼。

這裡我們將討論一些關於異常處理的 Java 最佳實踐。在我們討論異常處理的最佳實踐之前,先讓我們瞭解下幾個重要的概念,那就是什麼是異常以及異常的分類。

什麼是異常?

異常的英文單詞是 exception,異常本質上是程式上的錯誤,包括程式邏輯錯誤和系統錯誤。比如使用空的引用、數組下標越界、記憶體溢出錯誤等,這些都是意外的情況,背離我們程式本身的意圖。錯誤在我們編寫程式的過程中會經常發生,包括編譯期間和運行期間的錯誤,在編譯期間出現的錯誤有編譯器幫助我們一起修正,然而運行期間的錯誤便不是編譯器力所能及了,並且運行期間的錯誤往往是難以預料的。假若程式在運行期間出現了錯誤,如果置之不理,程式便會終止或直接導致系統崩潰,顯然這不是我們希望看到的結果。

如何對運行期間出現的錯誤進行處理和補救呢?Java 提供了異常機制來進行處理,通過異常機制來處理程式運行期間出現的錯誤。通過異常機制,我們可以更好地提升程式的健壯性。

異常分類

Java 把異常當作對象來處理,並定義一個基類 java.lang.Throwable 作為所有異常的超類。

Java 包括三種類型的異常: 檢查性異常(checked exceptions)、非檢查性異常(unchecked Exceptions) 和錯誤(errors)。

  • 檢查性異常(checked exceptions) 是必須在在方法的 throws 子句中聲明的異常。它們擴展了異常,旨在成為一種“在你面前”的異常類型。JAVA希望你能夠處理它們,因為它們以某種方式依賴於程式之外的外部因素。檢查的異常表示在正常系統操作期間可能發生的預期問題。 當你嘗試通過網路或文件系統使用外部系統時,通常會發生這些異常。 大多數情況下,對檢查性異常的正確響應應該是稍後重試,或者提示用戶修改其輸入。
  • 非檢查性異常(unchecked Exceptions) 是不需要在throws子句中聲明的異常。 由於程式錯誤,JVM並不會強制你處理它們,因為它們大多數是在運行時生成的。 它們擴展了 RuntimeException。 最常見的例子是 NullPointerException, 未經檢查的異常可能不應該重試,正確的操作通常應該是什麼都不做,並讓它從你的方法和執行堆棧中出來。
  • 錯誤(errors) 是嚴重的運行時環境問題,肯定無法恢復。 例如 OutOfMemoryError,LinkageError 和 StackOverflowError,通常會讓程式崩潰。

所有不是 Runtime Exception 的異常,統稱為 Checked Exception,又被稱為檢查性異常。這類異常的產生不是程式本身的問題,通常由外界因素造成的。為了預防這些異常產生時,造成程式的中斷或得到不正確的結果,Java 要求編寫可能產生這類異常的程式代碼時,一定要去做異常的處理。

Java 語言將派生於 RuntimeException 類或 Error 類的所有異常稱為非檢查性異常。

Java 異常層次結構圖如下圖所示:

Java中的異常層次結構

在瞭解了異常的基本概念以及分類後,現在讓我們開始探索異常處理的最佳實踐吧。

異常處理最佳實踐

不要忽略捕捉的異常

catch (NoSuchMethodException e) {
   return null;
}

雖然捕捉了異常但是卻沒有做任何處理,除非你確信這個異常可以忽略,不然不應該這樣做。這樣會導致外面無法知曉該方法發生了錯誤,無法確定定位錯誤原因。

在你的方法里拋出定義具體的檢查性異常

public void foo() throws Exception { //錯誤方式
}

一定要避免出現上面的代碼示例,它破壞了檢查性異常的目的。 聲明你的方法可能拋出的具體檢查性異常,如果只有太多這樣的檢查性異常,你應該把它們包裝在你自己的異常中,併在異常消息中添加信息。 如果可能的話,你也可以考慮代碼重構。

public void foo() throws SpecificException1, SpecificException2 { //正確方式
}

捕獲具體的子類而不是捕獲 Exception 類

try {
   someMethod();
} catch (Exception e) { //錯誤方式
   LOGGER.error("method has failed", e);
}

捕獲異常的問題是,如果稍後調用的方法為其方法聲明添加了新的檢查性異常,則開發人員的意圖是應該處理具體的新異常。如果你的代碼只是捕獲異常(或 Throwable),永遠不會知道這個變化,以及你的代碼現在是錯誤的,並且可能會在運行時的任何時候中斷。

永遠不要捕獲 Throwable 類

這是一個更嚴重的麻煩,因為 Java Error 也是 Throwable 的子類,Error 是 JVM 本身無法處理的不可逆轉的條件,對於某些 JVM 的實現,JVM 可能實際上甚至不會在 Error 上調用 catch 子句。

始終正確包裝自定義異常中的異常,以便堆棧跟蹤不會丟失

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " + e.getMessage());  //錯誤方式
}

這破壞了原始異常的堆棧跟蹤,並且始終是錯誤的,正確的做法是:

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " , e);  //正確方式
}

要麼記錄異常要麼拋出異常,但不要一起執行

catch (NoSuchMethodException e) {  
//錯誤方式 
   LOGGER.error("Some information", e);
   throw e;
}

正如上面的代碼中,記錄和拋出異常會在日誌文件中產生多條日誌消息,代碼中存在單個問題,並且對嘗試分析日誌的同事很不友好。

finally 塊中永遠不要拋出任何異常

try {
  someMethod();  //Throws exceptionOne
} finally {
  cleanUp();    //如果finally還拋出異常,那麼exceptionOne將永遠丟失
}

只要 cleanUp() 永遠不會拋出任何異常,上面的代碼沒有問題,但是如果 someMethod() 拋出一個異常,並且在 finally 塊中,cleanUp() 也拋出另一個異常,那麼程式只會把第二個異常拋出來,原來的第一個異常(正確的原因)將永遠丟失。如果在 finally 塊中調用的代碼可能會引發異常,請確保要麼處理它,要麼將其記錄下來。永遠不要讓它從 finally 塊中拋出來。

始終只捕獲實際可處理的異常

catch (NoSuchMethodException e) {
   throw e; //避免這種情況,因為它沒有任何幫助
}

這是最重要的概念,不要為了捕捉異常而捕捉,只有在想要處理異常時才捕捉異常,或者希望在該異常中提供其他上下文信息。如果你不能在 catch 塊中處理它,那麼最好的建議就是不要只為了重新拋出它而捕獲它。

不要使用 printStackTrace() 語句或類似的方法

完成代碼後,切勿忽略 printStackTrace(),最終別人可能會得到這些堆棧,並且對於如何處理它完全沒有任何方法,因為它不會附加任何上下文信息。

對於不打算處理的異常,直接使用 finally

try {
  someMethod();  //Method 2
} finally {
  cleanUp();    //do cleanup here
}

這是一個很好的做法,如果在你的方法中你正在訪問 Method 2,而 Method 2 拋出一些你不想在 Method 1 中處理的異常,但是仍然希望在發生異常時進行一些清理,然後在 finally 塊中進行清理,不要使用 catch 塊。

記住早 throw 晚 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() 調用使用多行日誌消息可能在你的測試用例中看起來不錯,但是當它在具有 100 個並行運行的線程的應用程式伺服器的日誌文件中顯示時,所有信息都輸出到相同的日誌文件,即使它們在實際代碼中為前後行,但是在日誌文件中這兩個日誌消息可能會間隔 100 多行。應該這樣做:

LOGGER.debug("Using cache sector A, using retry sector B");

將所有相關信息儘可能地傳遞給異常

有用的異常消息和堆棧跟蹤非常重要,如果你的日誌不能定位異常位置,那要日誌有什麼用呢?

終止掉被中斷線程

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {} //別這樣做
  doSomethingCool();
}

InterruptedException 異常提示應該停止程式正在做的事情,比如事務超時或線程池被關閉等。

應該盡最大努力完成正在做的事情,並完成當前執行的線程,而不是忽略 InterruptedException。修改後的程式如下:

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {
    break;
  }
}
doSomethingCool();

對於重覆的 try-catch,使用模板方法

在代碼中有許多類似的 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 中記錄應用程式中的所有異常

把用 JavaDoc 記錄運行時可能拋出的所有異常作為一種習慣,其中也儘量包括用戶應該遵循的操作,以防這些異常發生。

總結

這篇文章首先介紹了什麼是異常,以及異常的三種分類,然後通過 20 個最佳實踐來討論如何處理異常,希望能在以後異常處理的時候有所改進及感悟。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • html文本格式化標簽 1. 在網頁中,有時需要為文字設置 粗體 、 斜體 或下劃線 效果,這是就需要用到HTML中的文本格式標簽,是文字以特殊的方式顯示 2. 標簽語義:突出重要性,比普通文字更重要 | 語義 | 標簽 | 說明 | | | | | | 加粗 | 或者 | 推薦使用標簽,語義更加強 ...
  • JavaScript Math 對象允許您對數字執行數學任務。 實例 Math.PI; // 返回 3.141592653589793 實例 Math.round() Math.round(x) 的返回值是 x 四捨五入為最接近的整數: 實例 Math.round(6.8); // 返回 7 Mat ...
  • 原文地址:https://www.cnblogs.com/SamWeb/p/8417940.html 昨天看了一篇vue的教程,作者用async/ await來發送非同步請求,從服務端獲取數據,代碼很簡潔,同時async/await 已經被標準化,也是需要學習一下了。 先說一下async的用法,它作為 ...
  • 問題描述: 在IE8及以下版本時,點擊label標簽無法自動觸發checkbox的click事件,導致無法產生希望的效果。 原HTML代碼: 1 $("input:checkbox[name='menu']").each(function (index, element) { 2 $(this).c ...
  • *** 【Layer】 ****************************************************************************************************************************Layer彈窗:layer. ...
  • 1、定義: 跨域,指的是瀏覽器不能執行其他網站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器施加的安全限制。 那麼問題來了,什麼是同源策略呢? 同源策略:功能變數名稱,協議,埠相同。 例:同一瀏覽器的兩個tab頁中分別打開來百度和谷歌的頁面 當瀏覽器的百度tab頁執行一個腳本的時候會檢查這個腳本是屬於哪個 ...
  • HTML只是簡寫全寫是(Hyper Text Markup Language)表示的是:超文本標記語言; HTML5表示的是html的第5次重大修改的第5個版本,(而html5是W3C和WHATWG合作的結果) WHATWG指的是網頁超文本應用技術小組是一個以推動網路HTML5為標準而成立的組織。在 ...
  • 示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 關於 和`transform`的動畫性能的話題,機會總是會涉及到 “合成層” 或者 “ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...