首頁 Java java教程 Java 異常處理的誤解與經驗總結

Java 異常處理的誤解與經驗總結

Feb 06, 2017 am 11:28 AM
java 例外處理 經驗總結

本文著重介紹了 Java 異常選擇和使用中的一些誤解,希望各位讀者能夠熟練掌握異常處理的一些注意點和原則,注意總結和歸納。處理好了異常,才能提升開發人員的基本素養,提升系統的健壯性,提升使用者體驗,提升產品的價值。

誤解一:異常的選擇

圖1. 異常分類

Java 異常處理的誤解與經驗總結

圖1 描述了這兩異常的結構,其實我們都知道異常分檢測異常和非檢測異常,但是在實際中又混淆了這兩異常的結構,其實我們都知道異常分檢測異常和非檢測異常,但是在實際中又混淆了這兩異常種異常的應用。由於非檢測異常使用方便,許多開發人員就認為檢測異常沒什麼用。其實異常的應用情境可以概括為以下:

呼叫程式碼不能繼續執行,需要立即終止。發生這種情況的可能性太多太多,例如伺服器連線不上、參數不正確等。這些時候都適用非檢測異常,不需要呼叫程式碼的明確捕捉和處理,而且程式碼簡潔明了。

呼叫程式碼需要進一步處理和恢復。假如將SQLException 定義為非偵測異常,這樣操作資料時開發人員理所當然的認為SQLException 不需要呼叫程式碼的明確捕捉和處理,進而導致嚴重的Connection 不關閉、Transaction 不回滾、DB 中出現髒資料等情況,正因為SQLException 定義為偵測異常,才會驅使開發人員去明確捕捉,並且在程式碼產生異常後清理資源。當然清理資源後,可以繼續拋出非偵測異常,阻止程式的執行。根據觀察和理解,檢測異常大多可以應用於工具類中。

誤區二:將異常直接顯示在頁面或客戶端

將異常直接印在客戶端的例子屢見不鮮,以JSP 為例,一旦程式碼運行出現異常,預設容器將異常堆疊資訊直接列印在頁面上。其實從客戶角度來說,任何異常都沒有實際意義,絕大多數的客戶根本看不懂異常訊息,軟體開發也要盡量避免將異常直接呈現給使用者。


清單1

package com.ibm.dw.sample.exception;
/**
* 自定义 RuntimeException
* 添加错误代码属性
*/
public class RuntimeException extends java.lang.RuntimeException { 
    //默认错误代码 
   public static final Integer GENERIC = 1000000; 
   //错误代码
   private Integer errorCode; 
    public RuntimeException(Integer errorCode, Throwable cause) {
           this(errorCode, null, cause);
    }
    public RuntimeException(String message, Throwable cause) {
           //利用通用错误代码
           this(GENERIC, message, cause);
    }
    public RuntimeException(Integer errorCode, String message, Throwable cause) {
           super(message, cause);
           this.errorCode = errorCode;
    }
    public Integer getErrorCode() {
           return errorCode;
    } 
}
登入後複製

正如示例代碼所示,在異常中引入錯誤代碼,一旦出現異常,我們只要將異常的錯誤代碼呈現給用戶,或者將錯誤代碼轉換成更通俗易懂的提示。其實這裡的錯誤代碼還包含另一個功能,開發人員亦可以根據錯誤代碼準確的知道了發生了什麼類型異常。

迷思三:對程式碼層次結構的污染

我們常將程式碼分Service、Business Logic、DAO 等不同的層次結構,DAO 層中會包含拋出異常的方法,如清單2 所示:

清單2

public Customer retrieveCustomerById(Long id) throw SQLException {
//根据 ID 查询数据库
}
登入後複製

上面這段程式碼咋一看沒什麼問題,但是從設計耦合角度仔細考慮一下,這裡的SQLException 污染到了上層調用程式碼,調用層需要顯式的利用try-catch 捕捉,或者向更上層進一步拋出。根據設計隔離原則,我們可以適當地修改成:


清单 3

public Customer retrieveCustomerById(Long id) {
    try{
           //根据 ID 查询数据库
    }catch(SQLException e){
           //利用非检测异常封装检测异常,降低层次耦合
           throw new RuntimeException(SQLErrorCode, e);
    }finally{
           //关闭连接,清理资源
    }
}
登入後複製

误区四:忽略异常

如下异常处理只是将异常输出到控制台,没有任何意义。而且这里出现了异常并没有中断程序,进而调用代码继续执行,导致更多的异常。

清单 4

