Spring BootRESTAPIエラー処理ガイド

公開: 2022-03-11

意味のあるエラーメッセージを提供しながらAPIでエラーを正しく処理することは、APIクライアントが問題に適切に対応するのに役立つため、非常に望ましい機能です。 デフォルトの動作では、理解が難しく、最終的にAPIクライアントには役に立たないスタックトレースが返される傾向があります。 エラー情報をフィールドに分割することで、APIクライアントはそれを解析し、ユーザーにより良いエラーメッセージを提供することもできます。 この記事では、SpringBootを使用してRESTAPIを構築するときに適切なエラー処理を行う方法について説明します。

不可解で長いエラーメッセージについて混乱している人

Springを使用してRESTAPIを構築することは、ここ数年の間にJava開発者にとって標準的なアプローチになりました。 Spring Bootを使用すると、多くの定型コードが削除され、さまざまなコンポーネントの自動構成が可能になるため、大幅に役立ちます。 ここで説明する知識を適用する前に、これらのテクノロジーを使用したAPI開発の基本に精通していることを前提としています。 基本的なRESTAPIの開発方法がまだわからない場合は、SpringMVCに関するこの記事またはSpringRESTサービスの構築に関する別の記事から始める必要があります。

エラー応答を明確にする

この記事全体を通して、鳥を表すオブジェクトを取得するためのRESTAPIを実装するアプリケーションのGitHubでホストされているソースコードを使用します。 この記事で説明されている機能と、エラー処理シナリオの例がいくつかあります。 そのアプリケーションに実装されているエンドポイントの概要は次のとおりです。

</ tr>
GET /birds/{birdId} 鳥に関する情報を取得し、見つからない場合は例外をスローします。
GET /birds/noexception/{birdId} この呼び出しは、鳥が見つからない場合に例外をスローしないことを除いて、鳥に関する情報も取得します。
POST /birds 鳥を作成します。

Spring Framework MVCモジュールには、エラー処理に役立ついくつかの優れた機能が付属しています。 ただし、これらの機能を使用して例外を処理し、APIクライアントに意味のある応答を返すのは開発者の責任です。

次のJSONオブジェクトを使用して/birdsエンドポイントにHTTPPOSTを発行した場合のデフォルトのSpringBoot回答の例を見てみましょう。このオブジェクトは、フィールド「mass」に文字列「aaa」があり、整数が必要です。

 { "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }

適切なエラー処理なしのSpringBootのデフォルトの回答:

 { "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }

ええと…応答メッセージにはいくつかの良いフィールドがありますが、それは例外が何であったかに焦点を合わせすぎています。 ちなみに、これはSpringBootのクラスDefaultErrorAttributesです。 timestampフィールドは、タイムスタンプがどの測定単位に含まれるかについての情報すら含まない整数です。 exceptionフィールドはJava開発者のみが関心を持ち、メッセージにより、APIコンシューマーは、関連のないすべての実装の詳細で失われます。 。 そして、エラーの原因となった例外から抽出できる詳細がもっとあったらどうなるでしょうか。 それでは、これらの例外を適切に処理し、より優れたJSON表現にラップして、APIクライアントの作業を楽にする方法を学びましょう。

Java 8の日付と時刻のクラスを使用するため、最初にJacksonJSR310コンバーターのMaven依存関係を追加する必要があります。 @JsonFormatアノテーションを使用して、Java8の日付と時刻のクラスをJSON表現に変換します。

 <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>

では、APIエラーを表すクラスを定義しましょう。 REST呼び出し中に発生するエラーに関する関連情報を保持するのに十分なフィールドを持つApiErrorというクラスを作成します。

 class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
  • statusプロパティは、操作呼び出しのステータスを保持します。 クライアントエラーを通知する4xxから、サーバーエラーを意味する5xxまでのいずれかになります。 一般的なシナリオは、たとえばクライアントが無効な電子メールアドレスなどの不適切な形式のフィールドを送信した場合に、BAD_REQUESTを意味するhttpコード400です。

  • timestampプロパティは、エラーが発生した日時インスタンスを保持します。

  • messageプロパティは、エラーに関するユーザーフレンドリーなメッセージを保持します。

  • debugMessageプロパティは、エラーをより詳細に説明するシステムメッセージを保持します。

  • subErrorsプロパティは、発生したサブエラーの配列を保持します。 これは、1回の呼び出しで複数のエラーを表すために使用されます。 例として、複数のフィールドが検証に失敗した検証エラーがあります。 ApiSubErrorクラスは、それらをカプセル化するために使用されます。

 abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }

したがって、 ApiValidationErrorは、 ApiSubErrorを拡張し、REST呼び出し中に発生した検証の問題を表現するクラスです。

以下に、この記事の終わりまでに何ができるかを理解するために、ここで説明する改善を実装した後に生成されるJSON応答の例をいくつか示します。

エンドポイントGET /birds/2呼び出し中にエンティティが見つからない場合に返されるJSONの例を次に示します。

 { "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }

鳥の質量に対して無効な値を指定してPOST /birds呼び出しを発行したときに返されるJSONの別の例を次に示します。

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }

SpringBootエラー処理

例外を処理するために使用されるSpringアノテーションのいくつかを調べてみましょう。

RestControllerは、REST操作を処理するクラスの基本アノテーションです。

ExceptionHandlerは、ハンドラーの実行(コントローラー操作)中にスローされた例外を処理するメカニズムを提供するSpringアノテーションです。 このアノテーションは、コントローラークラスのメソッドで使用される場合、このコントローラー内でのみスローされた例外を処理するためのエントリポイントとして機能します。 全体として、最も一般的な方法は、 @ControllerAdvice @ExceptionHandler使用して、例外処理がグローバルに、またはコントローラーのサブセットに適用されるようにすることです。

ControllerAdviceは、Spring 3.2で導入されたアノテーションであり、その名前が示すように、複数のコントローラーの「アドバイス」です。 これは、単一のExceptionHandlerを複数のコントローラーに適用できるようにするために使用されます。 このようにして、このような例外の処理方法を1か所で定義できます。このハンドラーは、このControllerAdviceでカバーされるクラスから例外がスローされたときに呼び出されます。 影響を受けるコントローラーのサブセットは、@ ControllerAdviceで次のセレクターを使用して定義できます: annotations()@ControllerAdvice basePackageClasses() 、およびbasePackages() 。 セレクターが提供されていない場合、 ControllerAdviceはすべてのコントローラーにグローバルに適用されます。

したがって、 @ControllerAdvice @ExceptionHandler使用することで、例外を処理し、デフォルトのSpringBootエラー処理メカニズムよりも適切に編成されたApiErrorオブジェクトにそれらをラップするための中心点を定義できます。

例外の処理

成功したRESTクライアント呼び出しと失敗したRESTクライアント呼び出しで何が起こるかの表現

次のステップは、例外を処理するクラスを作成することです。 簡単にするために、これをRestExceptionHandlerと呼び、SpringBootのResponseEntityExceptionHandlerから拡張する必要があります。 すでにSpringMVC例外の基本的な処理を提供しているため、 ResponseEntityExceptionHandlerを拡張します。そのため、既存の例外を改善しながら、新しい例外のハンドラーを追加します。

ResponseEntityExceptionHandlerで処理される例外のオーバーライド

ResponseEntityExceptionHandlerのソースコードを見ると、 handleHttpMessageNotReadable()handleHttpMessageNotWritable() )のようなhandle******()と呼ばれる多くのメソッドがあります。 まず、 handleHttpMessageNotReadable()を拡張してHttpMessageNotReadableException例外を処理する方法を見てみましょう。 RestExceptionHandlerクラスのメソッドhandleHttpMessageNotReadable()をオーバーライドする必要があります。

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }

