GlobalExceptionHandler.java
Springboot/bookstore-api/src/main/java/com/example/bookstore/exception/GlobalExceptionHandler.java
package com.example.bookstore.exception;
import com.example.bookstore.dto.ApiErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 全域例外處理器(Global Exception Handler)。
*
* 【運作原理】
* @RestControllerAdvice = @ControllerAdvice + @ResponseBody
* 它像一張「安全網」,攔截所有 Controller / Service 拋出的例外,
* 依例外類型轉換成對應的 HTTP 狀態碼與統一 JSON 格式。
*
* 【好處】
* 1. Controller / Service 不必到處寫 try-catch,邏輯乾淨。
* 2. 所有錯誤回應格式一致(ApiErrorResponse),前端好處理。
* 3. 錯誤與狀態碼的對應關係集中在一個檔案,一目了然。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 404 Not Found:查無資料。
* 例如 GET /api/books/999 但資料庫沒有 id=999 的書。
*/
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException ex, HttpServletRequest request) {
return build(HttpStatus.NOT_FOUND, ex.getMessage(), request);
}
/**
* 409 Conflict:資料重複(違反唯一性)。
* 例如註冊了已存在的帳號、新增了重複的 ISBN。
*/
@ExceptionHandler(DuplicateResourceException.class)
public ResponseEntity<ApiErrorResponse> handleDuplicate(
DuplicateResourceException ex, HttpServletRequest request) {
return build(HttpStatus.CONFLICT, ex.getMessage(), request);
}
/**
* 400 Bad Request:請求本文的「欄位驗證」失敗。
* 當 @Valid 驗證不通過時,Spring 會拋出這個例外,
* 我們把每個欄位的錯誤訊息整理成 map 回傳,方便前端逐欄顯示。
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiErrorResponse> handleValidation(
MethodArgumentNotValidException ex, HttpServletRequest request) {
// 收集所有欄位錯誤:{ 欄位名稱: 錯誤訊息 }
Map<String, String> fieldErrors = new HashMap<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
fieldErrors.put(fieldError.getField(), fieldError.getDefaultMessage());
}
ApiErrorResponse body = ApiErrorResponse.of(
HttpStatus.BAD_REQUEST, "輸入資料驗證失敗",
request.getRequestURI(), fieldErrors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
}
/**
* 400 Bad Request:請求本文不是合法的 JSON。
* 例如漏了逗號、引號,或 Content-Type 對但內容是空的。
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiErrorResponse> handleUnreadable(
HttpMessageNotReadableException ex, HttpServletRequest request) {
return build(HttpStatus.BAD_REQUEST, "請求本文格式錯誤,請確認是合法的 JSON", request);
}
/**
* 401 Unauthorized:登入失敗(帳號或密碼錯誤)。
* AuthenticationManager 驗證失敗時拋出 BadCredentialsException。
* 資安小知識:訊息刻意不說「是帳號錯還是密碼錯」,避免被駭客列舉帳號。
*/
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiErrorResponse> handleBadCredentials(
BadCredentialsException ex, HttpServletRequest request) {
return build(HttpStatus.UNAUTHORIZED, "帳號或密碼錯誤", request);
}
/**
* 403 Forbidden:已登入,但「權限不足」。
* 例如 ROLE_USER 嘗試呼叫只有 ROLE_ADMIN 能用的刪除 API,
* @PreAuthorize 檢查失敗時會拋出 AccessDeniedException。
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiErrorResponse> handleAccessDenied(
AccessDeniedException ex, HttpServletRequest request) {
return build(HttpStatus.FORBIDDEN, "權限不足,無法執行此操作", request);
}
/**
* 500 Internal Server Error:兜底處理。
* 任何沒被上面攔到的例外都會到這裡,避免把堆疊追蹤(stack trace)
* 等內部細節外洩給客戶端;完整錯誤只記錄在伺服器日誌。
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleGeneral(
Exception ex, HttpServletRequest request) {
log.error("未預期的錯誤", ex); // 完整堆疊寫入伺服器日誌,方便除錯
return build(HttpStatus.INTERNAL_SERVER_ERROR,
"伺服器發生未預期的錯誤,請稍後再試", request);
}
/** 共用的回應建構方法,避免重複程式碼 */
private ResponseEntity<ApiErrorResponse> build(
HttpStatus status, String message, HttpServletRequest request) {
ApiErrorResponse body = ApiErrorResponse.of(status, message, request.getRequestURI());
return ResponseEntity.status(status).body(body);
}
}
Related articles
BookstoreApiApplication.java
BookstoreApiApplication.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/BookstoreApiApplication.java).
Read article →DataInitializer.java
DataInitializer.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/DataInitializer.java).
Read article →SecurityConfig.java
SecurityConfig.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/SecurityConfig.java).
Read article →AuthController.java
AuthController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/AuthController.java).
Read article →BookController.java
BookController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/BookController.java).
Read article →ApiErrorResponse.java
ApiErrorResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/ApiErrorResponse.java).
Read article →