Flux réactifs Java 9

Java 9 flux réactifs

1. Vue d'ensemble

Dans cet article, nous examinerons les flux réactifs Java 9. En termes simples, nous pourrons utiliser la classeFlow, qui englobe les principaux blocs de construction pour la création d'une logique de traitement de flux réactif.

Reactive Streams est une norme pour le traitement de flux asynchrone avec contre-pression non bloquante. Cette spécification est définie dans lesReactive Manifesto, et il en existe diverses implémentations, par exemple,RxJava ouAkka-Streams.

2. Présentation de l'API réactive

Pour construire unFlow, nous pouvons utiliser trois abstractions principales et les composer dans une logique de traitement asynchrone.

Every Flow needs to process events that are published to it by a Publisher instance; lePublisher a une méthode -subscribe().

Si l'un des abonnés souhaite recevoir des événements publiés par lui, il doit s'abonner auxPublisher. donnés

The receiver of messages needs to implement the Subscriber interface. En général, c'est la fin de chaque traitement deFlow car son instance n'envoie plus de messages.

Nous pouvons considérerSubscriber comme unSink. Il y a quatre méthodes qui doivent être écrasées -onSubscribe(), onNext(), onError(), etonComplete(). Nous les examinerons dans la section suivante.

If we want to transform incoming message and pass it further to the next Subscriber, we need to implement the Processor interface. Cela agit à la fois comme unSubscriber car il reçoit des messages, et comme unPublisher car il traite ces messages et les envoie pour un traitement ultérieur.

3. Publication et consommation de messages

Disons que nous voulons créer un simpleFlow, dans lequel nous avons unPublisher publiant des messages, et un simpleSubscriber consommant des messages au fur et à mesure qu'ils arrivent - un à la fois.

Créons une classeEndSubscriber. Nous devons implémenter l'interfaceSubscriber. Ensuite, nous remplacerons les méthodes requises.

La méthodeonSubscribe() est appelée avant le début du traitement. L'instance desSubscription est passée comme argument. C'est une classe qui permet de contrôler le flux des messages entreSubscriber et lesPublisher:

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

Nous avons également initialisé unList vide deconsumedElements qui sera utilisé dans les tests.

Maintenant, nous devons implémenter les méthodes restantes de l'interfaceSubscriber. La méthode principale ici est onNext () - elle est appelée chaque fois que lePublisher publie un nouveau message:

@Override
public void onNext(T item) {
    System.out.println("Got : " + item);
    subscription.request(1);
}

Notez que lorsque nous avons commencé l'abonnement dans la méthodeonSubscribe() et lorsque nous avons traité un message, nous devons appeler la méthoderequest() sur lesSubscription pour signaler que leSubscriber actuel est prêt à consommer plus de messages.

Enfin, nous devons implémenteronError() - qui est appelé chaque fois qu'une exception sera lancée dans le traitement, ainsi queonComplete() – appelé lorsque lePublisher est fermé:

@Override
public void onError(Throwable t) {
    t.printStackTrace();
}

@Override
public void onComplete() {
    System.out.println("Done");
}

Écrivons un test pour le traitementFlow. Nous allons utiliser la classeSubmissionPublisher - une construction dejava.util.concurrent - qui implémente l'interfacePublisher.

Nous allons soumettre les élémentsN auxPublisher - que nosEndSubscriber recevront:

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

Notez que nous appelons la méthodeclose() sur l'instance duEndSubscriber.. Elle invoquera le rappel deonComplete() en dessous à chaqueSubscriber desPublisher. donnés

L'exécution de ce programme produira le résultat suivant:

Got : 1
Got : x
Got : 2
Got : x
Got : 3
Got : x
Done

4. Transformation des messages

Disons que nous voulons construire une logique similaire entre unPublisher et unSubscriber, mais aussi appliquer une transformation.

Nous allons créer la classeTransformProcessor qui implémenteProcessor et étendSubmissionPublisher – car ce sera à la foisPublisher et Subscriber.

Nous allons passer unFunction qui transformera les entrées en sorties:

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

Soit maintenantwrite a quick test avec un flux de traitement dans lequelPublisher publie les élémentsString.

NosTransformProcessor analyseront lesString commeInteger - ce qui signifie qu'une conversion doit avoir lieu ici:

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

Notez que l'appel de la méthodeclose() sur la basePublisher entraînera l'appel de la méthodeonComplete() sur lesTransformProcessor.

Gardez à l'esprit que tous les éditeurs de la chaîne de traitement doivent être fermés de cette façon.

5. Contrôle de la demande de messages à l'aide desSubscription

Supposons que nous souhaitons utiliser uniquement le premier élément de l'abonnement, appliquer une logique et terminer le traitement. Nous pouvons utiliser la méthoderequest() pour y parvenir.

Modifions nosEndSubscriber pour ne consommer que N nombre de messages. Nous passerons ce nombre comme argument du constructeur dehowMuchMessagesConsume:

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);
        }
    }
    //...

}

Nous pouvons demander des éléments aussi longtemps que nous le voulons.

Écrivons un test dans lequel nous ne voulons consommer qu’un élément desSubscription: donnés

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

Bien que lepublisher publie six éléments, notreEndSubscriber ne consommera qu'un seul élément car il signale la demande de traitement uniquement de celui-ci.

En utilisant la méthoderequest() sur lesSubscription,, nous pouvons implémenter un mécanisme de contre-pression plus sophistiqué pour contrôler la vitesse de consommation des messages.

6. Conclusion

Dans cet article, nous avons examiné les flux réactifs Java 9.

Nous avons vu comment créer un traitementFlow constitué d'unPublisher et d'unSubscriber. Nous avons créé un flux de traitement plus complexe avec la transformation d'éléments en utilisantProcessors.

Enfin, nous avons utilisé lesSubscription pour contrôler la demande d'éléments par lesSubscriber.

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.