例外ハンドリング

RESTful Webサービスでの例外ハンドリング方法とレスポンスの返却方法について説明します。

Spring Web MVC では例外が発生した場合、例外情報をログに出力し、クライアントにエラーレスポンスを返却します。

発生した例外が Spring Web MVCがデフォルトでハンドリングする例外 であれば、アプリケーション側で設定や実装を行わなくても、適切なレスポンスが返却されます。それ以外の例外であれば、デフォルトではHTTPステータスコードに500が設定されたレスポンスが返却されます。

レスポンスボディにはエラー情報が出力されます。出力する内容は server.error プロパティでカスマイズすることができます。プロパティの詳細については Server Properties を参照してください。

レスポンスの例
HTTP/1.1 500
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 28 May 2018 07:20:57 GMT
Connection: keep-alive

{
  "timestamp": "2018-05-28T07:20:57.132+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/users/1"
}

例外に応じてHTTPステータスコードやレスポンスボディをカスタマイズする場合は、例外ハンドリングを実装する必要があります。

以下のサンプルコードの動作確認環境については、 動作確認環境と依存ライブラリについて を参照してください。

アプリケーション全体の例外ハンドリングをカスタマイズする例

アプリケーション全体で例外に応じた処理が決まっている場合は、@RestControllerAdvice アノテーションを設定したクラスで例外ハンドリングを行います。

どの例外を処理するかは、メソッドに設定された@ExceptionHandlerアノテーションの情報により決まります。 返却するステータスコードは @ResponseStatus アノテーションに設定します。

Tip

例外クラスに設定したResponseStatusのreason属性を指定した場合、 レスポンスボディには、reason属性に指定した値がメッセージとして表示されます。 reason属性に指定した値は、MessageSourceを使用して解決されるため、 messages.propertiesに定義することも可能です。

ResponseEntityExceptionHandlerを継承して例外ハンドリングクラスを作成する

ResponseEntityExceptionHandler は、Spring Web MVC内で発生する例外をハンドリングするクラスです。 ResponseEntityExceptionHandlerを継承したクラスを作成すると、ハンドリングする例外に応じたステータスコードと空のレスポンスボディが返却されます。 ハンドリングする例外は、Spring Web MVCがデフォルトでハンドリングする例外 と同様です。

全ての例外を@ExceptionHandlerアノテーションで1つずつハンドリングしていくのは大変なので、基本的な例外ハンドリングはResponseEntityExceptionHandlerに委譲して、それ以外の例外を@ExceptionHandlerアノテーションでハンドリングする使用方法をお薦めします。

なお、ResponseEntityExceptionHandlerを継承した場合も、@RestControllerAdviceアノテーションを設定する必要があります。

実装例

  • 入力値チェックでエラーが発生した場合の処理をカスタマイズしています。

  • 排他制御エラーが発生した場合に送出される例外を、@ExceptionHandler を使用してハンドリングしています。

  • 独自に作成した例外(CustomValidationException)を、@ExceptionHandler を使用してハンドリングしています。

  • 独自に作成した例外(UserNotFoundException)に ResponseStatus を設定して、Spring Web MVC に例外ハンドリングを委譲します。

GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private final MessageSource messageSource;

    public GlobalExceptionHandler(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    /**
     * リクエストボディに設定された値に対する入力値チェック時に、エラーが発生した場合のハンドリングを実施します。
     * レスポンスボディには、{@link BindingResult}から取得したフィールド名と、メッセージを出力します。
     */
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        return handleExceptionInternal(
                ex,
                body(ex.getBindingResult()),
                headers,
                status,
                request);
    }

    /**
     * 入力形式に誤りがあった場合のハンドリングを実施します。
     * レスポンスボディには、固定のメッセージを出力します。
     */
    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(
            HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        return handleExceptionInternal(
                ex,
                body("keel.api-error-handling.HttpMessageNotReadableException"),
                headers,
                status,
                request);
    }

    //optimistic-lock-example-start

    /**
     * {@link ResponseEntityExceptionHandler}がハンドリングしない例外については、{@link ExceptionHandler}を使用してハンドリングします。
     * 楽観ロック例外が発生した場合は、HTTPステータスコードに409を設定します。
     */
    @ExceptionHandler(OptimisticLockingFailureException.class)
    @ResponseStatus(value = HttpStatus.CONFLICT)
    public void handleOptimisticLockingFailureException() {
    }
    //optimistic-lock-example-end

    // database-validation-start

    /**
     * {@link ResponseEntityExceptionHandler} がハンドリングしない例外については、 {@link ExceptionHandler} を使用してハンドリングします。
     * 独自に作成した {@link CustomValidationException} が発生した場合は、HTTPステータスコードに400を設定し、エラー内容を返却しています。
     */
    @ExceptionHandler(CustomValidationException.class)
    public ResponseEntity<Object> handleCustomValidationException(CustomValidationException ex, WebRequest request) {
        return super.handleExceptionInternal(
                ex,
                body(ex.getBindingResult()),
                new HttpHeaders(),
                HttpStatus.BAD_REQUEST,
                request);
    }

    private List<ApiErrorResponse> body(BindingResult bindingResult) {
        return bindingResult
                .getFieldErrors()
                .stream()
                .map(fieldError -> new ApiErrorResponse(
                        fieldError.getField(),
                        messageSource.getMessage(fieldError, LocaleContextHolder.getLocale())))
                .collect(Collectors.toList());
    }

    private ApiErrorResponse body(String code) {
        String message = messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
        return new ApiErrorResponse(null, message);
    }
    // database-validation-end
}
ApiErrorResponse
public class ApiErrorResponse {

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private final String field;

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private final String message;

    ApiErrorResponse(String field, String message) {
        this.field = field;
        this.message = message;
    }

    public String getField() {
        return field;
    }

    public String getMessage() {
        return message;
    }
}
UserNotFoundException
/**
 * {@link ResponseStatus} が付いている例外を送出することで、 {@link GlobalExceptionHandler} でハンドリングしていなくても、
 * {@link ResponseStatus} に指定したHTTPステータスでレスポンスが返却されます。
 * <p>
 * {@code reason} に指定した文字列は、 {@code messages.properties} でキーとして定義されている場合は対応するメッセージに変換されます。
 * そうでない場合はそのまま表示されます。
 */
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "keel.api-error-handling.user-not-found")
public class UserNotFoundException extends RuntimeException {
}

サンプル全体は api-error-handling-sample を参照してください。

個別機能(Controller)で例外をハンドリングする例

アプリケーション全体ではなく個別機能(Controller)で例外をハンドリングし、メッセージを返却したい場合があります。 Webの場合 と同様に、Controller内に例外ハンドリング用のメソッドを作成します。

    /**
     * {@link MethodArgumentNotValidException}を補足して、当画面固有のメッセージをリクエストボディに出力します。
     * ステータスコードは、400を返却します。
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> methodArgumentNotValidExceptionHandler() {
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(
                        messageSource.getMessage(
                                "keel.api-error-handling.MethodArgumentNotValidException",
                                new Object[0],
                                LocaleContextHolder.getLocale()
                        )
                );
    }

サンプル全体は api-error-handling-sample を参照してください。