spring.svg

【Spring Boot】エラーハンドリング(REST API)

SpringBoot

例外スロー

自作例外クラス

Spring Boot では、自作した例外クラスに@ResponseStatusで HTTP ステータスを設定することができます。 この例外がスローされると、設定したステータスのレスポンスが返されます。

注意点として、非検査例外(RuntimeExceptionおよびサブクラス)を継承する必要があります。

@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
  public NotFoundexception(String message) {
    super(message);
  }
}

ResponseStatusException

@ResponseStatusExceptionは、例外スロー時に HTTP ステータスを設定することで、そのステータスのレスポンスを返すことができます。

@PostMapping
public void create(@RequestBody @Validated SampleDataParam param, BindingResult result) {
  if (result.hasErrors()) {
    throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
  }
  //
}

ResponseEntityExceptionHandler

ResponseEntityExceptionHandlerは抽象クラスとなっており、特定の例外がスローされた場合にどのようなレスポンスを返すかが実装されています。

以下の例外が対象になります。

例外ステータス説明
HttpRequestMethodNotSupportedException405定義していない HTTP メソッドでアクセスされた場合の例外
HttpMediaTypeNotSupportedException415サポートしていないContent-Typeを受信した場合の例外
appilication/jsonに対し、application/x-www-form-urlencodedで送信したなど
HttpMediaTypeNotAcceptableException406指定したContent-Type以外のデータをレスポンスに設定した場合の例外
MissingPathVariableException500パスパラメーターにおいて、パス定義({id})と参照定義(@PathVariable("id"))が異なる場合の例外
MissingServletRequestParameterException400@RequestParam(required = true)のパラメーターが存在しない場合の例外
ServletRequestBindingException400バインディングに関する例外
ConversionNotSupportedException500Bean プロパティに適したエディターまたはコンバーターが見つからない場合の例外
TypeMismatchException400パラメーターの値が定義した型に変換できない場合などの例外
HttpMessageNotReadableException400@RequestBodyによる変換に失敗した場合の例外
HttpMessageNotWritebleException500@ResponseBodyによる変換に失敗した場合の例外
MethodArgumentNotValidException400DTO によるバリデーションエラーの例外
MissingServletRequestPartException400multipart/form-dataの一部が存在しない場合の例外
BindException400@RequestParam@PathVariableによるバリデーションエラーの例外
AsyncRequestTimeoutException503非同期リクエストがタイムアウトした場合の例外

各例外の処理は、以下のようなhandleメソッドによって定義されています。

protected ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
  return handleExceptionInternal(ex, null, headers, status, request);
}

すべてのhandleメソッドは、handleExceptionInternal()を実行します。ここでレスポンスの情報が生成されます。

protected ResponseEntity<Object> handleExceptionInternal(
        Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
  if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
    request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
  }
  return new ResponseEntity<>(body, headers, status);
}

エラーレスポンスのカスタマイズ

特定の例外が発生した場合に、決まったエラー情報をレスポンスボディに設定することを考えます。

{
  "status": 400,
  "message": "Invalid Parameter"
}

エラークラスの作成

レスポンスボディに設定するエラー情報用のクラスを作成します。

public class ResponseError {
  private int status;
  private String message;

  public ResponseError(int status, String message) {
    this.status = status;
    this.message = message;
  }
}

ControllerAdvice

作成したすべての Controller で、共通の例外処理を行いたいと考えます。

このような場合にはControllerAdviceクラスを作成します。 REST API では@RestControllerAdviceをクラスに付与します。

@RestControllerAdvice
public class ApiControllerAdvice {
  //...
}

エラーレスポンスの設定

例外に関する処理を定義する場合は、上述したResponseEntityExceptionHandlerを継承します。

@RestControllerAdvice
public class ApiControllerAdvice extends ResponseEntityExceptionHandler {
  //...
}

handleExceptionInternal()をオーバーライドすることで、ResponseEntityExceptionHandlerで定義されている例外の共通処理を実装します。 以下の例ではレスポンスボディとして、作成したResponseErrorを必ず設定するようにしています。

@Override
protected ResponseEntity<Object> handleExceptionInternal(
        Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
  ResponseError re = new ResponseError(status.value(), ex.getMessage());
  return super.handleExceptionInternal(ex, re, headers, status, request);
}

例外毎に個別に設定したい場合は、各handleメソッドをオーバーライドします。 以下はMethodArgumentNotValidExceptionがスローされた場合の処理をオーバーライドしています。

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
  ResponseError re = new ResponseError(status.value(), ex.getMessage());
  return handleExceptionInternal(ex, re, headers, status, request);
}

例外の追加

ここまでの設定では、ResponseEntityExceptionHandlerに定義された例外にのみ適用されます。 つまり、最初に説明した自作クラスやResponseStatusExceptionは対象外となっています。

これらの例外でも同様の処理をしたい場合は、@ExceptionHandlerを付与したhandleメソッドを定義します。

例えば、ResponseStatusExceptionに対する処理は以下のように定義します。

@ExceptionHandler(ResponseStatusException.class)
protected ResponseEntity<Object> handleResponseStatus(ResponseStatusException ex, WebRequest request) {
  ResponseError re = new ResponseError(status.value(), ex.getMessage());
  return handleExceptionInternal(ex, null, new HttpHeaders(), ex.getStatus(), request);
}

@ExceptionHandlerの引数には、対象となる例外のクラスを指定します。

デフォルトの例外処理

@ExceptionHandlerを指定していない例外に対しての処理は、handleAll()をオーバーライドすることで定義します。 これを定義していない場合、レスポンスのデータにスタックトレースの内容が含まれてしまいます。

@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) {
  ResponseError re = new ResponseError(HttpStatus.INTERNAL_SERVER_ERROR, "予期せぬ例外が起こりました。");
  return new ResponseEntity<Object>(re, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}

これを定義してしまうと、ログにスタックトレースが出力されなくなるので、別途出力する処理を記述する必要があります。

StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
log.error(sw.toString());