
在 Spring Boot 應用程序中,有效的錯誤處理對於提供良好的用戶體驗和便於除錯至關重要
現在,讓我們更進一步,實現全域錯誤處理,以確保我們的 API 能夠優雅地處理各種可能發生的錯誤
首先,我們可以在 TodoController 中添加一個異常處理方法
在 method 上面加上 @ExceptionHandler annotation
參數是要補捉的異常類型,為了簡單,這裡使用 Exception
@RestController
@RequestMapping("/api/todos")
public class TodoController {
    // 其他程式碼保持不變
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
        ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
            "https://example.com/errors/internal-error",
            "Internal Server Error",
            HttpStatus.INTERNAL_SERVER_ERROR,
            e.getMessage(),
            "/api/todos"
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse<>(false, null, error));
    }
}
我們可以在另一個 method 裡面寫會 throw exception 的程式碼
var a  = 0;
var b = 1;
var r = b / a;
可以看到呼叫 API 後,收到的錯誤訊息

為了示範方便,我們先為 todo not found 建立一個自定義的 Exception
public class TodoNotFoundException extends Exception {
    public TodoNotFoundException(Long id) {
        super("Todo not found with id: " + id);
    }
}
建立一個 全域的異常處理類
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(TodoNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleTodoNotFoundException(TodoNotFoundException e) {
        ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
                "https://example.com/errors/not-found",
                "Todo not found",
                HttpStatus.NOT_FOUND,
                e.getMessage(),
                apiPath()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiResponse<>(false, null, error));
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
        String apiPath = apiPath();
        ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
                "https://example.com/errors/internal-error",
                "Internal Server Error",
                HttpStatus.INTERNAL_SERVER_ERROR,
                e.getMessage(),
                apiPath
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse<>(false, null, error));
    }
    private static String apiPath() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        return request.getRequestURI();
    }
}
另外,有寫了一個共用的方式 apiPath(),來取得目前呼叫的 API path,方便出錯時,讓使用者知道是那個 path 有問題
當然,都可以根據自己的需求來客制化調整
在其它的 controller 寫測試的程式碼試試
@GetMapping("/ex")
public String ex() {
    var a = 0;
    var b = 1;
    var c = b / a;
    return "";
}
可以看到,這裡的錯誤訊息,會是全域的錯誤處理類別 Exception 裡面的訊息

然後我們可以修改 Todo 中一個處理 not found 情況的程式碼,改為 throw TodoNotFoundException 來測試
 @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<Todo>> getTodo(@PathVariable Long id) throws
                                                                            TodoNotFoundException {
        Optional<Todo> todo = todos.stream()
                .filter(t -> t.getId().equals(id))
                .findFirst();
        if (todo.isPresent()) {
            return ResponseEntity.ok(new ApiResponse<>(true, todo.get(), null));
        } else {
            throw new TodoNotFoundException(id);
        }
    }
可以看到,這裡的錯誤訊息,會是全域的錯誤處理類別 TodoNotFoundException 裡面的訊息

繼承 Spring 提供的 ResponseEntityExceptionHandler 類,可以處理更多 Spring 特定的異常
我們調整上一個 GlobalExceptionHandler 的範列
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        ApiResponse.ErrorDetails error = new ApiResponse.ErrorDetails(
                "https://example.com/errors/invalid-json",
                "無法解析請求內容",
                HttpStatus.BAD_REQUEST,
                ex.getMessage(),
                request.getDescription(false)
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ApiResponse<>(false, null, error));
    }
}
這種方法提供了最大的靈活性和控制,可以處理各種 Spring 特定的異常
在 IDE 可以看到 override 的 method 類型非常多

以這裡的 handleHttpMessageNotReadable 為例
我們可以傳送一個不正確的 JSON,這個錯誤就可以被捕捉到了
### 創建一個新的 Todo
POST http://localhost:8080/api/todos
Content-Type: application/json
{
  "title":
  "completed": false
}
可以看到,這裡的錯誤訊息,會是 handleHttpMessageNotReadable 裡面的訊息

@ExceptionHandler ):適用於只需要在特定控制器中處理異常的簡單場景@ControllerAdvice ):適用於需要統一處理多個控制器異常的中等複雜度場景ResponseEntityExceptionHandler ):適用於需要精細控制 Spring MVC 異常處理的複雜場景選擇哪種方法取決於你的應用程序的複雜度和特定需求
對於大多數應用程序,使用 @ControllerAdvice 通常是一個很好的平衡點,它提供了足夠的靈活性和全域控制,同時保持相對簡單的實現
全域異常處理是構建健壯 API 的重要組成部分,它能夠大大提高代碼的可維護性和一致性
雖然實現起來可能稍顯複雜,但長遠來看,這種投資是值得的,特別是在大型項目或需要長期維護的 API 中,全域異常處理的優勢會更加明顯
同步刊登於 Blog 「Spring Boot API 開發:從 0 到 1」Day 13 - 全域錯誤處理
我的粉絲專頁
圖片來源:AI 產生