To-Do Calendar - Day25 Controller 層統一的例外處理

程式碼中存在很多通過 throw 拋出的 Exception,如果在 Controller 層每個方法都加上 try、catch,難保證散落在各方法的處理方式的一致性,所以這篇將使用 @ControllerAdvice 和 @ExceptionHandler 註解,去對 Controller 中的所有方法,進行統一的 Exception 處理。

統一例外處理的好處

  • 可以針對 Exception 返回統一的 http Response。
  • 將 Controller 層的 Exception 進行統一處理,減少重複的程式碼,提升擴展性和可維護性。

Spring Boot 提供的功能

  • ResponseEntity:自定義每個方法所返回的 http Response。
  • @ControllerAdvice + @ExceptionHandler:自定義每個 Exception 所返回的 http Response。

@ControllerAdvice

  • 只能加在 class 上。
  • @ControllerAdvice 是在 Spring 3.2 新增的註解,可以用來攔截並處理應用程式中全部 Controller 所拋出的 Exception 例外錯誤。
  • 本身也是 @Component,所以會被 Spring 掃描為 Bean。
  • 需要配合 @ExceptionHandler 使用。

@ExceptionHandler

  • 只能加上方法上。
  • 去 catch 方法所噴出的 Exception。

使用 @ControllerAdvice + @ExceptionHandler 後的運作流程差異

  • Spring Boot 預設的運作流程
    image
  • 使用 @ControllerAdvice + @ExceptionHandler 註解後的運作流程
    image

Info

@ControllerAdvice 註解底層的實現邏輯,就是透過 AOP 所實現的。

  • @ControllerAdvice 註解,就像是專門處理 Controller 所噴出的 Exception 的切面。
  • 加上 @ControllerAdvice 註解的 class,就會去橫貫所有加上 @Controller 或是 @RestController 註解的 class。
  • 當任何一個 Controller 裡面的方法噴出 Exception 時,@ControllerAdvice 註解就會偵測到,進而去找有沒有對應的 @ExceptionHandler 註解,可以去接住那個 Ecxception。


作法

建立自定義 Excetion 類別

  • 我們可以自己定義例外的類別,為應用程式的各種狀況提供客製化的例外類別。
  • 先定義一個 ServiceException,再讓其他自定義的例外類別去繼承,這樣使用 @ExceptionHandler 接例外的時候比較方便。
  • ServiceException
      public class ServiceExcetion extends RuntimeException {
          private String errorMessage;
    
          public ServiceExcetion(String errorMessage) {
              this.errorMessage = errorMessage;
          }
      }
    
  • PasswordNotMatchException
      public class PasswordNotMatchException extends ServiceExcetion {
          public AccountNotFoundException(String errorMessage) {
              super(errorMessage);
          }
      }
    
  • AccountNotFoundException
      public class AccountNotFoundException extends ServiceExcetion {
          public AccountNotFoundException(String errorMessage) {
              super(errorMessage);
          }
      }
    
  • AccountDuplicateException
      public class AccountDuplicateException extends ServiceExcetion {
          public AccountDuplicateException(String errorMessage) {
              super(errorMessage);
          }
      }
    

建立統一處理例外的 GlobalExceptionHandler

  • 在 class 上加上 @ControllerAdvice 註解。
  • 新增一個 handle 方法,並加上 @ExceptionHandler 註解,其 value 設為 ServiceExcetion.class,這樣 handle 方法就會處理所有 Controller 層拋出的 ServiceExcetion 及其子類的 Exception。
    @ControllerAdvice
    public class GlobalExceptionHandler {
        @ExceptionHandler(ServiceExcetion.class)
        public ResponseEntity<String> handle(ServiceExcetion e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getErrorMessage());
        }
    }
    

修改 Service 層的方法

  • login
      @Override
      public User login(UserLoginRequest userLoginRequest) {
        // 檢查帳號是否存在
        User user = userDao.getUserByEmail(userLoginRequest.getEmail());
        if (user == null) {
            throw new AccountNotFoundException("帳號或密碼錯誤");
        }
        if (!user.getPassword().equals(userLoginRequest.getPassword())) {
            throw new PasswordNotMatchException("帳號或密碼錯誤");
        }
      }
    
  • register
      @Override
      public User register(UserRegisterRequest userRegisterRequest) {
        // 檢查帳號是否重複註冊
        User user = userDao.getUserByEmail(userRegisterRequest.getEmail());
    
        if (user != null) {
            throw new AccountDuplicateException("該 email 已被註冊");
        }
    
        return userDao.createUser(userRegisterRequest);
    }
    

運行結果

image

登入失敗:密碼錯誤(400)

image

註冊失敗:帳號重複(409)



參考資料

comments

comments powered by Disqus