RxJava One Observable, Mehrere Abonnenten

RxJava One Observable, Mehrere Abonnenten

1. Überblick

Das Standardverhalten mehrerer Abonnenten ist nicht immer wünschenswert. In diesem Artikel erfahren Sie, wie Sie dieses Verhalten ändern und mehrere Abonnenten ordnungsgemäß behandeln.

Schauen wir uns zunächst das Standardverhalten mehrerer Abonnenten an.

2. Standardverhalten

Nehmen wir an, wir haben die folgendenObservable:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        subscriber.onNext(gettingValue(1));
        subscriber.onNext(gettingValue(2));

        subscriber.add(Subscriptions.create(() -> {
            LOGGER.info("Clear resources");
        }));
    });
}

Dies gibt zwei Elemente aus, sobaldSubscribers abonniert ist.

In unserem Beispiel haben wir zweiSubscribers:

LOGGER.info("Subscribing");

Subscription s1 = obs.subscribe(i -> LOGGER.info("subscriber#1 is printing " + i));
Subscription s2 = obs.subscribe(i -> LOGGER.info("subscriber#2 is printing " + i));

s1.unsubscribe();
s2.unsubscribe();

Stellen Sie sich vor, dass das Abrufen jedes Elements eine kostspielige Operation ist - beispielsweise eine intensive Berechnung oder das Öffnen einer URL-Verbindung.

Um die Sache einfach zu halten, geben wir einfach eine Nummer zurück:

private static Integer gettingValue(int i) {
    LOGGER.info("Getting " + i);
    return i;
}

Hier ist die Ausgabe:

Subscribing
Getting 1
subscriber#1 is printing 1
Getting 2
subscriber#1 is printing 2
Getting 1
subscriber#2 is printing 1
Getting 2
subscriber#2 is printing 2
Clear resources
Clear resources

Wie wir sehen können,getting each element as well as clearing the resources is performed twice by default - einmal für jedesSubscriber. Das wollen wir nicht. Die KlasseConnectableObservablehilft, das Problem zu beheben.

3. ConnectableObservable

Die KlasseConnectableObservableermöglicht es, das Abonnement für mehrere Abonnenten freizugeben und die zugrunde liegenden Vorgänge nicht mehrmals auszuführen.

Aber zuerst erstellen wir einConnectableObservable.

3.1. publish()

Mit der Methodepublish() wird ausObservable einConnectableObservable erstellt:

ConnectableObservable obs = Observable.create(subscriber -> {
    subscriber.onNext(gettingValue(1));
    subscriber.onNext(gettingValue(2));
    subscriber.add(Subscriptions.create(() -> {
        LOGGER.info("Clear resources");
    }));
}).publish();

Aber im Moment tut es nichts. Was es funktioniert, ist dieconnect()-Methode.

3.2. connect()

Until ConnectableObservable‘s connect() method isn’t called Observable‘s onSubcribe() callback isn’t triggered auch wenn es einige Abonnenten gibt.

Lassen Sie uns dies demonstrieren:

LOGGER.info("Subscribing");
obs.subscribe(i -> LOGGER.info("subscriber #1 is printing " + i));
obs.subscribe(i -> LOGGER.info("subscriber #2 is printing " + i));
Thread.sleep(1000);
LOGGER.info("Connecting");
Subscription s = obs.connect();
s.unsubscribe();

Wir abonnieren und warten eine Sekunde, bevor wir uns verbinden. Die Ausgabe ist:

Subscribing
Connecting
Getting 1
subscriber #1 is printing 1
subscriber #2 is printing 1
Getting 2
subscriber #1 is printing 2
subscriber #2 is printing 2
Clear resources

Wie wir sehen können:

    • Das Erhalten von Elementen erfolgt nur einmal, wie wir wollten

    • Das Löschen von Ressourcen erfolgt ebenfalls nur einmal

    • Das Abrufen von Elementen beginnt eine Sekunde nach dem Abonnieren.

    • Durch das Abonnieren werden keine Elemente mehr ausgegeben. Dies geschieht nur mitconnect()

Diese Verzögerung kann vorteilhaft sein - manchmal müssen wir allen Abonnenten die gleiche Reihenfolge von Elementen zuweisen, auch wenn einer von ihnen früher als der andere abonniert.

3.3. Die konsistente Ansicht der Observablen -connect() Nachsubscribe()

Dieser Anwendungsfall kann in unseren vorherigenObservablenicht demonstriert werden, da es kalt läuft und beide Abonnenten ohnehin die gesamte Folge von Elementen erhalten.

Stellen Sie sich stattdessen vor, dass ein emittierendes Element nicht vom Zeitpunkt des Abonnements abhängt, z. B. Ereignisse, die per Mausklick ausgelöst werden. Stellen Sie sich nun auch vor, dass ein zweitesSubscriber eine Sekunde nach dem ersten abonniert.

Die erstenSubscriber erhalten alle in diesem Beispiel emittierten Elemente, während die zweitenSubscriber nur einige Elemente empfangen.

Andererseits kann die Verwendung derconnect()-Methode an der richtigen Stelle beiden Teilnehmern die gleiche Ansicht über dieObservable-Sequenz geben.

Beispiel für HotObservable

Erstellen wir ein heißesObservable. Bei Mausklicks aufJFrame werden Elemente ausgegeben.

Jedes Element ist die x-Koordinate des Klicks:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        frame.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                subscriber.onNext(e.getX());
            }
        });
        subscriber.add(Subscriptions.create(() {
            LOGGER.info("Clear resources");
            for (MouseListener listener : frame.getListeners(MouseListener.class)) {
                frame.removeMouseListener(listener);
            }
        }));
    });
}

Das Standardverhalten von HotObservable

Wenn wir nun zweiSubscribers nacheinander mit einem zweiten Intervall abonnieren, das Programm ausführen und mit dem Klicken beginnen, werden wir sehen, dass die erstenSubscriber mehr Elemente erhalten:

