web-dev-qa-db-ja.com

Reactorで例外をスローする正しい方法

私はプロジェクト Reactor および一般的なリアクティブプログラミングを始めたばかりです。

現在、次のようなコードに取り組んでいます:

Mono.just(userId)
    .map(repo::findById)
    .map(user-> {
        if(user == null){
            throw new UserNotFoundException();
        }
        return user;
    })
    // ... other mappings

この例はおそらく馬鹿げており、このケースを実装するより良い方法は確かにありますが、ポイントは次のとおりです。

mapブロックでthrow new例外を使用するのは間違っていますか、これをreturn Mono.error(new UserNotFoundException())に置き換える必要がありますか?

これらの2つの方法に実際の違いはありますか?

17
davioooh

例外をスローする便利な方法として考えられるいくつかの方法があります。

Flux/Mono.handleを使用して要素を処理します

エラーまたは空のストリームを引き起こす可能性のある要素の処理を簡素化できる方法の1つは、演算子handleです。

次のコードは、問題を解決するためにそれを使用する方法を示しています。

Mono.just(userId)
    .map(repo::findById)
    .handle((user, sink) -> {
        if(!isValid(user)){
            sink.error(new InvalidUserException());
        } else if (isSendable(user))
            sink.next(user);
        }
        else {
            //just ignore element
        }
    })

ご覧のとおり、.handle演算子は、要素を処理するためにBiConsumer<T, SynchronousSink<>を渡す必要があります。ここでは、BiConsumerに2つのパラメーターがあります。最初の要素は上流からの要素であり、2番目の要素はSynchronousSinkであり、要素を下流に同期的に供給するのに役立ちます。このような手法により、要素の処理のさまざまな結果を提供する機能が拡張されます。たとえば、要素が無効な場合、同じSycnchronousSyncにエラーを供給して、アップストリームをキャンセルし、ダウンストリームにonError信号を生成できます。次に、同じhandle演算子を使用して「フィルタリング」できます。ハンドルBiConsumerが実行され、要素が指定されていない場合、Reactorはそれを一種のフィルタリングと見なし、追加の要素を要求します。最後に、要素が有効な場合は、SynchronousSink#nextを呼び出して要素をダウンストリームに伝播するか、マッピングを適用するだけで、handlemap演算子として使用できます。さらに、パフォーマンスに影響を与えずにその演算子を安全に使用し、要素の検証やダウンストリームへのエラー送信などの複雑な要素検証を提供できます。

#concatMap + Mono.errorを使用してスローします

マッピング中に例外をスローするオプションの1つは、mapconcatMapに置き換えることです。本質的に、concatMapflatMapとほぼ同じです。唯一の違いは、concatMapは一度に1つのサブストリームのみを許可することです。このような動作により、内部実装が大幅に簡素化され、パフォーマンスに影響しません。したがって、より機能的な方法で例外をスローするには、次のコードを使用できます。

Mono.just(userId)
    .map(repo::findById)
    .concatMap(user-> {
        if(!isValid(user)){
            return Mono.error(new InvalidUserException());
        }
        return Mono.just(user);
    })

無効なユーザーの場合の上記のサンプルでは、​​Mono.errorを使用して例外を返します。 Flux.errorを使用して、fluxでも同じことができます。

Flux.just(userId1, userId2, userId3)
    .map(repo::findById)
    .concatMap(user-> {
        if(!isValid(user)){
            return Flux.error(new InvalidUserException());
        }
        return Mono.just(user);
    })

Note、どちらの場合も、要素が1つしかないcoldストリームを返します。 Reactorには、返されるストリームがコールドスカラーストリームである場合のパフォーマンスを改善する最適化がいくつかあります。したがって、Flux/Monoを使用することをお勧めしますconcatMap + .justemptyerrorは、より複雑なマッピングが必要な場合に、結果としてreturn nullまたはthrow new ...

注意!ヌル可能性について着信エレメントをチェックしないでください。 Reactor Projectはnull値を送信しません。これはReactive Streamsの仕様に違反しているためです( Rule 2.1。したがって、repo.findByIdはnullを返し、ReactorはNullPointerExceptionをスローします。

待って、なぜconcatMapflatMapよりも優れているのですか?

本質的に、flatMapは、一度に実行されている複数のサブストリームの要素をマージするように設計されています。つまり、flatMapはその下に非同期ストリームを持っている必要があるため、複数のスレッドでデータを処理したり、複数のネットワーク呼び出しを行ったりする可能性があります。その後、このような期待は実装に多大な影響を与えるため、flatMapは複数のストリーム(Threads)(同時データ構造の使用を意味します)からのデータを処理できる必要があります。 (サブストリームごとにQueuesの追加メモリ割り当てを意味します)、Reactive Streamsの仕様ルールに違反しません(実際に複雑な実装を意味します)。これらすべての事実と、単純なmap操作(同期)をFlux/Mono.error(実行の同期性を変更しない)を使用してより便利な例外をスローする方法に置き換えるという事実を数えると、このような複雑な演算子は必要なく、一度に1つのストリームを非同期処理するように設計され、スカラーのコールドストリームを処理するための最適化がいくつか行われている、はるかに単純なconcatMapを使用できます.

switchOnEmptyを使用して例外をスローします

したがって、結果が空のときに例外をスローする別のアプローチは、switchOnEmpty演算子です。次のコードは、そのアプローチの使用方法を示しています。

Mono.just(userId)
    .flatMap(repo::findById)
    .switchIfEmpty(Mono.error(new UserNotFoundExeception()))

ご覧のとおり、この場合、repo::findByIdには、戻り型としてMonoまたはUserが必要です。したがって、Userインスタンスが見つからない場合、結果ストリームは空になります。したがって、ReactorはMonoパラメーターとして指定された代替switchIfEmptyを呼び出します。

例外をそのまま投げる

読みにくいコードまたは悪い習慣としてカウントされる可能性がありますが、例外をそのままスローすることもできます。このパターンはリアクティブストリームの仕様に違反していますが、リアクタはスローされた例外をキャッチし、onError信号としてダウンストリームに伝播します

お持ち帰り

  1. 複雑な要素処理を提供するには、.handle演算子を使用します
  2. マッピング中に例外をスローする必要があるときにconcatMap + Mono.errorを使用しますが、このような手法は非同期要素処理の場合に最適です。
  3. 既にflatMapが配置されている場合は、flatMap + Mono.errorを使用します
  4. Nullは戻り型として禁止されているため、ダウンストリームでnullの代わりにmapを使用すると、onErrorで予期しないNullPointerExceptionを取得できます。
  5. 特定の関数を呼び出した結果がemptyストリームで終了した場合にエラー信号を送信する必要がある場合は、すべての場合にswitchIfEmptyを使用します
31
Oleh Dokuka