Leitfaden für Akka-Streams

Führer zu den Akka Bächen

1. Überblick

In diesem Artikel werden wir uns dieakka-streams-Bibliothek ansehen, die auf dem Akka-Actor-Framework aufgebaut ist, das denreactive streams manifesto entspricht. The Akka Streams API allows us to easily compose data transformation flows from independent steps.

Darüber hinaus erfolgt die gesamte Verarbeitung auf reaktive, nicht blockierende und asynchrone Weise.

2. Maven-Abhängigkeiten

Um zu beginnen, müssen wir die Bibliothekenakka-stream undakka-stream-testkit zu unserenpom.xml: hinzufügen


    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

Um mit Akka Streams arbeiten zu können, müssen uns die wichtigsten API-Konzepte bekannt sein:

  • Source the entry point to processing in the akka-stream library - Wir können eine Instanz dieser Klasse aus mehreren Quellen erstellen. Zum Beispiel können wir die Methodesingle() verwenden, wenn wir einSource aus einem einzelnenString erstellen möchten, oder wir können einSource aus einemIterable erstellen von Elementen

  • Flow – the main processing building block - Jede Instanz vonFlowhat einen Eingabe- und einen Ausgabewert

  • Materializer – we can use one if we want our Flow to have some side effects like logging or saving results; Am häufigsten übergeben wir den AliasNotUsed alsMaterializer, um anzuzeigen, dass unsereFlow keine Nebenwirkungen haben sollten

  • Sink operation – when we are building a Flow, it is not executed until we will register a Sink operation drauf - es ist eine Terminaloperation, die alle Berechnungen in den gesamtenFlow auslöst

4. Flows in Akka-Streams erstellen

Beginnen wir mit einem einfachen Beispiel, in dem wir zeigen, wiecreate and combine multiple Flows - einen Strom von Ganzzahlen verarbeitet und das durchschnittliche Bewegungsfenster von Ganzzahlpaaren aus dem Strom berechnet wird.

Wir analysieren ein durch Semikolons getrenntesString von Ganzzahlen als Eingabe, um unsereakka-stream Source für das Beispiel zu erstellen.

4.1. Verwenden vonFlow zum Analysieren der Eingabe

Zuerst erstellen wir eineDataImporter-Klasse, die eine Instanz derActorSystem verwendet, die wir später zum Erstellen unsererFlow verwenden werden:

public class DataImporter {
    private ActorSystem actorSystem;

    // standard constructors, getters...
}

Als Nächstes erstellen wir eineparseLine-Methode, die aus unserer begrenzten EingabeString.List vonInteger generiert. Beachten Sie, dass wir hier die Java Stream-API nur zum Parsen verwenden:

private List parseLine(String line) {
    String[] fields = line.split(";");
    return Arrays.stream(fields)
      .map(Integer::parseInt)
      .collect(Collectors.toList());
}

Unsere anfänglichenFlow wendenparseLine auf unsere Eingabe an, um einFlow mit dem EingabetypString und dem AusgabetypInteger zu erstellen:

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

Wenn wir dieparseLine()-Methode aufrufen, weiß der Compiler, dass das Argument für diese Lambda-FunktionString ist - genau wie der Eingabetyp für unsereFlow.

Beachten Sie, dass wir diemapConcat()-Methode verwenden - entsprechend der Java 8flatMap()-Methode -, weil wir dieList vonInteger, die vonparseLine() zurückgegeben werden, reduzieren möchten aFlow vonInteger, so dass nachfolgende Schritte in unserer Verarbeitung sich nicht mitList befassen müssen.

4.2. Verwenden vonFlow zum Durchführen von Berechnungen

Zu diesem Zeitpunkt haben wir unsereFlow der analysierten ganzen Zahlen. Jetzt müssen wirimplement logic that will group all input elements into pairs and calculate an average of those pairs.

Jetzt werden wircreate a Flow of Integers and group them using the grouped() method.

Als nächstes wollen wir einen Durchschnitt berechnen.

Da wir nicht an der Reihenfolge interessiert sind, in der diese Durchschnittswerte verarbeitet werden, können wirhave averages calculated in parallel using multiple threads by using the mapAsyncUnordered() method eingeben und die Anzahl der Threads als Argument an diese Methode übergeben.

Die Aktion, die als Lambda anFlow übergeben wird, mussCompletableFuture zurückgeben, da diese Aktion im separaten Thread asynchron berechnet wird:

private Flow computeAverage() {
    return Flow.of(Integer.class)
      .grouped(2)
      .mapAsyncUnordered(8, integers ->
        CompletableFuture.supplyAsync(() -> integers.stream()
          .mapToDouble(v -> v)
          .average()
          .orElse(-1.0)));
}

Wir berechnen Durchschnittswerte in acht parallelen Threads. Beachten Sie, dass wir die Java 8 Stream-API zur Berechnung eines Durchschnitts verwenden.

