S SmartDocs
系列: Springboot java 130 行 · 更新於 2026-08-14

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);
    }
}

相關文章

Springboot java EN 更新於 2026-08-14

BookstoreApiApplication.java

BookstoreApiApplication.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/BookstoreApiApplication.java).

閱讀文章 →
Springboot java EN 更新於 2026-08-14

DataInitializer.java

DataInitializer.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/DataInitializer.java).

閱讀文章 →
Springboot java EN 更新於 2026-08-14

SecurityConfig.java

SecurityConfig.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/SecurityConfig.java).

閱讀文章 →
Springboot java EN 更新於 2026-08-14

AuthController.java

AuthController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/AuthController.java).

閱讀文章 →
Springboot java EN 更新於 2026-08-14

BookController.java

BookController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/BookController.java).

閱讀文章 →
Springboot java EN 更新於 2026-08-14

ApiErrorResponse.java

ApiErrorResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/ApiErrorResponse.java).

閱讀文章 →