public void retrieveObjectById(Long id){
  try{
      //..some code that throws SQLException
   }catch(SQLException ex){
    /**
      *了解的人都知道,这里的异常打印毫无意义,仅仅是将错误堆栈输出到控制台。
      * 而在 Production 环境中,需要将错误堆栈输出到日志。
      * 而且这里 catch 处理之后程序继续执行,会导致进一步的问题*/
      
         ex.printStacktrace();
    }
}
登入後複製

可以重构成:

清单 5

public void retrieveObjectById(Long id){
try{
   //..some code that throws SQLException
}
catch(SQLException ex){
   throw new RuntimeException(“Exception in retieveObjectById”, ex);
}
finally{
   //clean up resultset, statement, connection etc
}
}
登入後複製

这个误区比较基本,一般情况下都不会犯此低级错误。

误区五:将异常包含在循环语句块中

如下代码所示,异常包含在 for 循环语句块中。

清单 6

for(int i=0; i<100; i++){
   try{
   }catch(XXXException e){
        //….
   }
}
登入後複製

我们都知道异常处理占用系统资源。一看,大家都认为不会犯这样的错误。换个角度,类 A 中执行了一段循环,循环中调用了 B 类的方法,B 类中被调用的方法却又包含 try-catch 这样的语句块。褪去类的层次结构,代码和上面如出一辙。

误区六:利用 Exception 捕捉所有潜在的异常

一段方法执行过程中抛出了几个不同类型的异常,为了代码简洁,利用基类 Exception 捕捉所有潜在的异常,如下例所示:

清单 7

public void retrieveObjectById(Long id){
   try{
       //…抛出 IOException 的代码调用
       //…抛出 SQLException 的代码调用
   }catch(Exception e){
       //这里利用基类 Exception 捕捉的所有潜在的异常,如果多个层次这样捕捉,会丢失原始异常的有效信息
       throw new RuntimeException(“Exception in retieveObjectById”, e);
   }
}
登入後複製

可以重构成

清单 8

public void retrieveObjectById(Long id){
   try{
       //..some code that throws RuntimeException, IOException, SQLException
   }catch(IOException e){
       //仅仅捕捉 IOException
       throw new RuntimeException(/*指定这里 IOException 对应的错误代码*/code,“Exception in retieveObjectById”, e);
   }catch(SQLException e){
       //仅仅捕捉 SQLException
       throw new RuntimeException(/*指定这里 SQLException 对应的错误代码*/code,“Exception in retieveObjectById”, e);
   }
}
登入後複製

误区七:多层次封装抛出非检测异常

如果我们一直坚持不同类型的异常一定用不同的捕捉语句,那大部分例子可以绕过这一节了。但是如果仅仅一段代码调用会抛出一种以上的异常时,很多时候没有必要每个不同类型的 Exception 写一段 catch 语句,对于开发来说,任何一种异常都足够说明了程序的具体问题。

清单 9

try{
   //可能抛出 RuntimeException、IOExeption 或者其它;
   //注意这里和误区六的区别,这里是一段代码抛出多种异常。以上是多段代码,各自抛出不同的异常
}catch(Exception e){
   //一如既往的将 Exception 转换成 RuntimeException,但是这里的 e 其实是 RuntimeException 的实例,已经在前段代码中封装过
   throw new RuntimeException(/**/code, /**/, e);
}
登入後複製

如果我们如上例所示,将所有的 Exception 再转换成 RuntimeException,那么当 Exception 的类型已经是 RuntimeException 时,我们又做了一次封装。将 RuntimeException 又重新封装了一次,进而丢失了原有的 RuntimeException 携带的有效信息。


解决办法是我们可以在 RuntimeException 类中添加相关的检查,确认参数 Throwable 不是 RuntimeException 的实例。如果是,将拷贝相应的属性到新建的实例上。或者用不同的 catch 语句块捕捉 RuntimeException 和其它的 Exception。个人偏好方式一,好处不言而喻。

误区八:多层次打印异常

我们先看一下下面的例子,定义了 2 个类 A 和 B。其中 A 类中调用了 B 类的代码,并且 A 类和 B 类中都捕捉打印了异常。

清单 10

public class A {
private static Logger logger = LoggerFactory.getLogger(A.class);
public void process(){
    try{
    //实例化 B 类,可以换成其它注入等方式
    B b = new B();
    b.process();
    //other code might cause exception
   } catch(XXXException e){
      //如果 B 类 process 方法抛出异常,异常会在 B 类中被打印,在这里也会被打印,从而会打印 2 次
      logger.error(e);
      throw new RuntimeException(/* 错误代码 */ errorCode, /*异常信息*/msg, e);
      }
   }
}
public class B{
private static Logger logger = LoggerFactory.getLogger(B.class);
   public void process(){
       try{
           //可能抛出异常的代码
       }
       catch(XXXException e){
           logger.error(e);
           throw new RuntimeException(/* 错误代码 */ errorCode, /*异常信息*/msg, e);
       }
}
}
登入後複製