4.3. Zusammensetzen mehrererFlows zu einem einzelnenFlow

DieFlow-API ist eine fließende Abstraktion, die es uns ermöglicht,compose multiple Flow instances to achieve our final processing goal zu verwenden. Wir können granulare Flows haben, bei denen einer beispielsweiseJSON,analysiert, während ein anderer eine Transformation durchführt und ein anderer Statistiken sammelt.

Eine solche Granularität hilft uns, prüfbareren Code zu erstellen, da wir jeden Verarbeitungsschritt unabhängig testen können.

Wir haben zwei Flüsse darüber erstellt, die unabhängig voneinander funktionieren können. Jetzt wollen wir sie zusammenstellen.

Zuerst möchten wir unsere EingabeString analysieren, und als nächstes möchten wir einen Durchschnitt für einen Strom von Elementen berechnen.

Wir können unsere Flüsse mit der Methodevia()zusammensetzen:

Flow calculateAverage() {
    return Flow.of(String.class)
      .via(parseContent())
      .via(computeAverage());
}

Wir haben einFlow mit dem EingabetypString und zwei weiteren Flows danach erstellt. parseContent()Flow nimmt eineString-Eingabe und gibt eineInteger als Ausgabe zurück. DiecomputeAverage() Flow nehmen dieseInteger und berechnen eine durchschnittliche Rückgabe vonDouble als Ausgabetyp.

5. Hinzufügen vonSink zuFlow

Wie bereits erwähnt, wird bis zu diesem Punkt das gesamteFlow noch nicht ausgeführt, da es faul ist. To start execution of the Flow we need to define a Sink. Die OperationSinkkann beispielsweise Daten in einer Datenbank speichern oder Ergebnisse an einen externen Webdienst senden.

Angenommen, wir haben eineAverageRepository-Klasse mit der folgendensave()-Methode, die Ergebnisse in unsere Datenbank schreibt:

CompletionStage save(Double average) {
    return CompletableFuture.supplyAsync(() -> {
        // write to database
        return average;
    });
}

Jetzt möchten wir eineSink-Operation erstellen, die diese Methode verwendet, um die Ergebnisse unsererFlow-Verarbeitung zu speichern. Um unsereSink, zu erstellen, müssen wir zuerstcreate a Flow that takes a result of our processing as the input type erstellen. Als nächstes möchten wir alle unsere Ergebnisse in der Datenbank speichern.

Auch hier ist uns die Reihenfolge der Elemente egal, daher können wirperform the save() operations in parallel mit der MethodemapAsyncUnordered() verwenden.

Um ausFlow einSink zu erstellen, müssen wirtoMat() mitSink.ignore() als erstem Argument undKeep.right() als zweitem Argument aufrufen, da wir a zurückgeben möchten Status der Verarbeitung:

private Sink> storeAverages() {
    return Flow.of(Double.class)
      .mapAsyncUnordered(4, averageRepository::save)
      .toMat(Sink.ignore(), Keep.right());
}

6. Definieren einer Quelle fürFlow

Das Letzte, was wir tun müssen, istcreate a Source from the input*String*. Wir könnencalculateAverage()Flow mit der Methodevia() auf diese Quelle anwenden.

Um dann dieSink zur Verarbeitung hinzuzufügen, müssen wir dierunWith()-Methode aufrufen und die soeben erstelltenstoreAverages() Sink übergeben:

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();
          }
      });
}

Beachten Sie, dass wir nach Abschluss der Verarbeitung den RückrufwhenComplete()hinzufügen, in dem wir abhängig vom Ergebnis der Verarbeitung einige Aktionen ausführen können.

7. Testen vonAkka Streams

Wir können unsere Verarbeitung mitakka-stream-testkit. testen

Der beste Weg, um die tatsächliche Logik der Verarbeitung zu testen, besteht darin, die gesamte Logik vonFlowzu testen undTestSink zu verwenden, um die Berechnung auszulösen und die Ergebnisse zu bestätigen.

In unserem Test erstellen wir dieFlow, die wir testen möchten, und als Nächstes erstellen wirSource aus dem Inhalt der Testeingabe:

@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);
}

Wir überprüfen, ob wir vier Eingabeargumente erwarten, und zwei Ergebnisse, die Durchschnittswerte sind, können in beliebiger Reihenfolge eingehen, da unsere Verarbeitung asynchron und parallel erfolgt.

8. Fazit

In diesem Artikel haben wir uns die Bibliothekakka-streamangesehen.

Wir haben einen Prozess definiert, der mehrereFlows kombiniert, um den gleitenden Durchschnitt der Elemente zu berechnen. Dann haben wir einSource definiert, das ein Einstiegspunkt für die Stream-Verarbeitung ist, und einSink, das die eigentliche Verarbeitung auslöst.

Schließlich haben wir einen Test für unsere Verarbeitung mitakka-stream-testkit geschrieben.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.