1. 概要

このチュートリアルでは、SpringRESTAPIのグローバルエラーハンドラーを実装する方法について説明します。

各例外のセマンティクスを使用して、クライアントにとって意味のあるエラーメッセージを作成し、問題を簡単に診断するためのすべての情報をクライアントに提供することを明確に目標としています。

2. カスタムエラーメッセージ

有線でエラーを送信するための単純な構造を実装することから始めましょう— ApiError

public class ApiError {

    private HttpStatus status;
    private String message;
    private List<String> errors;

    public ApiError(HttpStatus status, String message, List<String> errors) {
        super();
        this.status = status;
        this.message = message;
        this.errors = errors;
    }

    public ApiError(HttpStatus status, String message, String error) {
        super();
        this.status = status;
        this.message = message;
        errors = Arrays.asList(error);
    }
}

ここでの情報は単純である必要があります。

  • status –HTTPステータスコード
  • メッセージ–例外に関連するエラーメッセージ
  • error –作成されたエラーメッセージのリスト

そしてもちろん、Springの実際の例外処理ロジックには、 @ControllerAdviceアノテーションを使用します。

@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
    ...
}

3. 不正なリクエストの例外を処理する

3.1. 例外の処理

次に、最も一般的なクライアントエラーを処理する方法を見てみましょう。基本的に、クライアントが無効なリクエストをAPIに送信するシナリオです。

  • BindException –この例外は、致命的なバインディングエラーが発生した場合にスローされます。
  • MethodArgumentNotValidException @Valid で注釈が付けられた引数が検証に失敗した場合、この例外がスローされます。

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
  MethodArgumentNotValidException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    List<String> errors = new ArrayList<String>();
    for (FieldError error : ex.getBindingResult().getFieldErrors()) {
        errors.add(error.getField() + ": " + error.getDefaultMessage());
    }
    for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
        errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
    }
    
    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
    return handleExceptionInternal(
      ex, apiError, headers, apiError.getStatus(), request);
}

ResponseEntityExceptionHandlerから基本メソッドをオーバーライドし、独自のカスタム実装を提供していることに注意してください。

常にそうなるとは限りません。 場合によっては、後でここで説明するように、基本クラスにデフォルトの実装がないカスタム例外を処理する必要があります。

次:

  • MissingServletRequestPartException –この例外は、マルチパートリクエストの一部が見つからない場合にスローされます。

  • MissingServletRequestParameterException –この例外は、リクエストにパラメータがない場合にスローされます。

@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
  MissingServletRequestParameterException ex, HttpHeaders headers, 
  HttpStatus status, WebRequest request) {
    String error = ex.getParameterName() + " parameter is missing";
    
    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}
  • ConstraintViolationException –この例外は、制約違反の結果を報告します。

@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleConstraintViolation(
  ConstraintViolationException ex, WebRequest request) {
    List<String> errors = new ArrayList<String>();
    for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
        errors.add(violation.getRootBeanClass().getName() + " " + 
          violation.getPropertyPath() + ": " + violation.getMessage());
    }

    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}
  • TypeMismatchException –この例外は、間違ったタイプでBeanプロパティを設定しようとしたときにスローされます。

  • MethodArgumentTypeMismatchException –この例外は、メソッド引数が予期されたタイプではない場合にスローされます。

@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(
  MethodArgumentTypeMismatchException ex, WebRequest request) {
    String error = 
      ex.getName() + " should be of type " + ex.getRequiredType().getName();

    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}

3.2. クライアントからのAPIの消費

MethodArgumentTypeMismatchExceptionに遭遇するテストを見てみましょう。

longではなくStringとしてidを使用してリクエストを送信します。

@Test
public void whenMethodArgumentMismatch_thenBadRequest() {
    Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("should be of type"));
}

そして最後に、これと同じ要求を検討します。

Request method:	GET
Request path:	http://localhost:8080/spring-security-rest/api/foos/ccc

この種のJSONエラー応答は次のようになります

{
    "status": "BAD_REQUEST",
    "message": 
      "Failed to convert value of type [java.lang.String] 
       to required type [java.lang.Long]; nested exception 
       is java.lang.NumberFormatException: For input string: \"ccc\"",
    "errors": [
        "id should be of type java.lang.Long"
    ]
}

