Guide des flux Akka

Guide des cours d'eau Akka

1. Vue d'ensemble

Dans cet article, nous examinerons la bibliothèqueakka-streams qui est construite au sommet du framework d'acteur Akka, qui adhère auxreactive streams manifesto. The Akka Streams API allows us to easily compose data transformation flows from independent steps.

De plus, tous les traitements sont effectués de manière réactive, non bloquante et asynchrone.

2. Dépendances Maven

Pour commencer, nous devons ajouter les bibliothèquesakka-stream etakka-stream-testkit dans nospom.xml:


    com.typesafe.akka
    akka-stream_2.11
    2.5.2


    com.typesafe.akka
    akka-stream-testkit_2.11
    2.5.2

3. API Akka Streams

Pour utiliser Akka Streams, nous devons connaître les concepts de base des API:

  • Source the entry point to processing in the akka-stream library - nous pouvons créer une instance de cette classe à partir de plusieurs sources; par exemple, nous pouvons utiliser la méthodesingle() si nous voulons créer unSource à partir d'un seulString, ou nous pouvons créer unSource à partir d'unIterable d'éléments

  • Flow – the main processing building block - chaque instanceFlow a une valeur d'entrée et une valeur de sortie

  • Materializer – we can use one if we want our Flow to have some side effects like logging or saving results; le plus souvent, nous passerons l'aliasNotUsed en tant queMaterializer pour indiquer que nosFlow ne devraient pas avoir d'effets secondaires

  • Sink operation – when we are building a Flow, it is not executed until we will register a Sink operation dessus - c'est une opération de terminal qui déclenche tous les calculs dans l'ensemble deFlow

4. Création deFlows dans Akka Streams

Commençons par créer un exemple simple, où nous allons montrer commentcreate and combine multiple Flows - pour traiter un flux d'entiers et calculer la fenêtre mobile moyenne des paires d'entiers à partir du flux.

Nous analyserons unString d'entiers délimité par des points-virgules comme entrée pour créer nosakka-stream Source pour l'exemple.

4.1. Utilisation d'unFlow pour analyser l'entrée

Commençons par créer une classeDataImporter qui prendra une instance desActorSystem que nous utiliserons plus tard pour créer nosFlow:

public class DataImporter {
    private ActorSystem actorSystem;

    // standard constructors, getters...
}

Ensuite, créons une méthodeparseLine qui générera unList deInteger à partir de notre entrée délimitéeString. Gardez à l'esprit que nous utilisons l'API Java Stream ici uniquement pour l'analyse:

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

NotreFlow initial appliqueraparseLine à notre entrée pour créer unFlow avec le type d'entréeString et le type de sortieInteger:

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

Lorsque nous appelons la méthodeparseLine(), le compilateur sait que l'argument de cette fonction lambda sera unString - identique au type d'entrée de nosFlow.

Notez que nous utilisons la méthodemapConcat() - équivalente à la méthode Java 8flatMap() - car nous voulons aplatir lesList deInteger renvoyés parparseLine() en unFlow deInteger afin que les étapes suivantes de notre traitement n'aient pas besoin de traiter lesList.

4.2. Utilisation d'unFlow pour effectuer des calculs

À ce stade, nous avons nosFlow d'entiers analysés. Maintenant, nous avons besoin deimplement logic that will group all input elements into pairs and calculate an average of those pairs.

Maintenant, nous allonscreate a Flow of Integers and group them using the grouped() method.

Ensuite, nous voulons calculer une moyenne.

Comme nous ne sommes pas intéressés par l'ordre dans lequel ces moyennes seront traitées, nous pouvonshave averages calculated in parallel using multiple threads by using the mapAsyncUnordered() method, en passant le nombre de threads comme argument à cette méthode.

L'action qui sera transmise en tant que lambda auFlow doit renvoyer unCompletableFuture car cette action sera calculée de manière asynchrone dans le thread séparé:

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

