Java 9 reaktive Streams

Reaktive Java 9-Streams

1. Überblick

In diesem Artikel befassen wir uns mit den Java 9 Reactive Streams. Einfach ausgedrückt, können wir die KlasseFlowverwenden, die die primären Bausteine ​​für die Erstellung der Verarbeitungslogik für reaktive Streams enthält.

Reactive Streams ist ein Standard für die asynchrone Stream-Verarbeitung mit nicht blockierendem Gegendruck. Diese Spezifikation ist inReactive Manifesto, definiert und es gibt verschiedene Implementierungen davon, zum BeispielRxJava oderAkka-Streams.

2. Reaktive API-Übersicht

Um einFlow zu erstellen, können wir drei Hauptabstraktionen verwenden und sie zu einer asynchronen Verarbeitungslogik zusammensetzen.

Every Flow needs to process events that are published to it by a Publisher instance; Publisher hat eine Methode -subscribe().

Wenn einer der Abonnenten von ihm veröffentlichte Ereignisse empfangen möchte, muss er die angegebenenPublisher. abonnieren

The receiver of messages needs to implement the Subscriber interface. In der Regel ist dies das Ende jederFlow-Verarbeitung, da die Instanz keine weiteren Nachrichten sendet.

Wir können unsSubscriber alsSink. vorstellen. Dies hat vier Methoden, die überschrieben werden müssen -onSubscribe(), onNext(), onError(), undonComplete().. Wir werden uns diese im nächsten Abschnitt ansehen.

If we want to transform incoming message and pass it further to the next Subscriber, we need to implement the Processor interface. Dies wirkt sowohl alsSubscriber, weil es Nachrichten empfängt, als auch alsPublisher, weil es diese Nachrichten verarbeitet und zur weiteren Verarbeitung sendet.

3. Nachrichten veröffentlichen und konsumieren

Nehmen wir an, wir möchten ein einfachesFlow,erstellen, in dem einPublisherNachrichten veröffentlicht, und ein einfachesSubscriber, das Nachrichten verbraucht, sobald sie eintreffen - einzeln.

Erstellen wir eineEndSubscriber-Klasse. Wir müssen dieSubscriber-Schnittstelle implementieren. Als Nächstes überschreiben wir die erforderlichen Methoden.

Die MethodeonSubscribe() wird aufgerufen, bevor die Verarbeitung beginnt. Die Instanz vonSubscription wird als Argument übergeben. Diese Klasse wird verwendet, um den Nachrichtenfluss zwischenSubscriber undPublisher: zu steuern

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

Wir haben auch leereList vonconsumedElements initialisiert, die in den Tests verwendet werden.

Jetzt müssen wir die verbleibenden Methoden von derSubscriber-Schnittstelle implementieren. Die Hauptmethode hier ist onNext () - dies wird immer dann aufgerufen, wennPublisher eine neue Nachricht veröffentlicht:

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

Beachten Sie, dass wir beim Starten des Abonnements mit der MethodeonSubscribe() und beim Verarbeiten einer Nachricht die Methoderequest() fürSubscription aufrufen müssen, um zu signalisieren, dass die aktuelleSubscriber ist bereit, mehr Nachrichten zu verbrauchen.

Zuletzt müssen wironError() implementieren - das aufgerufen wird, wenn eine Ausnahme in der Verarbeitung ausgelöst wird, sowieonComplete() –, das aufgerufen wird, wennPublisher geschlossen wird:

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

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

Schreiben wir einen Test für die VerarbeitungFlow.. Wir verwenden die KlasseSubmissionPublisher - ein Konstrukt ausjava.util.concurrent -, das die SchnittstellePublisher implementiert.

Wir werdenN Elemente anPublisher senden - die unsereEndSubscriber erhalten:

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

Beachten Sie, dass wir die Methodeclose() für die Instanz vonEndSubscriber. aufrufen. Sie ruft den Rückruf vononComplete() für alleSubscriber der angegebenenPublisher. auf

Das Ausführen dieses Programms erzeugt die folgende Ausgabe:

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

4. Transformation von Nachrichten

Nehmen wir an, wir möchten eine ähnliche Logik zwischen aPublisher und aSubscriber erstellen, aber auch eine Transformation anwenden.

Wir erstellen dieTransformProcessor-Klasse, dieProcessor implementiert undSubmissionPublisher – erweitert, da dies sowohlPublisher als auch Subscriber. ist

Wir übergeben einFunction, das Eingaben in Ausgaben umwandelt:

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

Lassen Sie uns nunwrite a quick test mit einem Verarbeitungsablauf ausführen, in demPublisherString Elemente veröffentlicht.

UnsereTransformProcessor analysieren dieString alsInteger - was bedeutet, dass hier eine Konvertierung stattfinden muss:

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

Beachten Sie, dass beim Aufrufen der Methodeclose() auf der BasisPublisher die MethodeonComplete() auf der BasisTransformProcessor aufgerufen wird.

Beachten Sie, dass alle Publisher in der Verarbeitungskette auf diese Weise geschlossen werden müssen.

5. Steuern der Nachfrage nach Nachrichten mitSubscription

Nehmen wir an, wir möchten nur das erste Element aus dem Abonnement verwenden, eine gewisse Logik anwenden und die Verarbeitung beenden. Wir können dierequest()-Methode verwenden, um dies zu erreichen.

Ändern Sie unsereEndSubscriber so, dass nur N Nachrichten verbraucht werden. Wir übergeben diese Zahl als Konstruktorargument vonhowMuchMessagesConsume:

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

}

Wir können Elemente anfordern, solange wir möchten.

Schreiben wir einen Test, in dem wir nur ein Element aus den angegebenenSubscription: verwenden möchten

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

Obwohlpublisher sechs Elemente veröffentlicht, verbrauchen unsereEndSubscriber nur ein Element, da dies die Nachfrage nach der Verarbeitung nur dieses einzelnen Elements signalisiert.

Durch Verwendung derrequest()-Methode fürSubscription, können wir einen ausgefeilteren Gegendruckmechanismus implementieren, um die Geschwindigkeit des Nachrichtenverbrauchs zu steuern.

6. Fazit

In diesem Artikel haben wir uns die Java 9 Reactive Streams angesehen.

Wir haben gesehen, wie man eine VerarbeitungFlow erstellt, die ausPublisher undSubscriber. besteht. Wir haben einen komplexeren Verarbeitungsfluss mit der Transformation von Elementen unter Verwendung vonProcessors erstellt.

Schließlich haben wirSubscription verwendet, um die Nachfrage nach Elementen durchSubscriber. zu steuern

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.