昨天,我們延伸了前天遇到的問題,複習了 Exception 的概念,並分析我們遇到的錯誤,最後提出了幾個 Exception 常見的處理方式。
今天想延伸昨天介紹的內容,實際應用在專案上解決我們遇到的問題。
但在此之前,我想先驗證前天的說法,確認我們的推論沒問題後,再進一步進行改寫。
前天提到,我們的 API 回傳收到了預期外的 403 Forbidden。對這個問題,我們的推論是:
MethodArgumentNotValidException
/error
SecutiryFilterChain
的設定,這個路徑被擋下了(權限不足)。為了驗證這個說法,我們可以透過在 SecurityConfig
開放 /error
的權限,並重發一次請求,來查看結果會怎麼樣,改寫內容如下:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST,"/users/**").permitAll()
**.requestMatchers("/error").permitAll()**
.anyRequest().authenticated()
);
return http.build();
}
加上 requestMatchers("/error").permitAll()
後,回到 API Tester 重發一次請求:
終於,如我們預期的,這次收到了 400 Bad Request 的回應。不過雖然如預期收到了回應,但我們可以看到錯誤訊息中並沒有提及觸發 Bad Request 的是哪一個參數。針對這個問題,開放權限也不是一個根本的解決方法,因此我們還是要針對 Exception 的問題進行處理。
在此服務下新增一個 exception 的 package,並建立 GlobalEexception
類別。
在類別上加上 @RRestControllerAdvice
的註解,並新增 handleValidationExceptions
方法,並在方法上加上 @ExceptionHandler
,傳入我們要處理的 Exception 類別。詳細內容如下:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
// 走訪所有驗證錯誤,將欄位名稱和錯誤訊息放入 Map 中
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
...
}
該方法回傳一個 ResponseEntity 的物件,我們迴圈取得錯誤訊息包含的欄位以及對應訊息後,放入 Map,最後可以指定 HttpStatusCode,一起回傳給調用的函式。
RestControllerAdvice
: 加上這個註解後的類別會監聽所有 @RestController 中拋出的例外,並尋找對應的處理方法。因為內建了 @ResponseBody,所以方法的回傳值會被自動序列化為 JSON。
@ExceptionHandler({Exception.class})
: 標註在方法上,宣告這個方法是特定類型 Exception 的處理器。當被監聽的 Controller 拋出指定的例外時,這個方法就會被觸發。
我們在 Service 檢查有無重複註冊的信箱時,有拋出 IllegalStateException,Controller 捕捉了這個RuntimeException,我們可以用相同的方式處理這個錯誤,詳細內容如下:
...
@ExceptionHandler(IllegalStateException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Map<String, String>> handleIllegalStateException(IllegalStateException ex) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", ex.getMessage());
return ResponseEntity.badRequest().body(errorResponse);
}
...
到 Controller 中,將 try-catch 移除,不僅是在整理程式碼,移除 try-catch 後若 ExceptionHandler 未正常生效,一樣會遇到 403 Frobidden 的問題,如此一來可以讓我們更好驗證是否成功處理 Exception。
...
@PostMapping
public ResponseEntity<?> registerUser(@Valid @RequestBody RegistrationRequest registrationRequest) {
UserEntity user = authService.registerUser(registrationRequest);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
...
設定好後,記得移除 /error 權限,以驗證我們發生的 Exception 不再被轉發至 /error 。
參數檢核: 再次以不符合長度規則的密碼進行測試,可以看到下圖的結果正確返回了400 Bad Request 以及 Response Body 中出現密碼不符合長度規格的訊息。因為請求還沒到 Controller 就被攔截,因此先出現參數檢核的錯誤而非信箱重覆註冊的錯誤也符合預期。
重複註冊信箱: 這次測試以重複郵件地址進行註冊,可以看到下圖結果正確返回了400 Bad Request 以及 Response Body 中出現了我們在Exception中自訂的錯誤訊息。
兩種測試結果都表明,我們的 GlobalExceptionHandler 成功地捕捉了來自框架的 Checked Exception 和我們自己拋出的 RuntimeException。
今天,我們成功驗證了 403 錯誤的成因。在驗證結果後,建立了GlobalException 來統一管理 Exception,最後也確認了功能如我們預期一般地回應。
明天,我想我們可以開始進行登入功能的實作了!(終於XD)