首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 異常處理六大反模式剖析:從錯誤代碼學習最佳實務

     編輯:Java 相關知識

前言

異常處理是 Java 程式健壯性的基石,但許多開發者陷入「能編譯通過就好」的誤區。本文透過一段包含六個嚴重問題的程式碼片段,深度剖析常見反模式,並提供重構後的標準寫法。

問題代碼(含 6 個嚴重缺陷)


OutputStreamWriter out = ...;
Connection conn = ...;

try {                              // ③ try 區塊過大
    Statement stat = conn.createStatement();
    ResultSet rs = stat.executeQuery("select uid, name from user");
    while (rs.next()) {
        out.println("ID:" + rs.getString("uid")    // ⑥ 輸出可能不完整
            + ",姓名:" + rs.getString("name"));
    }
    conn.close();                  // ③ 資源釋放位置錯誤
    out.close();
}
catch (Exception ex) {             // ② 捕獲過寬泛異常
    ex.printStackTrace();          // ① 拋棄異常、④ 缺乏錯誤上下文
}

六大反模式詳細剖析

反模式一:拋棄異常

位置:catch 區塊僅 printStackTrace()

危害:

  • 生產環境日誌淹沒在標準錯誤輸出,無告警機制
  • 呼叫端不知發生錯誤,繼續執行導致資料不一致
  • 違反「失敗快速」原則,錯誤隱性傳播

修正策略(四選一):

  1. 就地處理:重試、降級、預設值、通知使用者
  2. 重新拋出:throw ex; 保持堆疊追蹤
  3. 轉換異常:包裝為業務異常 throw new BusinessException("查詢失敗", ex);
  4. 不捕獲:讓上層統一處理(最常見於 Controller/Service 邊界)

反模式二:捕獲過寬泛異常

位置:catch (Exception ex)

危害:

  • 吞噬 RuntimeException(如 NullPointerException),掩蓋程式邏輯錯誤
  • 同一 catch 區塊處理異質異常(SQL 錯誤 vs IO 錯誤),無法針對性恢復
  • 阻礙編譯器檢查受檢異常完整性

修正:分別捕獲具體異常


catch (SQLException sqle) { /* 資料庫錯誤處理 */ }
catch (IOException ioe) { /* IO 錯誤處理 */ }

反模式三:資源未正確釋放

位置:conn.close()、out.close() 在 try 區塊末尾

危害:若前面程式碼拋出異常,close() 永不執行 → 連線洩漏、檔案鎖定、記憶體耗盡

修正:使用 try-with-resources (Java 7+) 或 finally 區塊


// 推薦:try-with-resources 自動關閉實作 AutoCloseable 的資源
try (Connection conn = dataSource.getConnection();
     Statement stat = conn.createStatement();
     ResultSet rs = stat.executeQuery(sql);
     OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")) {
    
    while (rs.next()) {
        out.write(...);
    }
} // 編譯器自動產生 finally 關閉所有資源,且正確處理關閉時的異常抑制

反模式四:錯誤資訊不足

位置:ex.printStackTrace() 僅輸出堆疊

危害:

  • 缺乏業務上下文(哪個使用者、哪筆資料、什麼操作)
  • 堆疊追蹤難閱讀,無法快速定位根因
  • 無法關聯分散式追蹤

修正:使用結構化日誌(SLF4J + Logback/Log4j2)


log.error("查詢用戶列表失敗, userId={}, sql={}", userId, sql, ex);
// 輸出:ERROR [http-nio-8080-exec-1] c.s.UserService - 查詢用戶列表失敗, userId=123, sql=select... 
// java.sql.SQLException: Connection timeout

反模式五:try 區塊過大

位置:整段業務邏輯包在單一 try

危害:

  • 難以判斷具體哪行拋出異常
  • 迫使捕獲寬泛異常
  • 混合不同錯誤處理邏輯

修正:最小化 try 區塊,僅包含可能拋出目標異常的操作


// 好:精準包裹資料庫操作
List users;
try (Connection conn = ds.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    users = executeQuery(ps);
}
// 好:精準包裹檔案寫入
try (Writer out = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")) {
    writeUsers(out, users);
}

反模式六:輸出資料不完整無提示

位置:迴圈中寫入,異常導致中斷無標記

危害:下游系統收到截斷資料,誤以為完整,導致錯誤決策

修正:

  1. 緩衝後一次寫入:記憶體允許時,先組裝完整內容再寫入
  2. 寫入失敗標記:在輸出流寫入錯誤標記或傳輸長度前綴
  3. 事務性輸出:寫暫存檔,成功後原子性重命名

重構後的標準範例


public void exportUsers(File outputFile) throws ExportException {
    String sql = "SELECT uid, name FROM user ORDER BY uid";
    
    // 1. 讀取資料(獨立 try 區塊)
    List users;
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql);
         ResultSet rs = ps.executeQuery()) {
        
        users = new ArrayList<>();
        while (rs.next()) {
            users.add(new User(rs.getLong("uid"), rs.getString("name")));
        }
    } catch (SQLException e) {
        log.error("讀取用戶資料失敗, sql={}", sql, e);
        throw new ExportException("資料庫查詢失敗", e);
    }
    
    // 2. 寫入檔案(獨立 try 區塊,緩衝後寫入)
    try (BufferedWriter out = Files.newBufferedWriter(outputFile.toPath(), StandardCharsets.UTF_8)) {
        out.write("ID,姓名\n");
        for (User u : users) {
            out.write(u.getId() + "," + u.getName() + "\n");
        }
        out.flush(); // 確保寫入磁碟
    } catch (IOException e) {
        log.error("寫出用戶檔案失敗, file={}", outputFile, e);
        // 嘗試刪除不完整檔案
        Files.deleteIfExists(outputFile.toPath());
        throw new ExportException("檔案寫入失敗", e);
    }
}

現代 Java 異常處理最佳實務清單

  1. ✅ 使用 try-with-resources 管理資源
  2. ✅ 捕獲 具體異常型別,避免 catch (Exception)
  3. ✅ 異常轉換時 保留原始原因 (throw new BizEx(msg, cause))
  4. ✅ 使用 結構化日誌 記錄完整上下文
  5. ✅ 最小化 try 區塊 範圍
  6. ✅ 區分 業務異常 (Checked) 與 系統異常 (Unchecked)
  7. ✅ 定義 全域異常處理器 (@ControllerAdvice) 統一回應格式
  8. ✅ 避免在 finally 中拋出異常(會掩蓋原始異常)
  9. ✅ 考慮 冪等性 與 重試機制(暫時性錯誤)

總結

良好的異常處理不只是「不讓程式當機」,而是讓錯誤可見、可追蹤、可恢復、可審計。將這六大反模式內化為肌肉記憶,配合現代 Java 語法,能顯著提升系統穩定性與維運效率。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved