RxJava и обработка ошибок

1. Вступление

В этой статье мы рассмотрим, как обрабатывать исключения и ошибки с помощью RxJava.

Во-первых, имейте в виду, что Observable обычно не генерирует исключения. Вместо этого по умолчанию Observable вызывает метод onOrror () своего Observer, уведомляя наблюдателя о том, что только что произошла неисправимая ошибка, а затем завершает работу, не вызывая больше своих методов Observer’s .

  • Операторы обработки ошибок, которые мы собираемся представить, изменяют поведение по умолчанию, возобновляя или повторяя последовательность Observable . **

2. Зависимости Maven

Во-первых, давайте добавим RxJava в pom.xml :

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

Последнюю версию артефакта можно найти Вот .

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. Обрабатывать исключение Только

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. Повторить при условии

Условная повторная попытка также возможна в RxJava, используя retry с предикатами или используя 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

Помимо этих базовых опций, существует также интересный метод повтора:

retryWhen .

Это возвращает Observable, скажем, «NewO», который излучает те же значения, что и источник ObservableSource , скажем «OldO», но если возвращенное Observable «NewO» вызывает onComplete или 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 );
}

Обратите внимание, как эта логика повторяется три раза и постепенно задерживает каждую попытку.

5. Резюме

В этой статье мы представили несколько способов обработки ошибок и исключений в RxJava.

Есть также несколько специфичных для RxJava исключений, связанных с обработкой ошибок - посмотрите https://github.com/ReactiveX/RxJava/wiki/Error-Handling#rxjava-specific-exceptions-and-what-to-do-about -им[официальная вики]для более подробной информации.

Как всегда, полную реализацию можно найти в over на Github .