Reactor Coreの概要
1. 前書き
Reactor Coreは、リアクティブプログラミングモデルを実装するJava8ライブラリです。 これは、リアクティブアプリケーションを構築するための標準であるReactive Streams Specificationの上に構築されています。
非リアクティブなJava開発の背景から、リアクティブに移行することは非常に急な学習曲線になる可能性があります。 これは、Java 8Stream APIと比較すると、同じ高レベルの抽象化であると誤解される可能性があるため、より困難になります。
この記事では、このパラダイムをわかりやすく説明します。 リアクティブコードを作成する方法の図を作成するまで、Reactorを少しずつ進め、後のシリーズで登場するより高度な記事の基礎を築きます。
2. リアクティブストリームの仕様
Reactorを見る前に、Reactive Streams Specificationを見る必要があります。 これはReactorが実装するものであり、ライブラリの基礎を築きます。
基本的に、ReactiveStreamsは非同期ストリーム処理の仕様です。
つまり、大量のイベントが非同期に生成および消費されるシステムです。 1秒間に数千件の株価更新が金融アプリケーションに流れ込み、それらの更新にタイムリーに応答する必要があると考えてください。
これの主な目標の1つは、背圧の問題に対処することです。 イベントを処理できるよりも速くイベントをコンシューマに送信するプロデューサがある場合、最終的にコンシューマはシステムリソースを使い果たしてイベントに圧倒されます。 バックプレッシャーとは、これを防ぐために消費者がどのくらいのデータを送信するかをプロデューサーに伝えることができることを意味します。これは仕様で定められているものです。
3. Mavenの依存関係
4. データストリームの生成
アプリケーションをリアクティブにするために、最初にできることは、データのストリームを生成することです。 これは、前に説明した株価更新の例のようなものです。 このデータがなければ、反応するものは何もありません。そのため、これは論理的な最初のステップです。 Reactive Coreは、これを可能にする2つのデータ型を提供します。
4.1. Flux
これを行う最初の方法は、Flux.を使用することです。これは、0..n要素を放出できるストリームです。 簡単なものを作成してみましょう。
Flux just = Flux.just("1", "2", "3");
この場合、3つの要素の静的ストリームがあります。
4.2. Mono
これを行う2番目の方法は、0..1要素のストリームであるMono,を使用することです。 1つをインスタンス化してみましょう:
Mono just = Mono.just("foo");
これは、Fluxとほぼ同じように見え、動作しますが、今回は1つの要素のみに制限されています。
4.3. なぜフラックスだけではないのですか?
さらに実験する前に、これら2つのデータ型がある理由を強調する価値があります。
まず、FluxとMonoの両方がReactive StreamsPublisherインターフェースの実装であることに注意してください。 両方のクラスは仕様に準拠しており、代わりにこのインターフェイスを使用できます。
Publisher just = Mono.just("foo");
しかし、実際には、このカーディナリティーを知ることは有用です。 これは、いくつかの操作が2つのタイプのいずれかに対してのみ意味があり、より表現力が高いためです(リポジトリ内のfindOne()を想像してください)。
5. ストリームの購読
これで、データストリームを生成する方法の高レベルの概要が得られました。要素を出力するには、データをサブスクライブする必要があります。
5.1. 要素の収集
subscribe()メソッドを使用して、ストリーム内のすべての要素を収集しましょう。
List elements = new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.subscribe(elements::add);
assertThat(elements).containsExactly(1, 2, 3, 4);
購読するまでデータは流れ始めません。 ロギングも追加したことに注意してください。これは、舞台裏で何が起こっているかを確認するときに役立ちます。
5.2. 要素の流れ
適切にログを記録したら、それを使用して、データがストリームをどのように流れているかを視覚化できます。
20:25:19.550 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
20:25:19.553 [main] INFO reactor.Flux.Array.1 - | request(unbounded)
20:25:19.553 [main] INFO reactor.Flux.Array.1 - | onNext(1)
20:25:19.553 [main] INFO reactor.Flux.Array.1 - | onNext(2)
20:25:19.553 [main] INFO reactor.Flux.Array.1 - | onNext(3)
20:25:19.553 [main] INFO reactor.Flux.Array.1 - | onNext(4)
20:25:19.553 [main] INFO reactor.Flux.Array.1 - | onComplete()
まず、すべてがメインスレッドで実行されています。 この記事の後半で並行性についてさらに検討するため、これについては詳しく説明しません。 しかし、すべてを順番に処理できるため、物事は単純になります。
次に、ログに記録したシーケンスを1つずつ見ていきましょう。
-
onSubscribe() –これはストリームをサブスクライブするときに呼び出されます
-
request(unbounded) –subscribeを呼び出すと、舞台裏でSubscription.が作成されます。このサブスクリプションはストリームから要素を要求します。 この場合、デフォルトでunbounded,になり、使用可能なすべての要素を要求します。
-
onNext() –これはすべての要素で呼び出されます
-
onComplete() –これは、最後の要素を受け取った後、lastと呼ばれます。 実際にはonError()もあり、例外がある場合に呼び出されますが、この場合はありません。
これは、Reactive Streams仕様の一部としてSubscriberインターフェースに配置されたフローであり、実際には、onSubscribe().の呼び出しの舞台裏でインスタンス化されたものです。これは便利な方法ですが、より良い方法です。何が起こっているのかを理解しましょう。Subscriberインターフェースを直接提供しましょう。
Flux.just(1, 2, 3, 4)
.log()
.subscribe(new Subscriber() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(Integer integer) {
elements.add(integer);
}
@Override
public void onError(Throwable t) {}
@Override
public void onComplete() {}
});
上記のフローの可能な各ステージは、Subscriber実装のメソッドにマップされていることがわかります。 たまたま、Fluxがこの冗長性を減らすためのヘルパーメソッドを提供してくれました。
5.3. Java 8Streamsとの比較
それでも、収集を実行しているJava 8Streamと同義の何かがあるように見えるかもしれません。
List collected = Stream.of(1, 2, 3, 4)
.collect(toList());
私たちだけがしません。
主な違いは、Reactiveがプッシュモデルであるのに対し、Java 8Streamsはプルモデルであるということです。 In reactive approach. events are pushed to the subscribers as they come in.
次に注意するのは、Streamsターミナル演算子は、ターミナルであり、すべてのデータをプルして結果を返すことです。 Reactiveを使用すると、外部リソースから無限のストリームを受信し、アドホックベースで複数のサブスクライバを接続および削除できます。 また、ストリームの結合、ストリームのスロットル、バックプレッシャーの適用などを行うこともできます。これについては次に説明します。
6. 背圧
次に考慮すべきことは、背圧です。 この例では、サブスクライバーはプロデューサーにすべての要素を一度にプッシュするように指示しています。 これは、加入者にとって圧倒的になり、そのリソースをすべて消費してしまう可能性があります。
Backpressure is when a downstream can tell an upstream to send it fewer data in order to prevent it from being overwhelmed。
Subscriberの実装を変更して、バックプレッシャを適用できます。 request()を使用して、一度に2つの要素のみを送信するようにアップストリームに指示しましょう。
Flux.just(1, 2, 3, 4)
.log()
.subscribe(new Subscriber() {
private Subscription s;
int onNextAmount;
@Override
public void onSubscribe(Subscription s) {
this.s = s;
s.request(2);
}
@Override
public void onNext(Integer integer) {
elements.add(integer);
onNextAmount++;
if (onNextAmount % 2 == 0) {
s.request(2);
}
}
@Override
public void onError(Throwable t) {}
@Override
public void onComplete() {}
});
コードを再度実行すると、request(2)が呼び出され、続いて2つのonNext()呼び出しが行われ、次にrequest(2)が再度呼び出されます。
23:31:15.395 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
23:31:15.397 [main] INFO reactor.Flux.Array.1 - | request(2)
23:31:15.397 [main] INFO reactor.Flux.Array.1 - | onNext(1)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onNext(2)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | request(2)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onNext(3)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onNext(4)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | request(2)
23:31:15.398 [main] INFO reactor.Flux.Array.1 - | onComplete()
基本的に、これはリアクティブプルバックプレッシャーです。 準備が整ったときにのみ、特定の量の要素のみをプッシュするようにアップストリームに要求しています。 私たちがツイッターからツイートをストリーミングしていると想像した場合、何をすべきかを決めるのはアップストリーム次第です。 ツイートが送られてきたが、ダウンストリームからのリクエストがない場合、アップストリームはアイテムをドロップしたり、バッファに保存したり、その他の戦略を立てたりできます。
7. ストリームでの操作
ストリーム内のデータに対して操作を実行し、適切と思われるイベントに応答することもできます。
7.1. ストリーム内のデータのマッピング
実行できる簡単な操作は、変換を適用することです。 この場合、ストリーム内のすべての数値を2倍にします。
Flux.just(1, 2, 3, 4)
.log()
.map(i -> i * 2)
.subscribe(elements::add);
onNext()が呼び出されると、map()が適用されます。
7.2. 2つのストリームを組み合わせる
その後、別のストリームをこのストリームと組み合わせることで、さらに面白くすることができます。 zip()関数:を使用してこれを試してみましょう
Flux.just(1, 2, 3, 4)
.log()
.map(i -> i * 2)
.zipWith(Flux.range(0, Integer.MAX_VALUE),
(one, two) -> String.format("First Flux: %d, Second Flux: %d", one, two))
.subscribe(elements::add);
assertThat(elements).containsExactly(
"First Flux: 2, Second Flux: 0",
"First Flux: 4, Second Flux: 1",
"First Flux: 6, Second Flux: 2",
"First Flux: 8, Second Flux: 3");
ここでは、1ずつ増加し続ける別のFluxを作成し、元のFluxと一緒にストリーミングしています。 ログを調べると、これらがどのように連携するかを確認できます。
20:04:38.064 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
20:04:38.065 [main] INFO reactor.Flux.Array.1 - | onNext(1)
20:04:38.066 [main] INFO reactor.Flux.Range.2 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription)
20:04:38.066 [main] INFO reactor.Flux.Range.2 - | onNext(0)
20:04:38.067 [main] INFO reactor.Flux.Array.1 - | onNext(2)
20:04:38.067 [main] INFO reactor.Flux.Range.2 - | onNext(1)
20:04:38.067 [main] INFO reactor.Flux.Array.1 - | onNext(3)
20:04:38.067 [main] INFO reactor.Flux.Range.2 - | onNext(2)
20:04:38.067 [main] INFO reactor.Flux.Array.1 - | onNext(4)
20:04:38.067 [main] INFO reactor.Flux.Range.2 - | onNext(3)
20:04:38.067 [main] INFO reactor.Flux.Array.1 - | onComplete()
20:04:38.067 [main] INFO reactor.Flux.Array.1 - | cancel()
20:04:38.067 [main] INFO reactor.Flux.Range.2 - | cancel()
Fluxごとに1つのサブスクリプションがあることに注意してください。 onNext()呼び出しも交互に行われるため、zip()関数を適用すると、ストリーム内の各要素のインデックスが一致します。
8. ホットストリーム
現在、私たちは主にコールドストリームに焦点を当てています。 これらは静的で固定長のストリームであり、簡単に処理できます。 リアクティブのより現実的なユースケースは、無限に発生するものです。
たとえば、常に反応する必要のあるマウスの動きのストリームやtwitterフィードを作成できます。 これらのタイプのストリームは、常に実行されており、いつでもサブスクライブでき、データの開始を逃すため、ホットストリームと呼ばれます。
8.1. ConnectableFluxの作成
ホットストリームを作成する1つの方法は、コールドストリームを1つに変換することです。 永久に続くFluxを作成して、結果をコンソールに出力します。これにより、外部リソースからのデータの無限ストリームがシミュレートされます。
ConnectableFlux
publish()を呼び出すと、ConnectableFlux.が与えられます。これは、subscribe()を呼び出しても放出が開始されないため、複数のサブスクリプションを追加できることを意味します。
publish.subscribe(System.out::println);
publish.subscribe(System.out::println);
このコードを実行しようとしても、何も起こりません。 Fluxが放出を開始するのは、connect(),を呼び出すまではありません。 購読しているかどうかは関係ありません。
8.2. 調整
コードを実行すると、コンソールはログ記録に圧倒されます。 これは、消費者に渡されるデータが多すぎる状況をシミュレートしています。 スロットルでこれを回避してみましょう:
ConnectableFlux
ここでは、2秒間隔のsample()メソッドを紹介しました。 これで、値は2秒ごとにサブスクライバーにプッシュされるだけになり、コンソールのhe騒が少なくなります。
もちろん、ウィンドウ処理やバッファリングなど、ダウンストリームに送信されるデータの量を減らすための複数の戦略がありますが、これらはこの記事の範囲外になります。
9. 並行性
上記の例はすべて、現在メインスレッドで実行されています。 ただし、必要に応じて、コードを実行するスレッドを制御できます。 Schedulerインターフェースは、非同期コードを抽象化したものであり、多くの実装が提供されています。 mainとは別のスレッドにサブスクライブしてみましょう。
Flux.just(1, 2, 3, 4)
.log()
.map(i -> i * 2)
.subscribeOn(Schedulers.parallel())
.subscribe(elements::add);
Parallelスケジューラーにより、サブスクリプションが別のスレッドで実行されます。これは、ログを確認することで証明できます。
20:03:27.505 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
20:03:27.529 [parallel-1] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
20:03:27.531 [parallel-1] INFO reactor.Flux.Array.1 - | request(unbounded)
20:03:27.531 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(1)
20:03:27.531 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(2)
20:03:27.531 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(3)
20:03:27.531 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(4)
20:03:27.531 [parallel-1] INFO reactor.Flux.Array.1 - | onComplete()
並行性はこれよりも興味深いものになります。別の記事でそれを調べる価値があります。
10. 結論
この記事では、ReactiveCoreの概要をエンドツーエンドで説明しました。 ストリームのパブリッシュとサブスクライブ、バックプレッシャの適用、ストリームの操作、データの非同期処理の方法について説明しました。 これにより、リアクティブアプリケーションを作成するための基盤ができればいいのですが。
このシリーズの後半の記事では、より高度な並行性およびその他の事後的な概念について説明します。 Reactor with Springをカバーする別の記事もあります。
このアプリケーションのソースコードはover on GitHubで入手できます。これは、そのまま実行できるはずのMavenプロジェクトです。