【Spring BootでREST API】#3 エラー処理

Web

今回の目標

前回はAPIの作成について基本的なことを説明しました。

【Spring BootでREST API】#2 API作成の基本
今回はREST APIの基本的な作成方法を説明していきます。ますは認証については考えず、シンプルなGET、POST、PUT、DELETEに対応するAPIを作成していきます。

今回は前回作成したAPIにエラー処理を実装していきます。前回の内容をそのまま使用しますので、まだ見ていない方は一度見ていただきたいと思います。

実装するエラー処理について

今回はAPIによって次のようなエラーを設定します。(PUT、DELETEは割愛します)

パスHttpメソッドHttpステータス詳細
/api/product/{id}GET404: Not Found該当商品がない
/api/productPOST400: Bad Requestバリデーションエラー
409: Conflictキー違反

エラー処理

エラー処理の方法にはいくつか方法があるみたいです。ここでは2種類の方法を紹介します。

独自のExceptionクラスを作成

該当するHttpステータスごとに独自のExceptionクラスを作成します。ポイントは、@ResponseStatusを付与することです。これにより、この例外が発生した場合は設定したHttpステータスが送信されるようになります。

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

では1件取得用のAPIにエラー処理を追加し、存在しないidにアクセスしてみます。

@GetMapping("{id}")
public findById(@PathVariable("id") String id) {
  if (!products.containsKey(id)) {
    throw new NotFoundException(String.format("Not Found[id: %s]", id));
  }
  return products.get(id);
}

結果を見ると、Httpステータスは404で、レスポンスボディにエラーの詳細が設定されていることが分かります。

ResponseStatusException

ResponseStatusExceptionは、独自にExecptionクラスを作成せずにエラー処理を行うことができます。Exception作成時にHttpStatusを指定することで、どのHttpステータスを送信するかを決定します。

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public create(@Validated Product product, BindingResut result) {
  if (result.hasErrors()) {
    throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
  }
  if (products.containsKey(product.getId())) {
    throw new ResponseStatusException(HttpStaus.CONFLICT, String.format("Conflict[id: %s]", product.getId()))
  }
  this.products.put(product.getId(), product);
}

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

例えばレスポンスボディの内容が気に入らないので変えたい!という場合はどのようにすればよいのでしょうか。

答えは、ResponseEntityExceptionHandlerを継承したクラスをControllerAdviceとして作成します。ControllerAdviceはすべてのControllerで共通する処理を定義するものです。REST APIの場合は、@RestControllerAdviceを付与します。

@RestControllerAdvice
public class ControllerAdvice extends ResponseEntityExceptionHandler {
  //…
}

ResponseEntityExceptionHandler

少しだ継承するResponseEntityExceptionHandlerのソースコードを見てみます。

public abstract class ResponseEntityExceptionHandler {
  //…
  @ExceptionHandler({
      HttpRequestMethodNotSupportedException.class,
      HttpMediaTypeNotSupportedException.class,
      HttpMediaTypeNotAcceptableException.class,
      MissingPathVariableException.class,
      MissingServletRequestParameterException.class,
      ServletRequestBindingException.class,
      ConversionNotSupportedException.class,
      TypeMismatchException.class,
      HttpMessageNotReadableException.class,
      HttpMessageNotWritableException.class,
      MethodArgumentNotValidException.class,
      MissingServletRequestPartException.class,
      BindException.class,
      NoHandlerFoundException.class,
      AsyncRequestTimeoutException.class
    })
  @Nullable
  public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
    HttpHeaders headers = new HttpHeaders();

    if (ex instanceof HttpRequestMethodNotSupportedException) {
      HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
      return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
    }
    else if (ex instanceof HttpMediaTypeNotSupportedException) {
      HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
      return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
    }
    //…
  }
  //…
  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);
  }
}

handleExceptionメソッドに@ExceptionHandlerが付与されているため、ここに設定されているExceptionはすべてここで処理されます。例えば定義していないHttpメソッドでアクセスした場合(405)などの処理が該当します。

メソッドの処理としては、Exceptionごとにどのようなレスポンスを返すかをResponseEntity<>に設定しています。実際にExceptionによってどのHttpステータスを送信するかは、上記のソースコードだけでもわかると思います。

また省略していますが、各Exceptionの処理は必ずhandleExceptionInternalメソッドの結果(ResponseEntity<>)を返すようになっています。つまりレスポンスについて共通する部分を変更したい場合は、このメソッドをオーバーライドします。

そして今回変更したいレスポンスボディは、handleExceptionInternalメソッドの引数であるbodyに該当します。が、handleExceptionメソッドの各Eaxceptionの処理からは、すべてnullとして呼び出されています。つまり、レスポンスボディは設定されていません。

あれ?でも今までの結果にはレスポンスボディが設定されていましたよね?

正直これについてはよくわかりませんでした…。おそらくデフォルトで処理されているところ(クラス)があると思うのですが…。

わかることは、ResponseEntityExceptionHandlerを継承したControllerAdviceを作ることで、これらの結果を上書きできるということです。

エラーオブジェクトの作成

レスポンスボディに設定するためのクラスを作成します。とりあえず必要な情報を適当に定義しておきます。

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

  public Error(int status, String message) {
    this.status = status;
    this.message = message;
  }
  //Setter・Getter
}

レスポンスボディの設定

まずはhandleExceptionメソッドで処理されるExceptionについて、作成したエラーオブジェクトの内容がレスポンスボディに設定されるようにします。先述したように、レスポンスについてはhandleExceptionInternalメソッドをオーバーライドします。

実装としては、bodyがnullの場合は作成したエラーオブジェクトを、そうでない場合は指定されたオブジェクトをレスポンスボディに設定するようにします。

@RestControllerAdvice
public class ControllerAdvice extends ResponseEntityExceptionHandler {
  @Override
  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);
    }
    Object obj = body == null ? new Error(status.value(), ex.getMessage()) : body;
    return new ResponseEntity<>(obj, headers, status);
  }
}

確認のため「/api/product」にDELETEメソッドでアクセスしてみます。

その他の例外処理

ここまでの設定で処理されるのは、あくまでhandleExceptionメソッドで設定されたExceptionだけです。それ以外のExceptionについては別途実装する必要があります。

基本的にはhandleExceptionを参考に実装すればよいと思います。例としてResponseStatusExceptionについてのみ実装しておきます。

@ExceptionHandler(ResponseStatusException.class)
protected ResponseEntity<Object> handleResponseStatusException(ResponseStatusException ex, WebRequest request) {
  return handleExceptionInternal(ex, null, new HttpHeaders(), ex.getStatus(), request);
}

あとがき

今回はエラー処理について説明していきました。正直どれが正解?かはわからないのですが、適切なエラーステータスを返すことは重要なことなので、是非覚えてほしいところです。

 

- Spring Bootのおすすめ書籍はコチラ -

コメント

タイトルとURLをコピーしました