Nous calculons des moyennes sur huit threads parallèles. Notez que nous utilisons l'API Java 8 Stream pour calculer une moyenne.

4.3. Composition de plusieursFlows en un seulFlow

L'APIFlow est une abstraction fluide qui nous permet decompose multiple Flow instances to achieve our final processing goal. Nous pouvons avoir des flux granulaires où l'un, par exemple, analyseJSON,, un autre effectue une transformation, et un autre collecte des statistiques.

Une telle granularité nous aidera à créer plus de code testable, car nous pouvons tester chaque étape de traitement indépendamment.

Nous avons créé deux flux ci-dessus qui peuvent fonctionner indépendamment l'un de l'autre. Maintenant, nous voulons les composer ensemble.

Tout d'abord, nous voulons analyser notre entréeString, et ensuite, nous voulons calculer une moyenne sur un flux d'éléments.

Nous pouvons composer nos flux en utilisant la méthodevia():

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

Nous avons créé unFlow ayant le type d'entréeString et deux autres flux après lui. LeparseContent()Flow prend une entréeString et renvoie unInteger en sortie. LecomputeAverage() Flow prend ceInteger et calcule une moyenne renvoyantDouble comme type de sortie.

5. Ajout deSink auxFlow

Comme nous l'avons mentionné, à ce stade, l'ensemble desFlow n'est pas encore exécuté car il est paresseux. To start execution of the Flow we need to define a Sink. L'opérationSink peut, par exemple, enregistrer des données dans une base de données ou envoyer des résultats à un service Web externe.

Supposons que nous ayons une classeAverageRepository avec la méthodesave() suivante qui écrit les résultats dans notre base de données:

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

Maintenant, nous voulons créer une opérationSink qui utilise cette méthode pour enregistrer les résultats de notre traitementFlow. Pour créer nosSink,, nous avons d'abord besoin decreate a Flow that takes a result of our processing as the input type. Ensuite, nous voulons enregistrer tous nos résultats dans la base de données.

Encore une fois, nous ne nous soucions pas de l'ordre des éléments, nous pouvons doncperform the save() operations in parallel en utilisant la méthodemapAsyncUnordered().

Pour créer unSink à partir desFlow, nous devons appeler lestoMat() avecSink.ignore() comme premier argument etKeep.right() comme second car nous voulons retourner un état du traitement:

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

6. Définition d'une source pourFlow

La dernière chose que nous devons faire est decreate a Source from the input*String*.. Nous pouvons appliquer uncalculateAverage()Flow à cette source en utilisant la méthodevia().

Ensuite, pour ajouter lesSink au traitement, nous devons appeler la méthoderunWith() et passer lesstoreAverages() Sink que nous venons de créer:

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

Notez que lorsque le traitement est terminé, nous ajoutons le rappelwhenComplete(), dans lequel nous pouvons effectuer une action en fonction du résultat du traitement.

7. Test deAkka Streams

Nous pouvons tester notre traitement en utilisant lesakka-stream-testkit.

La meilleure façon de tester la logique réelle du traitement est de tester toute la logiqueFlow et d'utiliserTestSink pour déclencher le calcul et affirmer les résultats.

Dans notre test, nous créons lesFlow que nous voulons tester, et ensuite, nous créons unSource à partir du contenu d'entrée du test:

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

Nous vérifions que nous attendons quatre arguments d'entrée et que deux résultats sous forme de moyennes peuvent arriver dans n'importe quel ordre, car notre traitement est effectué de manière asynchrone et parallèle.

8. Conclusion

Dans cet article, nous examinions la bibliothèqueakka-stream.

Nous avons défini un processus qui combine plusieursFlows pour calculer la moyenne mobile des éléments. Ensuite, nous avons défini unSource qui est un point d'entrée du traitement du flux et unSink qui déclenche le traitement réel.

Enfin, nous avons écrit un test pour notre traitement en utilisant lesakka-stream-testkit.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.