RxJava et gestion des erreurs

1. Introduction

Dans cet article, nous verrons comment gérer les exceptions et les erreurs à l’aide de RxJava.

Tout d’abord, gardez à l’esprit que le Observable ne génère généralement pas d’exceptions. Par défaut, Observable appelle la méthode onError () de son observateur, notifiant à l’observateur qu’une erreur irrécupérable vient de se produire, puis se ferme sans invoquer d’autres méthodes de son observateur .

  • Les opérateurs de traitement des erreurs que nous sommes sur le point d’introduire modifient le comportement par défaut en reprenant ou en réessayant la séquence Observable .

2. Dépendances Maven

Premièrement, ajoutons le RxJava dans le pom.xml :

<dependency>
    <groupId>io.reactivex.rxjava2</groupId>
    <artifactId>rxjava</artifactId>
    <version>2.1.3</version>
</dependency>

La dernière version de l’artefact est disponible à l’adresse ici .

3. La gestion des erreurs

Lorsqu’une erreur se produit, nous devons généralement la gérer d’une manière ou d’une autre. Par exemple, modifiez les états externes associés en reprenant la séquence avec les résultats par défaut ou laissez-le simplement de manière à ce que l’erreur puisse se propager.

3.1. Action sur erreur

Avec doOnError , nous pouvons invoquer n’importe quelle action nécessaire lorsque cela se produit. une erreur:

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

En cas d’exception levée lors de l’exécution de l’action, RxJava englobe l’exception dans une 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. Reprendre avec les éléments par défaut

Bien que nous puissions appeler des actions avec doOnError , mais l’erreur interrompt toujours le flux de séquence standard. Parfois, nous voulons reprendre la séquence avec une option par défaut, c’est ce que 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");
}

Si vous préférez un fournisseur d’éléments dynamique par défaut, vous pouvez utiliser le 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. Reprendre avec une autre séquence

Au lieu de nous rabattre sur un seul élément, nous pouvons fournir une séquence de données de repli en utilisant http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#onErrorResumeNext-io.reactivex.ObservableSource- Resume- ErrorResumeNext i]en rencontrant des erreurs. Cela aiderait à prévenir la propagation des erreurs:

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

Si la séquence de secours diffère en fonction des types d’exception spécifiques ou si la séquence doit être générée par une fonction, vous pouvez la transmettre à 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. Exception de poignée Seulement

RxJava fournit également une méthode de secours qui permet de continuer la séquence avec un Observable fourni lorsqu’une exception (mais aucune erreur) est déclenchée:

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

Comme le montre le code ci-dessus, lorsqu’une erreur survient, onExceptionResumeNext n’intervient pas pour reprendre la séquence.

4. Réessayer en cas d’erreur

La séquence normale peut être interrompue par une défaillance temporaire du système ou une erreur d’arrière-plan. Dans ces situations, nous souhaitons réessayer et attendre que la séquence soit corrigée.

Heureusement, RxJava nous donne des options pour effectuer exactement cela.

4.1. Réessayez

En utilisant http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#retry-- _, , le Observable_ sera ré-abonné une infinité de fois jusqu’à ce qu’il n’y ait plus d’erreur. Mais la plupart du temps, nous préférerions un nombre fixe de tentatives:

@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. Réessayer sur condition

Une nouvelle tentative conditionnelle est également possible dans RxJava, à l’aide de retry avec prédicats ou en utilisant retryUntil :

@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

Au-delà de ces options de base, il existe également une méthode de tentative intéressante:

retryWhen .

Cela retourne un Observable, say “NewO”, qui émet les mêmes valeurs que la source ObservableSource , par exemple “OldO”, mais si le Observable “NewO” renvoyé appelle onComplete ou onError , le onComplete ou onError de l’abonné sera invoqué.

Et si «NewO» émet un élément, une réinscription à la source ObservableSource «OldO» sera déclenchée.

Les tests ci-dessous montrent comment cela fonctionne:

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

Une utilisation typique de retryWhen est un nombre limité de tentatives avec des retards variables:

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

Notez que cette logique tente trois fois et retarde chaque tentative de manière

5. Résumé

Dans cet article, nous avons présenté un certain nombre de méthodes de gestion des erreurs et des exceptions dans RxJava.

Il existe également plusieurs exceptions spécifiques à RxJava concernant la gestion des erreurs - consultez https://github.com/ReactiveX/RxJava/wiki/Error-Handling#rxjava-specific-exceptions-and-what-to-do-do-about -them[le wiki officiel]pour plus de détails.

Comme toujours, l’implémentation complète est disponible sur over sur Github .