同一段异常会被打印 2 次。如果层次再复杂一点,不去考虑打印日志消耗的系统性能,仅仅在异常日志中去定位异常具体的问题已经够头疼的了。

其实打印日志只需要在代码的最外层捕捉打印就可以了,异常打印也可以写成 AOP,织入到框架的最外层。


误区九:异常包含的信息不能充分定位问题

异常不仅要能够让开发人员知道哪里出了问题,更多时候开发人员还需要知道是什么原因导致的问题,我们知道 java .lang.Exception 有字符串类型参数的构造方法,这个字符串可以自定义成通俗易懂的提示信息。


简单的自定义信息开发人员只能知道哪里出现了异常,但是很多的情况下,开发人员更需要知道是什么参数导致了这样的异常。这个时候我们就需要将方法调用的参数信息追加到自定义信息中。下例只列举了一个参数的情况,多个参数的情况下,可以单独写一个工具类组织这样的字符串。

清单 11

public void retieveObjectById(Long id){
   try{
       //..some code that throws SQLException
  }catch(SQLException ex){
       //将参数信息添加到异常信息中
       throw new RuntimeException(“Exception in retieveObjectById with Object Id :”+ id, ex);
  }
}
登入後複製

误区十:不能预知潜在的异常

在写代码的过程中,由于对调用代码缺乏深层次的了解,不能准确判断是否调用的代码会产生异常,因而忽略处理。在产生了 Production Bug 之后才想起来应该在某段代码处添加异常补捉,甚至不能准确指出出现异常的原因。这就需要开发人员不仅知道自己在做什么,而且要去尽可能的知道别人做了什么,可能会导致什么结果,从全局去考虑整个应用程序的处理过程。这些思想会影响我们对代码的编写和处理。


误区十一:混用多种第三方日志库

现如今 Java 第三方日志库的种类越来越多,一个大项目中会引入各种各样的框架,而这些框架又会依赖不同的日志库的实现。最麻烦的问题倒不是引入所有需要的这些日志库,问题在于引入的这些日志库之间本身不兼容。如果在项目初期可能还好解决,可以把所有代码中的日志库根据需要重新引入一遍,或者换一套框架。但这样的成本不是每个项目都承受的起的,而且越是随着项目的进行,这种风险就越大。


怎麼樣才能有效的避免類似的問題發生呢,現在的大多數框架已經考慮到了類似的問題,可以通過配置Properties 或xml 文件、參數或者運行時掃描Lib 庫中的日誌實現類,真正在應用程序運行時才確定具體應用哪個特定的日誌庫。

其實根據不需要多層次列印日誌那條原則,我們就可以簡化很多原本調用日誌列印程式碼的類別。很多情況下,我們可以利用攔截器或過濾器實現日誌的列印,降低程式碼維護、遷移的成本。


結束語

以上純屬個人的經驗和總結,事物都是辯證的,沒有絕對的原則,適合自己的才是最有效的原則。希望以上的講解和分析可以對您有幫助。

以上就是Java 異常處理的誤解和經驗總結的內容,更多相關內容請關注PHP中文網(www.php.cn)!


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

在本文中,我們保留了最常被問到的 Java Spring 面試問題及其詳細答案。這樣你就可以順利通過面試。

您如何在PHP中有效處理異常(嘗試,捕捉,最後,投擲)? 您如何在PHP中有效處理異常(嘗試,捕捉,最後,投擲)? Apr 05, 2025 am 12:03 AM

在PHP中,異常處理通過try,catch,finally,和throw關鍵字實現。 1)try塊包圍可能拋出異常的代碼;2)catch塊處理異常;3)finally塊確保代碼始終執行;4)throw用於手動拋出異常。這些機制幫助提升代碼的健壯性和可維護性。

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

Java 中的時間戳至今 Java 中的時間戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的時間戳記到日期指南。這裡我們也結合範例討論了介紹以及如何在java中將時間戳記轉換為日期。

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

Java程序查找膠囊的體積 Java程序查找膠囊的體積 Feb 07, 2025 am 11:37 AM

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

PHP與其他語言:比較 PHP與其他語言:比較 Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

See all articles