iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0

昨天我們在進行功能測試的時候出現了意外的問題,也在文末點出了問題的原因。
因為與 Exception 的處理有關,今天決定花一點時間來整理先前認識 Exception 的筆記,好銜接明天的內容:)!

什麼是Exception?

講 Exception 之前,先複習一下族譜 XD

https://ithelp.ithome.com.tw/upload/images/20250924/201780992cqydR8dP7.png
圖片來源:oreilly - PURE Java™ 2

在 Java 的異常處理機制中,Throwable 是所有 ErrorException 的父類別(如上圖)。只有 Throwable 類別(或其子類)的實例才能被 JVM 拋出,或者被 Java 的 throw 語句拋出。理解 Throwable 是掌握 Java 異常處理的關鍵第一步!

無法處理的 Error

Error 及其子類別是系統拋出的問題,代表 Java 運行環境出現問題,這些問題通常是無法事先處理的。例如:

  • OutOfMemoryError: 記憶體不足錯誤。當 JVM 沒有足夠的記憶體來為物件分配空間,並且垃圾回收器也無法釋放出更多記憶體時拋出。
  • StackOverflowError: 堆疊溢位錯誤。通常發生在遞迴呼叫沒有終止條件,導致方法調用堆疊深度超過限制時。

可以處理的 Exception

Exception 及其子類別則是開發人員可以預先處理的問題,又分為兩大類:

1. 編譯時異常 (Checked Exception)

非 RuntimeException,也就是在編譯時就會被編譯器檢查到,提醒開發者這裡存在可預見的問題,此時就會強制要求開發人員進行處理。常見的例子有:

  • IOException: 處理輸入/輸出操作時可能發生的問題,如讀取檔案失敗。
  • SQLException: 與資料庫互動時可能發生的錯誤。

2. 運行時異常 (Unchecked Exception)

即 RuntimeException,編譯時尚未發現的異常,通常是在運行時由於程式邏輯錯誤導致的異常,因此不會強制要求開發人員預先處理,常見例子有:

  • NullPointerException: 當試圖在一個值為 null 的物件參考上呼叫方法或存取屬性時拋出。
  • ArrayIndexOutOfBoundsException: 試圖存取陣列時,使用了無效的索引。
  • IllegalArgumentException: 傳遞給方法的參數不合法或不適當。

MethodArgumentNotValidException 是哪一種 Exception?

按照上面的邏輯聽起來,可能會覺得 MethodArgumentNotValidException 屬於 RuntimeException,實則不然。到 Java 官方文件 中可以看到,MethodArgumentNotValidException 事實上繼承於 BindException ,而 BindExceptionException 的子類別,也就是說他實際上是一個 Checked Exception 。

那為什麼我們並沒有在編譯時就收到錯誤?

那是因為,在 Controller 上使用 @Valid 驗證參數時,Spring 已經將整個捕捉和處理的過程封裝了,開發人員通常不需要自己捕捉、處理此類錯誤。

取而代之,Spring 的 DispatcherServlet 捕捉到這個異常後,會啟動異常處理機制(Exception Handling Mechanism),去尋找 @ControllerAdvice@RestControllerAdvice 中有沒有定義此類異常的 @ExceptionHandler

我們在先前的流程中並沒有特別處理 Exception,後續的過程就會像我們在 log 中看到的一樣,轉發至/error 路徑時被Spring Security攔截並回傳了 403 Forbidden。

怎麼處理Exception?

try-catch 或 Throw Excption

在 Java 中,處理例外最基礎的方式就是 try-catch (捕捉處理) 與 throw (拋出傳遞)。throw 用於在偵測到錯誤時發出信號,而 try-catch 則是用來接收這個信號並進行處理。

例如在 Checked Exception 發生時,我們就被強制撰寫 try-catch 或 throw Excption 來讓程式碼通過編譯。或在預見可能出現 RuntimeException 時,透過 try-catch 捕捉並處理錯誤,如同我們在註冊用戶的 API 端點中撰寫的對應內容:


...
  @PostMapping
  public ResponseEntity<?> registerUser(@Valid @RequestBody UserRegistrationRequest request) {
      try {
          authService.registerUser(request);
          return ResponseEntity.status(HttpStatus.CREATED).build();
      } catch (IllegalStateException e) {
          // 手動捕捉信箱已存在的例外
          return ResponseEntity.badRequest().body(e.getMessage());
      }
  }

這麼做雖然很直觀,但有幾個缺點:

  • 內容冗贅:為了捕捉錯誤而在多處撰寫 try-catch,使得程式碼變得非常冗贅。
  • 職責不清:容易讓一個類別或方法參雜了過多的例外處理,難以符合單一職責原則。
  • **無法處理框架層級的例外:**就像 MethodArgumentNotValidException ,請求根本還沒到達Controller就被框架攔截,無從捕捉。

@RestControllerAdvice + @ExceptionHandler

為了解決上述問題,Spring 提供了全域例外處理 (Global Exception Handling) 機制。

@RestControllerAdvice 是 Spring MVC 框架提供的核心功能之一,這個 Annotation 用來修飾類別,類別中用來處理特定 Exception 的方法則以 @ExceptionHandler 進行修飾。

@RestControllerAdvice 等同於 @ControllerAdvice + @ResponseBody,除了可搭配@ExceptionHandler 來捕捉特定 Exception 外,錯誤的回傳皆預設轉換成 JSON/XML 回應,非常適合 RESTful API,也確保錯誤回應的格式清晰且一致。

為什麼說使用 RestControllerAdvice + @ExceptionHandler 可以解決 try-catch 的問題?因為我們可以透過建立一個 GlobalExceptionHandler 的類別集中處理來自各個 Controller 的 Exception,以明天實際修復 MethodArgumentNotValidException 的例子來說,可能就會像下面這個樣子:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        // ... 將錯誤整理成 Map ...
        return ResponseEntity.badRequest().body(errors);
    }
}

如此一來每當 Exception 發生時,由 Spring 為我們掃描有無對應的 @ExceptionHandler 方法,不需要撰寫過多的 try-catch,也可以處理被框架封裝的例外捕捉,並且將 Exception 集中管理,也大大提升了未來維護的效率。


今天我們複習了 Exception 的概念,也學習到了如何以 @RestControllerAdvice 更方便的管理 Exception。雖然明天會以上述的方式改寫當前 try-catch 的內容,但其實兩者並非相斥,而是相輔相成的解決方法,如果篇幅允許,屆時再來舉個合併使用的案例!


上一篇
Day9:Auth Service - 註冊功能測試與除錯
下一篇
Day11:Auth Service -Global Exception
系列文
吃出一個SideProject!13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言