RxJava und Fehlerbehandlung

RxJava und Fehlerbehandlung

1. Einführung

In diesem Artikel sehen wir uns an, wie Ausnahmen und Fehler mit RxJava behandelt werden.

Denken Sie zunächst daran, dassObservable normalerweise keine Ausnahmen auslöst. Stattdessen ruftObservable standardmäßig die MethodeObserver’s onError() auf, benachrichtigt den Beobachter, dass gerade ein nicht behebbarer Fehler aufgetreten ist, und wird dann beendet, ohne weitere Methoden vonObserver’saufzurufen.

Die Operatoren zur Fehlerbehandlung, die wir einführen werden, ändern das Standardverhalten, indem sie die SequenzObservablefortsetzen oder erneut versuchen.

2. Maven-Abhängigkeiten

Fügen wir zunächst RxJava inpom.xml hinzu:


    io.reactivex.rxjava2
    rxjava
    2.1.3

Die neueste Version des Artefakts befindet sich inhere.

3. Fehlerbehandlung

Wenn ein Fehler auftritt, müssen wir ihn normalerweise auf irgendeine Weise behandeln. Ändern Sie beispielsweise verwandte externe Zustände, und setzen Sie die Sequenz mit Standardergebnissen fort, oder lassen Sie sie einfach so, dass sich der Fehler ausbreiten kann.

3.1. Aktion bei Fehler

MitdoOnError können wir jede Aktion aufrufen, die bei einem Fehler erforderlich ist:

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

Wenn während der Ausführung der Aktion eine Ausnahme ausgelöst wird, schließt RxJava die Ausnahme inCompositeException:

@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. Mit Standardelementen fortsetzen

Wir können zwar Aktionen mitdoOnError aufrufen, aber der Fehler unterbricht immer noch den Standardsequenzfluss. Manchmal möchten wir die Sequenz mit einer Standardoption fortsetzen. Dies ist, wasonErrorReturnItem tut:

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

Wenn ein dynamischer Standardartikellieferant bevorzugt wird, können wir dieonErrorReturn verwenden:

@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. Mit einer anderen Sequenz fortsetzen

Anstatt auf ein einzelnes Element zurückzugreifen, können wir bei Fehlern eine Fallback-Datensequenz mitonErrorResumeNext angeben. Dies würde dazu beitragen, die Fehlerausbreitung zu verhindern:

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

Wenn sich die Fallback-Sequenz je nach Ausnahmetyp unterscheidet oder die Sequenz von einer Funktion generiert werden muss, können wir die Funktion anonErrorResumeNext: übergeben

@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. Ausnahme behandeln Only

RxJava bietet auch eine Fallback-Methode, mit der die Sequenz mit einem angegebenenObservable fortgesetzt werden kann, wenn eine Ausnahme (aber kein Fehler) ausgelöst wird:

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

Wie der obige Code zeigt, werden im Fehlerfall dieonExceptionResumeNextnicht aktiviert, um die Sequenz fortzusetzen.

4. Bei Fehler erneut versuchen

Die normale Sequenz kann durch einen vorübergehenden Systemfehler oder einen Backend-Fehler unterbrochen werden. In diesen Situationen möchten wir es erneut versuchen und warten, bis die Reihenfolge festgelegt ist.

Glücklicherweise gibt uns RxJava die Möglichkeit, genau das zu tun.

4.1. Wiederholen

Durch Verwendung vonhttp://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#retry--, werdenObservable unendlich oft neu abonniert, bis kein Fehler mehr vorliegt. Meistens bevorzugen wir jedoch eine feste Anzahl von Wiederholungsversuchen:

@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. Wiederholen Sie den Vorgang unter bestimmten Bedingungen

Bedingte Wiederholungsversuche sind auch in RxJava mitretry with predicates oderretryUntil möglich:

@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

Neben diesen grundlegenden Optionen gibt es auch eine interessante Retry-Methode:retryWhen.

Dies gibt einObservable, zurück, sagen wir“NewO”,, das die gleichen Werte wie die QuelleObservableSource ausgibt, sagen wir“OldO”,, aber wenn das zurückgegebeneObservable "NewO"onCompleteaufruft ) s oderonError werden dieonComplete oderonError des Teilnehmers aufgerufen.

Und wenn“NewO” ein Element ausgibt, wird ein erneutes Abonnement der QuelleObservableSource“OldO” ausgelöst.

Die folgenden Tests zeigen, wie dies funktioniert:

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

Eine typische Verwendung vonretryWhen sind begrenzte Wiederholungsversuche mit variablen Verzögerungen:

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

Beachten Sie, wie diese Logik dreimal wiederholt und jede Wiederholung schrittweise verzögert.

5. Zusammenfassung

In diesem Artikel haben wir eine Reihe von Möglichkeiten zur Behandlung von Fehlern und Ausnahmen in RxJava vorgestellt.

Es gibt auch einige RxJava-spezifische Ausnahmen in Bezug auf die Fehlerbehandlung. Weitere Informationen finden Sie unterthe official wiki.

Wie immer kann die vollständige Implementierungover on Github gefunden werden.