RxJava 1つの観測可能な複数の購読者

RxJava One Observable、複数のサブスクライバー

1. 概要

複数のサブスクライバーのデフォルトの動作が常に望ましいとは限りません。 この記事では、この動作を変更し、複数のサブスクライバーを適切な方法で処理する方法について説明します。

ただし、最初に、複数のサブスクライバーのデフォルトの動作を見てみましょう。

2. デフォルトの動作

次のObservableがあるとします。

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

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

これにより、Subscribersがサブスクライブするとすぐに2つの要素が発行されます。

この例では、2つのSubscribersがあります。

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

各要素を取得するのはコストのかかる操作であると想像してください。たとえば、集中的な計算やURL接続のオープンが含まれる場合があります。

簡単にするために、数値を返します。

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

これが出力です:

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

ご覧のとおり、getting each element as well as clearing the resources is performed twice by defaultSubscriberごとに1回。 これは私たちが望んでいることではありません。 ConnectableObservableクラスは、問題の修正に役立ちます。

3. ConnectableObservable

ConnectableObservableクラスを使用すると、サブスクリプションを複数のサブスクライバーと共有でき、基になる操作を複数回実行することはできません。

ただし、最初に、ConnectableObservableを作成しましょう。

3.1. publish()

publish()メソッドは、ObservableからConnectableObservableを作成するものです。

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

しかし、今のところは何もしません。 それを機能させるのはconnect()メソッドです。

3.2. connect()

一部のサブスクライバーが存在する場合でも、Until ConnectableObservable‘s connect() method isn’t called Observable‘s onSubcribe() callback isn’t triggered

これをデモンストレーションしましょう:

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

サブスクライブしてから、1秒待ってから接続します。 出力は以下のとおりです。

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

ご覧のとおり:

    • 要素の取得は、必要に応じて1回だけ発生します

    • クリアリソースは、同様に1回だけ

    • 要素を取得すると、加入後の第二を開始します。

    • サブスクライブしても、要素の放出はトリガーされなくなりました。 connect()のみがこれを行います

この遅延は有益である可能性があります。1つの購読者が別の購読者よりも早く購読している場合でも、すべての購読者に同じ要素のシーケンスを提供する必要がある場合があります。

3.3. オブザーバブルの一貫したビュー–subscribe()後のconnect()

このユースケースは、以前のObservableで実行され、両方のサブスクライバーが要素のシーケンス全体を取得するため、実証できません。

代わりに、放出する要素がサブスクリプションの瞬間、たとえばマウスクリックで発生するイベントに依存しないことを想像してください。 ここで、2番目のSubscriberが最初の1秒後にサブスクライブするとします。

最初のSubscriberは、この例で放出されたすべての要素を取得しますが、2番目のSubscriberは、一部の要素のみを受信します。

一方、適切な場所でconnect()メソッドを使用すると、両方のサブスクライバーにObservableシーケンスに関する同じビューを与えることができます。

ホットObservableの例

ホットなObservableを作成しましょう。 JFrameでのマウスクリックで要素を放出します。

各要素はクリックのx座標になります。

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

ホットObservableのデフォルトの動作

ここで、2番目の間隔で2つのSubscribersを次々にサブスクライブし、プログラムを実行してクリックを開始すると、最初のSubscriberがより多くの要素を取得することがわかります。

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()subscribe()

両方のサブスクライバーが同じシーケンスを取得できるようにするには、このObservableConnectableObservableに変換し、サブスクリプションの後に両方のSubscribersを呼び出します。

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

今、彼らは同じシーケンスを取得します:

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

したがって、ポイントは、すべてのサブスクライバーの準備が整う瞬間を待ってから、connect()を呼び出すことです。

Springアプリケーションでは、たとえば、アプリケーションの起動時にすべてのコンポーネントをサブスクライブし、onApplicationEvent()connect()を呼び出すことがあります。

しかし、例に戻りましょう。 connect()メソッドの前のすべてのクリックが失われることに注意してください。 要素を見逃したくないが、逆にそれらを処理する場合は、コードの前にconnect()を配置し、Subscriberがない場合にObservableにイベントを生成させることができます。

3.4. Subscriberがない場合のサブスクリプションの強制–subscribe()の前のconnect()

これを実証するために、例を修正しましょう。

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

手順は比較的簡単です。

  • まず、接続します

  • 次に、1秒間待って、最初のSubscriberをサブスクライブします。

  • 最後に、もう1秒待って、2番目のSubscriberをサブスクライブします。

doOnNext()演算子を追加したことに注意してください。 ここでは、たとえばデータベースに要素を保存できますが、コードでは「saving…」と出力します。

コードを起動してクリックを開始すると、connect()呼び出しの直後に要素が出力され、処理されていることがわかります。

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

サブスクライバーが存在しない場合でも、要素は処理されます。

So the connect() method starts emitting and processing elements regardless of whether someone is subscribedは、要素を消費する空のアクションを持つ人工的なSubscriberがあるかのように。

そして、実際のSubscribersがサブスクライブしている場合、この人工メディエーターは要素をそれらに伝播するだけです。

人工的なSubscriberの購読を解除するには、次のようにします。

s.unsubscribe();

どこで:

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

このメソッドを使用すると、返されるオブジェクトは通常のObservableであり、このメソッドはありませんが、基になるConnectableObservableを使用するため、自分でconnect()を呼び出すことはできません。

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

人工的なSubscriberの登録を解除することもできないことに注意してください。 すべての実際のSubscribersのサブスクライブを解除できますが、人工のSubscriberは引き続きイベントを処理します。

これを理解するために、最後のサブスクライバーがサブスクライブを解除した後、最後に何が起こっているかを見てみましょう。

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

ご覧のとおり、リソースのクリアは行われず、doOnNext()を使用した要素の保存は、2回目の登録解除後も続行されます。 これは、人工的なSubscriberが購読を解除せず、要素を消費し続けることを意味します。

3.6. refCount()

refCount()は、最初のSubscriberがサブスクライブするとすぐに接続も自動的に行われるという点で、autoConnect()に似ています。

autoconnect()とは異なり、最後のSubscriberがサブスクライブを解除すると、切断も自動的に行われます。

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. 結論

ConnectableObservableクラスは、ほとんど労力をかけずに複数のサブスクライバーを処理するのに役立ちます。

そのメソッドは似ていますが、実装の微妙さのためにメソッドの順序さえ重要であるため、サブスクライバーの動作を大きく変えます。

この記事で使用されているすべての例の完全なソースコードは、GitHub projectにあります。