4. NoHandlerFoundExceptionを処理します

次に、サーブレットをカスタマイズして、404応答を送信する代わりにこの例外をスローすることができます。

<servlet>
    <servlet-name>api</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet</servlet-class>        
    <init-param>
        <param-name>throwExceptionIfNoHandlerFound</param-name>
        <param-value>true</param-value>
    </init-param>
</servlet>

次に、これが発生すると、他の例外と同じように簡単に処理できます。

@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(
  NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
    String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();

    ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
    return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}

簡単なテストは次のとおりです。

@Test
public void whenNoHandlerForHttpRequest_thenNotFound() {
    Response response = givenAuth().delete(URL_PREFIX + "/api/xx");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.NOT_FOUND, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("No handler found"));
}

完全なリクエストを見てみましょう:

Request method:	DELETE
Request path:	http://localhost:8080/spring-security-rest/api/xx

およびエラーJSON応答

{
    "status":"NOT_FOUND",
    "message":"No handler found for DELETE /spring-security-rest/api/xx",
    "errors":[
        "No handler found for DELETE /spring-security-rest/api/xx"
    ]
}

次に、別の興味深い例外を見ていきます。

5. HttpRequestMethodNotSupportedExceptionを処理します

HttpRequestMethodNotSupportedException は、サポートされていないHTTPメソッドでリクエストを送信したときに発生します。

@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
  HttpRequestMethodNotSupportedException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    StringBuilder builder = new StringBuilder();
    builder.append(ex.getMethod());
    builder.append(
      " method is not supported for this request. Supported methods are ");
    ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));

    ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED, 
      ex.getLocalizedMessage(), builder.toString());
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}

この例外を再現する簡単なテストを次に示します。

@Test
public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() {
    Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("Supported methods are"));
}

そして、ここに完全なリクエストがあります:

Request method:	DELETE
Request path:	http://localhost:8080/spring-security-rest/api/foos/1

およびエラーJSON応答

{
    "status":"METHOD_NOT_ALLOWED",
    "message":"Request method 'DELETE' not supported",
    "errors":[
        "DELETE method is not supported for this request. Supported methods are GET "
    ]
}

6. HttpMediaTypeNotSupportedExceptionを処理します

次に、 HttpMediaTypeNotSupportedException を処理しましょう。これは、クライアントがサポートされていないメディアタイプでリクエストを送信したときに発生します。

@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
  HttpMediaTypeNotSupportedException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    StringBuilder builder = new StringBuilder();
    builder.append(ex.getContentType());
    builder.append(" media type is not supported. Supported media types are ");
    ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));

    ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, 
      ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}

この問題が発生する簡単なテストは次のとおりです。

@Test
public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() {
    Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("media type is not supported"));
}

最後に、サンプルリクエストを次に示します。

Request method:	POST
Request path:	http://localhost:8080/spring-security-
Headers:	Content-Type=text/plain; charset=ISO-8859-1

およびエラーJSON応答:

{
    "status":"UNSUPPORTED_MEDIA_TYPE",
    "message":"Content type 'text/plain;charset=ISO-8859-1' not supported",
    "errors":["text/plain;charset=ISO-8859-1 media type is not supported. 
       Supported media types are text/xml 
       application/x-www-form-urlencoded 
       application/*+xml 
       application/json;charset=UTF-8 
       application/*+json;charset=UTF-8 */"
    ]
}

7. デフォルトのハンドラー

最後に、フォールバックハンドラーを実装します。これは、特定のハンドラーを持たない他のすべての例外を処理するキャッチオールタイプのロジックです。

@ExceptionHandler({ Exception.class })
public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) {
    ApiError apiError = new ApiError(
      HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
    return new ResponseEntity<Object>(
      apiError, new HttpHeaders(), apiError.getStatus());
}

8. 結論

Spring REST APIの適切で成熟したエラーハンドラーを構築することは困難であり、間違いなく反復的なプロセスです。 うまくいけば、このチュートリアルは、APIクライアントがエラーをすばやく簡単に診断し、それらを通過するのを支援するための良い出発点であり、良いアンカーになるでしょう。

このチュートリアルの完全な実装は、GitHubプロジェクトにあります。 これはEclipseベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。