Akka Streamsのガイド
1. 概要
この記事では、reactive streams manifestoに準拠するAkkaアクターフレームワークの上に構築されたakka-streamsライブラリを見ていきます。 The Akka Streams API allows us to easily compose data transformation flows from independent steps.
さらに、すべての処理は、リアクティブ、非ブロッキング、非同期の方法で行われます。
2. Mavenの依存関係
開始するには、akka-streamおよびakka-stream-testkitライブラリをpom.xml:に追加する必要があります
com.typesafe.akka
akka-stream_2.11
2.5.2
com.typesafe.akka
akka-stream-testkit_2.11
2.5.2
3. Akka Streams API
Akka Streamsを使用するには、コアAPIの概念を認識する必要があります。
-
Source – the entry point to processing in the akka-stream library –複数のソースからこのクラスのインスタンスを作成できます。たとえば、単一のStringからSourceを作成する場合は、single()メソッドを使用できます。または、IterableからSourceを作成することもできます。要素の
-
Flow – the main processing building block –すべてのFlowインスタンスには、1つの入力値と1つの出力値があります
-
Materializer – we can use one if we want our Flow to have some side effects like logging or saving results;最も一般的には、NotUsedエイリアスをMaterializerとして渡して、Flowに副作用がないことを示します。
-
Sink operation – when we are building a Flow, it is not executed until we will register a Sink operation –これはFlow全体のすべての計算をトリガーする端末操作です
4. Akka StreamsでFlowsを作成する
簡単な例を作成することから始めましょう。ここでは、整数のストリームを処理し、ストリームから整数ペアの平均移動ウィンドウを計算する方法をcreate and combine multiple Flowsで示します。
セミコロンで区切られた整数のStringを入力として解析し、例のakka-stream Sourceを作成します。
4.1. Flowを使用して入力を解析する
まず、後でFlowを作成するために使用するActorSystemのインスタンスを取得するDataImporterクラスを作成しましょう。
public class DataImporter {
private ActorSystem actorSystem;
// standard constructors, getters...
}
次に、区切られた入力String.からListのIntegerを生成するparseLineメソッドを作成しましょう。ここでは解析のみにJava StreamAPIを使用していることに注意してください。
private List parseLine(String line) {
String[] fields = line.split(";");
return Arrays.stream(fields)
.map(Integer::parseInt)
.collect(Collectors.toList());
}
最初のFlowは、入力にparseLineを適用して、入力タイプStringと出力タイプIntegerのFlowを作成します。
private Flow parseContent() {
return Flow.of(String.class)
.mapConcat(this::parseLine);
}
parseLine()メソッドを呼び出すと、コンパイラーは、そのラムダ関数への引数がStringになることを認識します。これは、Flowへの入力タイプと同じです。
parseLine()によって返されるIntegerのListをフラット化するため、mapConcat()メソッド(Java 8flatMap()メソッドと同等)を使用していることに注意してください。 IntegerのFlow。これにより、処理の後続のステップでListを処理する必要がなくなります。
4.2. Flowを使用して計算を実行する
この時点で、解析された整数のFlowがあります。 ここで、implement logic that will group all input elements into pairs and calculate an average of those pairsにする必要があります。
ここで、create a Flow of Integers and group them using the grouped() methodにします。
次に、平均を計算します。
これらの平均が処理される順序には関心がないため、have averages calculated in parallel using multiple threads by using the mapAsyncUnordered() methodを実行して、スレッド数を引数としてこのメソッドに渡すことができます。
ラムダとしてFlowに渡されるアクションは、別のスレッドで非同期に計算されるため、CompletableFutureを返す必要があります。
private Flow computeAverage() {
return Flow.of(Integer.class)
.grouped(2)
.mapAsyncUnordered(8, integers ->
CompletableFuture.supplyAsync(() -> integers.stream()
.mapToDouble(v -> v)
.average()
.orElse(-1.0)));
}
8つの並列スレッドで平均を計算しています。 平均の計算にはJava 8 Stream APIを使用していることに注意してください。
4.3. 複数のFlowsを単一のFlowに構成する
Flow APIは、compose multiple Flow instances to achieve our final processing goalを可能にする流暢な抽象化です。 たとえば、あるものがJSON,を解析し、別のフローが何らかの変換を実行し、別のフローがいくつかの統計を収集する、きめ細かいフローを作成できます。
このような粒度は、各処理ステップを個別にテストできるため、よりテスト可能なコードを作成するのに役立ちます。
上記の2つのフローを作成しました。これらは互いに独立して機能します。 次に、それらを一緒に作成したいと思います。
最初に、入力Stringを解析し、次に、要素のストリームの平均を計算します。
via()メソッドを使用してフローを構成できます。
Flow calculateAverage() {
return Flow.of(String.class)
.via(parseContent())
.via(computeAverage());
}
入力タイプStringを持つFlowと、その後に2つの他のフローを作成しました。 parseContent()Flowは、String入力を受け取り、Integerを出力として返します。 computeAverage() FlowはそのIntegerを取得し、出力タイプとしてDoubleを返す平均を計算します。
5. FlowにSinkを追加する
すでに述べたように、この時点では、怠惰であるため、Flow全体はまだ実行されていません。 To start execution of the Flow we need to define a Sink。 Sink操作では、たとえば、データをデータベースに保存したり、結果を外部のWebサービスに送信したりできます。
データベースに結果を書き込む次のsave()メソッドを持つAverageRepositoryクラスがあるとします。
CompletionStage save(Double average) {
return CompletableFuture.supplyAsync(() -> {
// write to database
return average;
});
}
ここで、このメソッドを使用してFlow処理の結果を保存するSink操作を作成します。 Sink,を作成するには、最初にcreate a Flow that takes a result of our processing as the input typeを作成する必要があります。 次に、すべての結果をデータベースに保存します。
繰り返しますが、要素の順序は気にしないので、mapAsyncUnordered()メソッドを使用してperform the save() operations in parallelを実行できます。
FlowからSinkを作成するには、Sink.ignore()を最初の引数として、Keep.right()を2番目の引数としてtoMat()を呼び出す必要があります。これは、処理のステータス:
private Sink> storeAverages() {
return Flow.of(Double.class)
.mapAsyncUnordered(4, averageRepository::save)
.toMat(Sink.ignore(), Keep.right());
}
6. Flowのソースの定義
最後に行う必要があるのは、create a Source from the input*String*.です。via()メソッドを使用して、このソースにcalculateAverage()Flowを適用できます。
次に、Sinkを処理に追加するには、runWith()メソッドを呼び出して、作成したばかりのstoreAverages() Sinkを渡す必要があります。
CompletionStage
処理が終了したら、whenComplete()コールバックを追加していることに注意してください。このコールバックでは、処理の結果に応じて何らかのアクションを実行できます。
7. Akka Streamsのテスト
akka-stream-testkit.を使用して処理をテストできます
処理の実際のロジックをテストする最良の方法は、すべてのFlowロジックをテストし、TestSinkを使用して計算をトリガーし、結果をアサートすることです。
このテストでは、テストするFlowを作成し、次に、テスト入力コンテンツからSourceを作成します。
@Test
public void givenStreamOfIntegers_whenCalculateAverageOfPairs_thenShouldReturnProperResults() {
// given
Flow tested = new DataImporter(actorSystem).calculateAverage();
String input = "1;9;11;0";
// when
Source flow = Source.single(input).via(tested);
// then
flow
.runWith(TestSink.probe(actorSystem), ActorMaterializer.create(actorSystem))
.request(4)
.expectNextUnordered(5d, 5.5);
}
4つの入力引数を期待していることを確認しています。平均である2つの結果は、処理が非同期かつ並列に行われるため、任意の順序で到着できます。
8. 結論
この記事では、akka-streamライブラリを見ていました。
複数のFlowsを組み合わせて、要素の移動平均を計算するプロセスを定義しました。 次に、ストリーム処理のエントリポイントであるSourceと、実際の処理をトリガーするSinkを定義しました。
最後に、akka-stream-testkitを使用して処理のテストを作成しました。
これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。