HttpMessageNotReadableExceptionがスローされた場合、エラーメッセージは「MalformedJSON request」になり、エラーはApiErrorオブジェクト内にカプセル化されることを宣言しました。 以下に、この新しいメソッドをオーバーライドしたREST呼び出しの答えを示します。

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }

カスタム例外の処理

次に、SpringBootのResponseEntityExceptionHandler内でまだ宣言されていない例外を処理するメソッドを作成する方法を説明します。

データベース呼び出しを処理するSpringアプリケーションの一般的なシナリオは、リポジトリクラスを使用してIDでレコードを検索するための呼び出しを行うことです。 ただし、 CrudRepository.findOne()メソッドを調べると、オブジェクトが見つからない場合はnullが返されることがわかります。 つまり、サービスがこのメソッドを呼び出してコントローラーに直接戻ると、リソースが見つからなくてもHTTPコード200(OK)を取得します。 実際、適切なアプローチは、HTTP / 1.1仕様で指定されているHTTPコード404(NOT FOUND)を返すことです。

このケースを処理するために、 EntityNotFoundExceptionと呼ばれるカスタム例外を作成します。 これはカスタム作成された例外であり、 javax.persistence.EntityNotFoundExceptionとは異なります。これは、オブジェクトの作成を容易にするコンストラクターを提供し、 javax.persistence例外を別の方法で処理することを選択できるためです。

失敗したREST呼び出しの例

そうは言っても、 RestExceptionHandlerクラスにこの新しく作成されたEntityNotFoundExceptionExceptionHandlerを作成しましょう。 これを行うには、 handleEntityNotFound()というメソッドを作成し、@ ExceptionHandlerでアノテーションを付けて、クラスオブジェクト@ExceptionHandlerEntityNotFoundException.classます。 これは、 EntityNotFoundExceptionがスローされるたびに、Springがこのメソッドを呼び出して処理する必要があることをSpringに通知します。 @ExceptionHandlerを使用してメソッドにアノテーションを付けると、ここで説明するように、 WebRequestLocaleなどのさまざまな自動挿入パラメーターを受け入れます。 このhandleEntityNotFoundメソッドのパラメーターとして、例外EntityNotFoundException自体を提供するだけです。

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }

すごい! handleEntityNotFound()メソッドでは、HTTPステータスコードをNOT_FOUNDに設定し、新しい例外メッセージを使用しています。 GET /birds/2エンドポイントの応答は次のようになります。

 { "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }

結論

これらの例外をApiErrorオブジェクトに適切にマッピングし、APIクライアントが何が起こったかを知ることができるようにする重要な情報を提供できるように、例外処理を制御することが重要です。 ここからの次のステップは、アプリケーションコード内でスローされる例外に対して、より多くのハンドラーメソッド(@ExceptionHandlerを持つメソッド)を作成することです。 GitHubコードには、 MethodArgumentTypeMismatchExceptionConstraintViolationExceptionなどの他の一般的な例外の例が他にもあります。

この記事の構成に役立った追加のリソースは次のとおりです。

  • Baeldung-Springを使用したRESTのエラー処理

  • Springブログ-SpringMVCでの例外処理