本文へスキップ
BecomeCoder

Spring / Spring Bootコース · 第6章 実践 ― 実務で通用するSpringアプリへ · レッスン22

エラーへの対応 ― @ExceptionHandlerと@ControllerAdvice

ローカル実施

導入

前レッスンで、バリデーションに失敗すると例外が飛ぶと説明しました。この例外をそのまま放置すると、クライアントには内部のスタックトレースを含む生々しいエラー画面が返ってしまいます。これは避けたい事態です。

説明

特定のControllerの中だけで例外を処理したいなら、そのControllerに @ExceptionHandler メソッドを追加します。

@RestController
class UserApiController {

    // このController内で MethodArgumentNotValidException が発生したらここが呼ばれる
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldError().getDefaultMessage();
        return ResponseEntity.badRequest().body(new ErrorResponse(message));
    }
}

しかし、同じような例外処理をControllerごとにコピペするのは非効率です。アプリ全体で共通のエラー処理をまとめたい場合は、@ControllerAdvice を付けた専用クラスを1つ用意します。

@RestControllerAdvice // @ControllerAdvice + @ResponseBody
class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult().getFieldError().getDefaultMessage();
        return ResponseEntity.badRequest().body(new ErrorResponse(message));
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse(ex.getMessage()));
    }

    @ExceptionHandler(Exception.class) // 上のどれにも当てはまらない、想定外の例外
    public ResponseEntity<ErrorResponse> handleOther(Exception ex) {
        return ResponseEntity.internalServerError()
                .body(new ErrorResponse("サーバー内部でエラーが発生しました"));
    }
}
graph TD
  Any["アプリ内のどのControllerで<br/>例外が発生しても"] --> Advice["@RestControllerAdvice<br/>のクラスが横断的に受け止める"]
  Advice --> R1["バリデーション例外<br/>→ 400 Bad Request"]
  Advice --> R2["未検出の例外<br/>→ 404 Not Found"]
  Advice --> R3["想定外の例外<br/>→ 500 Internal Server Error"]

@ControllerAdvice を1つ用意しておくことで、「アプリ全体でどんな例外が、どんなHTTPステータスコードとエラーメッセージになって返るか」を1か所に集約できます。各Controllerは業務ロジックに専念でき、エラー処理を気にする必要がなくなります。

やってみよう

「注文しようとした商品の在庫が足りない」という業務エラーを表す OutOfStockException を新しく作るとしたら、@RestControllerAdvice にどんなハンドラを追加し、どのHTTPステータスコード(400? 404? 409?)を返すのがふさわしいか考えてみましょう。