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 預設的運作流程
- 使用 @ControllerAdvice + @ExceptionHandler 註解後的運作流程
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); }
運行結果
登入失敗:密碼錯誤(400) 註冊失敗:帳號重複(409)