1. 概要

このチュートリアルでは、実際の例を見ていきながら、SpringWebFluxプロジェクトでエラーを処理するために利用できるさまざまな戦略を見ていきます。

また、ある戦略を別の戦略よりも使用することが有利である可能性がある場所を指摘し、最後に完全なソースコードへのリンクを提供します。

2. 例の設定

Mavenのセットアップは、SpringWebFluxの概要を説明する以前の記事と同じです。

この例では、 クエリパラメーターとしてユーザー名を受け取り、結果として「Hellousername」を返すRESTfulエンドポイントを使用します。

まず、渡されたハンドラーのhandleRequestという名前のメソッドに/helloリクエストをルーティングするルーター関数を作成しましょう。

@Bean
public RouterFunction<ServerResponse> routeRequest(Handler handler) {
    return RouterFunctions.route(RequestPredicates.GET("/hello")
      .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), 
        handler::handleRequest);
    }

次に、 sayHello()メソッドを呼び出し、その結果を ServerResponse 本体に含める/返す方法を見つける、 handleRequest()メソッドを定義します。 :

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    return 
      //...
        sayHello(request)
      //...
}

最後に、 sayHello()メソッドは、「Hello」Stringとユーザー名を連結する単純なユーティリティメソッドです。

private Mono<String> sayHello(ServerRequest request) {
    //...
    return Mono.just("Hello, " + request.queryParam("name").get());
    //...
}

ユーザー名がリクエストの一部として存在する限り、たとえば、エンドポイントが「 / hello?username = Tonni 」と呼ばれる場合、このエンドポイントは常に正しく機能します。

ただし、「/ hello」などのユーザー名を指定せずに同じエンドポイントを呼び出すと、例外がスローされます。

以下では、WebFluxでこの例外を処理するために、コードをどこでどのように再編成できるかを見ていきます。

3. 機能レベルでのエラーの処理

MonoおよびFlux APIには、機能レベルでエラーを処理するための2つの主要な演算子が組み込まれています。

それらとその使用法を簡単に調べてみましょう。

3.1. onErrorReturnを使用したエラーの処理

onErrorReturn()を使用して、エラーが発生するたびに静的なデフォルト値を返すことができます。

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    return sayHello(request)
      .onErrorReturn("Hello Stranger")
      .flatMap(s -> ServerResponse.ok()
        .contentType(MediaType.TEXT_PLAIN)
        .bodyValue(s));
}

ここでは、バグのある連結関数 sayHello()が例外をスローするたびに、静的な「HelloStranger」を返します。

3.2. onErrorResumeを使用したエラーの処理

onErrorResumeを使用してエラーを処理する方法は3つあります。

  • 動的フォールバック値を計算する
  • フォールバック方式で代替パスを実行する
  • たとえば、カスタムビジネスの例外として、エラーをキャッチ、ラップ、および再スローします

値を計算する方法を見てみましょう。

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    return sayHello(request)
      .flatMap(s -> ServerResponse.ok()
        .contentType(MediaType.TEXT_PLAIN)
        .bodyValue(s))
      .onErrorResume(e -> Mono.just("Error " + e.getMessage())
        .flatMap(s -> ServerResponse.ok()
          .contentType(MediaType.TEXT_PLAIN)
          .bodyValue(s)));
}

ここでは、 sayHello()が例外をスローするたびに、動的に取得されたエラーメッセージが文字列「Error」に追加された文字列を返します。

次に、エラーが発生したときにフォールバックメソッドを呼び出します

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    return sayHello(request)
      .flatMap(s -> ServerResponse.ok()
        .contentType(MediaType.TEXT_PLAIN)
        .bodyValue(s))
      .onErrorResume(e -> sayHelloFallback()
        .flatMap(s -> ServerResponse.ok()
        .contentType(MediaType.TEXT_PLAIN)
        .bodyValue(s)));
}

ここでは、 sayHello()が例外をスローするたびに、代替メソッド sayHelloFallback()を呼び出しています。

onErrorResume()を使用する最後のオプションは、エラーをキャッチ、ラップ、および再スローすることです。たとえば、NameRequiredExceptionとして次のようになります。

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    return ServerResponse.ok()
      .body(sayHello(request)
      .onErrorResume(e -> Mono.error(new NameRequiredException(
        HttpStatus.BAD_REQUEST, 
        "username is required", e))), String.class);
}

ここでは、 sayHello()が例外をスローするたびに、「ユーザー名が必要です」というメッセージを含むカスタム例外をスローしています。

4. グローバルレベルでのエラーの処理

これまでに示したすべての例は、機能レベルでのエラー処理に取り組んできました。

ただし、WebFluxエラーをグローバルレベルで処理することを選択できます。これを行うには、次の2つの手順を実行するだけです。

  • グローバルエラー応答属性をカスタマイズする
  • グローバルエラーハンドラを実装する

ハンドラーがスローする例外は、HTTPステータスとJSONエラー本文に自動的に変換されます。

これらをカスタマイズするには、DefaultErrorAttributesクラス拡張し、その getErrorAttributes()メソッドをオーバーライドするだけです。

public class GlobalErrorAttributes extends DefaultErrorAttributes{
    
    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, 
      ErrorAttributeOptions options) {
        Map<String, Object> map = super.getErrorAttributes(
          request, options);
        map.put("status", HttpStatus.BAD_REQUEST);
        map.put("message", "username is required");
        return map;
    }

}

ここでは、ステータス BAD_REQUEST と、例外が発生したときにエラー属性の一部として返されるメッセージ「usernameisrequired」が必要です。

次に、グローバルエラーハンドラを実装しましょう。

このために、Springは、グローバルエラーの処理を拡張および実装するための便利なAbstractErrorWebExceptionHandlerクラスを提供します。

@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends 
    AbstractErrorWebExceptionHandler {

    // constructors

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(
      ErrorAttributes errorAttributes) {

        return RouterFunctions.route(
          RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(
       ServerRequest request) {

       Map<String, Object> errorPropertiesMap = getErrorAttributes(request, 
         ErrorAttributeOptions.defaults());

       return ServerResponse.status(HttpStatus.BAD_REQUEST)
         .contentType(MediaType.APPLICATION_JSON)
         .body(BodyInserters.fromValue(errorPropertiesMap));
    }
}

この例では、グローバルエラーハンドラの順序を-2に設定します。 これは、 @Order(-1)に登録されているDefaultErrorWebExceptionHandlerよりも高い優先度をに与えるためです。

errorAttributes オブジェクトは、Web例外ハンドラーのコンストラクターで渡すオブジェクトの正確なコピーになります。 これは、理想的には、カスタマイズされたエラー属性クラスである必要があります。

次に、すべてのエラー処理要求を renderErrorResponse()メソッドにルーティングすることを明確に示しています。

最後に、エラー属性を取得して、サーバーの応答本文内に挿入します。

これにより、エラーの詳細、HTTPステータス、およびマシンクライアントの例外メッセージを含むJSON応答が生成されます。 ブラウザクライアントの場合、同じデータをHTML形式でレンダリングする「ホワイトラベル」エラーハンドラがあります。 もちろん、これはカスタマイズできます。

5. 結論

この記事では、Spring WebFluxプロジェクトでエラーを処理するために利用できるさまざまな戦略を検討し、ある戦略を別の戦略よりも使用することが有利な場合があることを指摘しました。

約束どおり、記事に付属する完全なソースコードは、GitHubから入手できます。