RxJavaとエラー処理

RxJavaとエラー処理

1. 前書き

この記事では、RxJavaを使用して例外とエラーを処理する方法を見ていきます。

まず、Observableは通常例外をスローしないことに注意してください。 代わりに、デフォルトでは、ObservableObserver’s onError()メソッドを呼び出し、回復不能なエラーが発生したことをオブザーバーに通知してから、Observer’sメソッドをそれ以上呼び出さずに終了します。

導入しようとしているエラー処理演算子は、Observableシーケンスを再開または再試行することにより、デフォルトの動作を変更します。

2. Mavenの依存関係

まず、pom.xmlにRxJavaを追加しましょう。


    io.reactivex.rxjava2
    rxjava
    2.1.3

アーティファクトの最新バージョンはhereにあります。

3. エラー処理

エラーが発生した場合、通常は何らかの方法で処理する必要があります。 たとえば、関連する外部状態を変更し、デフォルトの結果でシーケンスを再開するか、エラーを伝播できるようにそのままにしておきます。

3.1. エラー時のアクション

doOnErrorを使用すると、エラーが発生したときに必要なアクションを呼び出すことができます。

@Test
public void whenChangeStateOnError_thenErrorThrown() {
    TestObserver testObserver = new TestObserver();
    AtomicBoolean state = new AtomicBoolean(false);
    Observable
      .error(UNKNOWN_ERROR)
      .doOnError(throwable -> state.set(true))
      .subscribe(testObserver);

    testObserver.assertError(UNKNOWN_ERROR);
    testObserver.assertNotComplete();
    testObserver.assertNoValues();

    assertTrue("state should be changed", state.get());
}

アクションの実行中に例外がスローされた場合、RxJavaは例外をCompositeExceptionでラップします。

@Test
public void whenExceptionOccurOnError_thenCompositeExceptionThrown() {
    TestObserver testObserver = new TestObserver();
    Observable
      .error(UNKNOWN_ERROR)
      .doOnError(throwable -> {
          throw new RuntimeException("unexcepted");
      })
      .subscribe(testObserver);

    testObserver.assertError(CompositeException.class);
    testObserver.assertNotComplete();
    testObserver.assertNoValues();
}

3.2. デフォルトのアイテムで再開

doOnErrorを使用してアクションを呼び出すことはできますが、エラーによって標準のシーケンスフローが中断されます。 デフォルトのオプションを使用してシーケンスを再開したい場合があります。これは、onErrorReturnItemが行うことです。

@Test
public void whenHandleOnErrorResumeItem_thenResumed(){
    TestObserver testObserver = new TestObserver();
    Observable
      .error(UNKNOWN_ERROR)
      .onErrorReturnItem("singleValue")
      .subscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertValueCount(1);
    testObserver.assertValue("singleValue");
}

動的なデフォルトのアイテムサプライヤが優先される場合は、onErrorReturnを使用できます。

@Test
public void whenHandleOnErrorReturn_thenResumed() {
    TestObserver testObserver = new TestObserver();
    Observable
      .error(UNKNOWN_ERROR)
      .onErrorReturn(Throwable::getMessage)
      .subscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertValueCount(1);
    testObserver.assertValue("unknown error");
}

3.3. 別のシーケンスで再開

単一のアイテムにフォールバックする代わりに、エラーが発生したときにonErrorResumeNextを使用してフォールバックデータシーケンスを提供する場合があります。 これにより、エラーの伝播を防ぐことができます。

@Test
public void whenHandleOnErrorResume_thenResumed() {
    TestObserver testObserver = new TestObserver();
    Observable
      .error(UNKNOWN_ERROR)
      .onErrorResumeNext(Observable.just("one", "two"))
      .subscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertValueCount(2);
    testObserver.assertValues("one", "two");
}

フォールバックシーケンスが特定の例外タイプによって異なる場合、またはシーケンスを関数で生成する必要がある場合は、関数をonErrorResumeNext:に渡すことができます。

@Test
public void whenHandleOnErrorResumeFunc_thenResumed() {
    TestObserver testObserver = new TestObserver();
    Observable
      .error(UNKNOWN_ERROR)
      .onErrorResumeNext(throwable -> Observable
        .just(throwable.getMessage(), "nextValue"))
      .subscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertValueCount(2);
    testObserver.assertValues("unknown error", "nextValue");
}

3.4. 例外を処理する Only

RxJavaは、例外(ただしエラーなし)が発生したときに、指定されたObservableでシーケンスを続行できるようにするフォールバックメソッドも提供します。

@Test
public void whenHandleOnException_thenResumed() {
    TestObserver testObserver = new TestObserver();
    Observable
      .error(UNKNOWN_EXCEPTION)
      .onExceptionResumeNext(Observable.just("exceptionResumed"))
      .subscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertValueCount(1);
    testObserver.assertValue("exceptionResumed");
}

@Test
public void whenHandleOnException_thenNotResumed() {
    TestObserver testObserver = new TestObserver();
    Observable
      .error(UNKNOWN_ERROR)
      .onExceptionResumeNext(Observable.just("exceptionResumed"))
      .subscribe(testObserver);

    testObserver.assertError(UNKNOWN_ERROR);
    testObserver.assertNotComplete();
}

上記のコードが示すように、エラーが発生した場合、onExceptionResumeNextはシーケンスを再開するために開始されません。

4. エラー時に再試行

