Java 9リアクティブストリーム
1. 概要
この記事では、Java 9 ReactiveStreamsについて説明します。 簡単に言えば、Flowクラスを使用できるようになります。このクラスは、リアクティブストリーム処理ロジックを構築するための主要なビルディングブロックを囲みます。
Reactive Streamsは、非ブロッキングバックプレッシャを使用した非同期ストリーム処理の標準です。 この仕様はReactive Manifesto,で定義されており、RxJavaやAkka-Streams.などのさまざまな実装があります。
2. リアクティブAPIの概要
Flowを構築するには、3つの主要な抽象化を使用して、それらを非同期処理ロジックに構成します。
Every Flow needs to process events that are published to it by a Publisher instance; Publisherには1つのメソッドがあります–subscribe().
サブスクライバーのいずれかがそれによって公開されたイベントを受信したい場合は、指定されたPublisher.にサブスクライブする必要があります
The receiver of messages needs to implement the Subscriber interface.通常、これはすべてのFlow処理の終わりです。これは、そのインスタンスがそれ以上メッセージを送信しないためです。
SubscriberはSink.と考えることができます。これには、オーバーライドする必要のある4つのメソッド(onSubscribe(), onNext(), onError(),とonComplete().)があります。次のセクションでそれらを見ていきます。
If we want to transform incoming message and pass it further to the next Subscriber, we need to implement the Processor interface.これは、メッセージを受信するためSubscriberとして機能し、それらのメッセージを処理してさらに処理するために送信するためPublisherとして機能します。
3. メッセージの公開と消費
単純なFlow,を作成し、Publisherがメッセージを公開し、単純なSubscriberがメッセージの到着時にメッセージを1つずつ消費するとします。
EndSubscriberクラスを作成しましょう。 Subscriberインターフェースを実装する必要があります。 次に、必要なメソッドをオーバーライドします。
onSubscribe()メソッドは、処理が開始される前に呼び出されます。 Subscriptionのインスタンスが引数として渡されます。 これは、SubscriberとPublisher:の間のメッセージの流れを制御するために使用されるクラスです。
public class EndSubscriber implements Subscriber {
private Subscription subscription;
public List consumedElements = new LinkedList<>();
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
}
また、テストで使用されるconsumedElementsの空のListを初期化しました。
次に、Subscriberインターフェイスから残りのメソッドを実装する必要があります。 ここでの主なメソッドはonNext()です。これは、Publisherが新しいメッセージを公開するたびに呼び出されます。
@Override
public void onNext(T item) {
System.out.println("Got : " + item);
subscription.request(1);
}
onSubscribe()メソッドでサブスクリプションを開始したとき、およびメッセージを処理したときに、Subscriptionでrequest()メソッドを呼び出して、現在のSubscriberがより多くのメッセージを消費する準備ができました。
最後に、onError()を実装する必要があります。これは、処理で例外がスローされるたびに呼び出され、Publisherが閉じられたときに呼び出されます。
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done");
}
Flow.の処理のテストを書いてみましょう。Publisherインターフェイスを実装するSubmissionPublisherクラス(java.util.concurrentからの構成)を使用します。
N要素をPublisherに送信します。これはEndSubscriberが受信します。
@Test
public void whenSubscribeToIt_thenShouldConsumeAll()
throws InterruptedException {
// given
SubmissionPublisher publisher = new SubmissionPublisher<>();
EndSubscriber subscriber = new EndSubscriber<>();
publisher.subscribe(subscriber);
List items = List.of("1", "x", "2", "x", "3", "x");
// when
assertThat(publisher.getNumberOfSubscribers()).isEqualTo(1);
items.forEach(publisher::submit);
publisher.close();
// then
await().atMost(1000, TimeUnit.MILLISECONDS)
.until(
() -> assertThat(subscriber.consumedElements)
.containsExactlyElementsOf(items)
);
}
EndSubscriber.のインスタンスでclose()メソッドを呼び出していることに注意してください。これにより、指定されたPublisher.のすべてのSubscriberの下でonComplete()コールバックが呼び出されます。
そのプログラムを実行すると、次の出力が生成されます。
Got : 1
Got : x
Got : 2
Got : x
Got : 3
Got : x
Done
4. メッセージの変換
PublisherとSubscriberの間に同様のロジックを構築したいが、何らかの変換を適用したいとします。
PublisherとSubscriber.の両方になるため、Processorを実装し、SubmissionPublisher –を拡張するTransformProcessorクラスを作成します。
入力を出力に変換するFunctionを渡します。
public class TransformProcessor
extends SubmissionPublisher
implements Flow.Processor {
private Function function;
private Flow.Subscription subscription;
public TransformProcessor(Function function) {
super();
this.function = function;
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(T item) {
submit(function.apply(item));
subscription.request(1);
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onComplete() {
close();
}
}
ここで、PublisherがString要素を公開する処理フローでwrite a quick testを作成しましょう。
TransformProcessorはStringをIntegerとして解析します。つまり、ここで変換を行う必要があります。
@Test
public void whenSubscribeAndTransformElements_thenShouldConsumeAll()
throws InterruptedException {
// given
SubmissionPublisher publisher = new SubmissionPublisher<>();
TransformProcessor transformProcessor
= new TransformProcessor<>(Integer::parseInt);
EndSubscriber subscriber = new EndSubscriber<>();
List items = List.of("1", "2", "3");
List expectedResult = List.of(1, 2, 3);
// when
publisher.subscribe(transformProcessor);
transformProcessor.subscribe(subscriber);
items.forEach(publisher::submit);
publisher.close();
// then
await().atMost(1000, TimeUnit.MILLISECONDS)
.until(() ->
assertThat(subscriber.consumedElements)
.containsExactlyElementsOf(expectedResult)
);
}
ベースPublisherでclose()メソッドを呼び出すと、TransformProcessorでonComplete()メソッドが呼び出されることに注意してください。
処理チェーン内のすべてのパブリッシャーは、この方法で閉じる必要があることに留意してください。
5. Subscriptionを使用したメッセージの需要の制御
サブスクリプションの最初の要素のみを消費し、ロジックを適用して処理を終了するとします。 これを実現するには、request()メソッドを使用できます。
N個のメッセージのみを消費するようにEndSubscriberを変更してみましょう。 その数値をhowMuchMessagesConsumeコンストラクター引数として渡します。
public class EndSubscriber implements Subscriber {
private AtomicInteger howMuchMessagesConsume;
private Subscription subscription;
public List consumedElements = new LinkedList<>();
public EndSubscriber(Integer howMuchMessagesConsume) {
this.howMuchMessagesConsume
= new AtomicInteger(howMuchMessagesConsume);
}
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(T item) {
howMuchMessagesConsume.decrementAndGet();
System.out.println("Got : " + item);
consumedElements.add(item);
if (howMuchMessagesConsume.get() > 0) {
subscription.request(1);
}
}
//...
}
必要に応じて要素をリクエストできます。
指定されたSubscription:から1つの要素のみを消費するテストを作成しましょう
@Test
public void whenRequestForOnlyOneElement_thenShouldConsumeOne()
throws InterruptedException {
// given
SubmissionPublisher publisher = new SubmissionPublisher<>();
EndSubscriber subscriber = new EndSubscriber<>(1);
publisher.subscribe(subscriber);
List items = List.of("1", "x", "2", "x", "3", "x");
List expected = List.of("1");
// when
assertThat(publisher.getNumberOfSubscribers()).isEqualTo(1);
items.forEach(publisher::submit);
publisher.close();
// then
await().atMost(1000, TimeUnit.MILLISECONDS)
.until(() ->
assertThat(subscriber.consumedElements)
.containsExactlyElementsOf(expected)
);
}
publisherは6つの要素を公開していますが、EndSubscriberは1つの要素のみを処理する必要があることを示しているため、1つの要素のみを消費します。
Subscription,でrequest()メソッドを使用することにより、メッセージ消費の速度を制御するためのより高度なバックプレッシャメカニズムを実装できます。
6. 結論
この記事では、Java 9 Reactive Streamsについて説明しました。
PublisherとSubscriber.で構成される処理Flowを作成する方法を見ました。Processorsを使用して要素を変換することで、より複雑な処理フローを作成しました。
最後に、Subscriptionを使用して、要素の需要をSubscriber.で制御しました。
これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。