Akka Streamsへのガイド

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.からListIntegerを生成する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と出力タイプIntegerFlowを作成します。

private Flow parseContent() {
    return Flow.of(String.class)
      .mapConcat(this::parseLine);
}

parseLine()メソッドを呼び出すと、コンパイラーは、そのラムダ関数への引数がStringになることを認識します。これは、Flowへの入力タイプと同じです。

parseLine()によって返されるIntegerListをフラット化するため、mapConcat()メソッド(Java 8flatMap()メソッドと同等)を使用していることに注意してください。 IntegerFlow。これにより、処理の後続のステップで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. FlowSinkを追加する

すでに述べたように、この時点では、怠惰であるため、Flow全体はまだ実行されていません。 To start execution of the Flow we need to define a SinkSink操作では、たとえば、データをデータベースに保存したり、結果を外部の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 calculateAverageForContent(String content) {
    return Source.single(content)
      .via(calculateAverage())
      .runWith(storeAverages(), ActorMaterializer.create(actorSystem))
      .whenComplete((d, e) -> {
          if (d != null) {
              System.out.println("Import finished ");
          } else {
              e.printStackTrace();
          }
      });
}

処理が終了したら、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プロジェクトであるため、そのままインポートして実行するのは簡単です。