一時的なシステム障害またはバックエンドエラーにより、通常のシーケンスが中断される場合があります。 これらの状況では、シーケンスが修正されるまで再試行して待機します。

幸いなことに、RxJavaはまさにそれを実行するオプションを提供します。

4.1. リトライ

http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#retry--,を使用すると、エラーが発生しなくなるまで、Observableが無限に再サブスクライブされます。 ただし、ほとんどの場合、一定の再試行回数を使用します。

@Test
public void whenRetryOnError_thenRetryConfirmed() {
    TestObserver testObserver = new TestObserver();
    AtomicInteger atomicCounter = new AtomicInteger(0);
    Observable
      .error(() -> {
          atomicCounter.incrementAndGet();
          return UNKNOWN_ERROR;
      })
      .retry(1)
      .subscribe(testObserver);

    testObserver.assertError(UNKNOWN_ERROR);
    testObserver.assertNotComplete();
    testObserver.assertNoValues();
    assertTrue("should try twice", atomicCounter.get() == 2);
}

4.2. 条件付きで再試行

条件付き再試行は、retry with predicatesまたはretryUntilを使用して、RxJavaでも実行可能です。

@Test
public void whenRetryConditionallyOnError_thenRetryConfirmed() {
    TestObserver testObserver = new TestObserver();
    AtomicInteger atomicCounter = new AtomicInteger(0);
    Observable
      .error(() -> {
          atomicCounter.incrementAndGet();
          return UNKNOWN_ERROR;
      })
      .retry((integer, throwable) -> integer < 4)
      .subscribe(testObserver);

    testObserver.assertError(UNKNOWN_ERROR);
    testObserver.assertNotComplete();
    testObserver.assertNoValues();
    assertTrue("should call 4 times", atomicCounter.get() == 4);
}

@Test
public void whenRetryUntilOnError_thenRetryConfirmed() {
    TestObserver testObserver = new TestObserver();
    AtomicInteger atomicCounter = new AtomicInteger(0);
    Observable
      .error(UNKNOWN_ERROR)
      .retryUntil(() -> atomicCounter.incrementAndGet() > 3)
      .subscribe(testObserver);
    testObserver.assertError(UNKNOWN_ERROR);
    testObserver.assertNotComplete();
    testObserver.assertNoValues();
    assertTrue("should call 4 times", atomicCounter.get() == 4);
}

4.3. RetryWhen

これらの基本的なオプションに加えて、興味深い再試行方法もあります:retryWhen

これは、ソースObservableSource、たとえば“OldO”,と同じ値を出力するObservable,、たとえば“NewO”,を返しますが、返されたObservableの場合、「NewO」は%(t5 )sまたはonErrorの場合、サブスクライバーのonCompleteまたはonErrorが呼び出されます。

また、“NewO”がアイテムを放出すると、ソースObservableSource“OldO”への再サブスクリプションがトリガーされます。

以下のテストは、これがどのように機能するかを示しています。

@Test
public void whenRetryWhenOnError_thenRetryConfirmed() {
    TestObserver testObserver = new TestObserver();
    Exception noretryException = new Exception("don't retry");
    Observable
      .error(UNKNOWN_ERROR)
      .retryWhen(throwableObservable -> Observable.error(noretryException))
      .subscribe(testObserver);

    testObserver.assertError(noretryException);
    testObserver.assertNotComplete();
    testObserver.assertNoValues();
}

@Test
public void whenRetryWhenOnError_thenCompleted() {
    TestObserver testObserver = new TestObserver();
    AtomicInteger atomicCounter = new AtomicInteger(0);
    Observable
      .error(() -> {
        atomicCounter.incrementAndGet();
        return UNKNOWN_ERROR;
      })
      .retryWhen(throwableObservable -> Observable.empty())
      .subscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertNoValues();
    assertTrue("should not retry", atomicCounter.get()==0);
}

@Test
public void whenRetryWhenOnError_thenResubscribed() {
    TestObserver testObserver = new TestObserver();
    AtomicInteger atomicCounter = new AtomicInteger(0);
    Observable
      .error(() -> {
        atomicCounter.incrementAndGet();
        return UNKNOWN_ERROR;
      })
      .retryWhen(throwableObservable -> Observable.just("anything"))
      .subscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertNoValues();
    assertTrue("should retry once", atomicCounter.get()==1);
}

retryWhenの一般的な使用法は、可変遅延を伴う制限付き再試行です。

@Test
public void whenRetryWhenForMultipleTimesOnError_thenResumed() {
    TestObserver testObserver = new TestObserver();
    long before = System.currentTimeMillis();
    Observable
      .error(UNKNOWN_ERROR)
      .retryWhen(throwableObservable -> throwableObservable
        .zipWith(Observable.range(1, 3), (throwable, integer) -> integer)
        .flatMap(integer -> Observable.timer(integer, TimeUnit.SECONDS)))
      .blockingSubscribe(testObserver);

    testObserver.assertNoErrors();
    testObserver.assertComplete();
    testObserver.assertNoValues();
    long secondsElapsed = (System.currentTimeMillis() - before)/1000;
    assertTrue("6 seconds should elapse",secondsElapsed == 6 );
}

このロジックが3回再試行し、各再試行が徐々に遅延することに注意してください。

5. 概要

この記事では、RxJavaでエラーと例外を処理するいくつかの方法を紹介しました。

エラー処理に関連するRxJava固有の例外もいくつかあります。詳細については、the official wikiを参照してください。

いつものように、完全な実装はover on Githubで見つけることができます。