public static void defaultBehaviour() throws InterruptedException {
    Observable obs = getObservable();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
subscribing #1
subscriber#1 is printing x-coordinate 280
subscriber#1 is printing x-coordinate 242
subscribing #2
subscriber#1 is printing x-coordinate 343
subscriber#2 is printing x-coordinate 343
unsubscribe#1
clearing resources
unsubscribe#2
clearing resources

connect() Nachsubscribe()

Damit beide Abonnenten die gleiche Sequenz erhalten, konvertieren wir dieseObservable inConnectableObservable und rufenconnect() nach dem Abonnement beideSubscribers auf:

public static void subscribeBeforeConnect() throws InterruptedException {

    ConnectableObservable obs = getObservable().publish();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i ->  LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe connected");
    s.unsubscribe();
}

Jetzt erhalten sie dieselbe Sequenz:

subscribing #1
subscribing #2
connecting:
subscriber#1 is printing x-coordinate 317
subscriber#2 is printing x-coordinate 317
subscriber#1 is printing x-coordinate 364
subscriber#2 is printing x-coordinate 364
unsubscribe connected
clearing resources

Es geht also darum, auf den Moment zu warten, in dem alle Teilnehmer bereit sind, und dannconnect() aufzurufen.

In einer Spring-Anwendung können wir beispielsweise alle Komponenten während des Anwendungsstarts abonnieren undconnect() inonApplicationEvent() aufrufen.

Kehren wir jedoch zu unserem Beispiel zurück. Beachten Sie, dass alle Klicks vor der Methodeconnect()übersehen werden. Wenn wir keine Elemente verpassen möchten, sondern sie im Gegenteil verarbeiten möchten, können wirconnect() früher in den Code einfügen und dieObservable zwingen, Ereignisse zu erzeugen, wenn keineSubscriber vorhanden sind.

3.4. Erzwingen eines Abonnements in Abwesenheit vonSubscriber -connect() vorsubscribe()

Um dies zu demonstrieren, korrigieren wir unser Beispiel:

public static void connectBeforeSubscribe() throws InterruptedException {
    ConnectableObservable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish();
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    obs.subscribe((i) -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    obs.subscribe((i) -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    s.unsubscribe();
}

Die Schritte sind relativ einfach:

  • Zuerst verbinden wir uns

  • Dann warten wir eine Sekunde und abonnieren die erstenSubscriber

  • Schließlich warten wir noch eine Sekunde und abonnieren die zweitenSubscriber

Beachten Sie, dass wir den OperatordoOnNext()hinzugefügt haben. Hier könnten wir zum Beispiel Elemente in der Datenbank speichern, aber in unserem Code drucken wir einfach „save…“.

Wenn wir den Code starten und mit dem Klicken beginnen, werden die Elemente unmittelbar nach dem Aufruf vonconnect()ausgegeben und verarbeitet:

connecting:
saving 306
saving 248
subscribing #1
saving 377
subscriber#1 is printing x-coordinate 377
saving 295
subscriber#1 is printing x-coordinate 295
saving 206
subscriber#1 is printing x-coordinate 206
subscribing #2
saving 347
subscriber#1 is printing x-coordinate 347
subscriber#2 is printing x-coordinate 347
clearing resources

Wenn es keine Abonnenten gäbe, würden die Elemente noch verarbeitet.

So the connect() method starts emitting and processing elements regardless of whether someone is subscribed als ob es ein künstlichesSubscriber mit einer leeren Aktion gäbe, die die Elemente verbraucht.

Und wenn einige echteSubscribers abonnieren, verbreitet dieser künstliche Mediator nur Elemente an sie.

Um die künstlichenSubscriber abzubestellen, führen wir Folgendes durch:

s.unsubscribe();

Wo:

Subscription s = obs.connect();

3.5. autoConnect()

This method implies that connect() isn’t called before or after subscriptions but automatically when the first Subscriber subscribes.

Mit dieser Methode können wirconnect() nicht selbst aufrufen, da das zurückgegebene Objekt ein gewöhnlichesObservable ist, das diese Methode nicht hat, aber ein zugrunde liegendesConnectableObservable verwendet:

public static void autoConnectAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
    .doOnNext(x -> LOGGER.info("saving " + x)).publish().autoConnect();

    LOGGER.info("autoconnect()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription s1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription s2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe 1");
    s1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe 2");
    s2.unsubscribe();
}

Beachten Sie, dass wir auch die künstlichenSubscribernicht abbestellen können. Wir können alle realenSubscribers abbestellen, aber die künstlichenSubscriber verarbeiten die Ereignisse weiterhin.

Um dies zu verstehen, schauen wir uns an, was am Ende passiert, nachdem sich der letzte Abonnent abgemeldet hat:

subscribing #1
saving 296
subscriber#1 is printing x-coordinate 296
saving 329
subscriber#1 is printing x-coordinate 329
subscribing #2
saving 226
subscriber#1 is printing x-coordinate 226
subscriber#2 is printing x-coordinate 226
unsubscribe 1
saving 268
subscriber#2 is printing x-coordinate 268
saving 234
subscriber#2 is printing x-coordinate 234
unsubscribe 2
saving 278
saving 268

Wie wir sehen können, werden keine Ressourcen gelöscht und das Speichern von Elementen mitdoOnNext() wird nach dem zweiten Abbestellen fortgesetzt. Dies bedeutet, dass die künstlichenSubscriber nicht abbestellen, sondern weiterhin Elemente verbrauchen.

3.6. refCount()

refCount() ähneltautoConnect() darin, dass die Verbindung auch automatisch erfolgt, sobald die erstenSubscriber abonniert sind.

Im Gegensatz zuautoconnect()erfolgt das Trennen der Verbindung auch automatisch, wenn sich das letzteSubscriberabmeldet:

public static void refCountAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish().refCount();

    LOGGER.info("refcount()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
refcount()
subscribing #1
saving 265
subscriber#1 is printing x-coordinate 265
saving 338
subscriber#1 is printing x-coordinate 338
subscribing #2
saving 203
subscriber#1 is printing x-coordinate 203
subscriber#2 is printing x-coordinate 203
unsubscribe#1
saving 294
subscriber#2 is printing x-coordinate 294
unsubscribe#2
clearing resources

4. Fazit

Die KlasseConnectableObservablehilft dabei, mehrere Abonnenten mit geringem Aufwand zu verwalten.

Die Methoden sehen ähnlich aus, ändern jedoch das Verhalten der Abonnenten erheblich, da Feinheiten bei der Implementierung eine Rolle spielen und sogar die Reihenfolge der Methoden eine Rolle spielt.

Den vollständigen Quellcode für alle in diesem Artikel verwendeten Beispiele finden Sie inGitHub project.