Java 9 Реактивные потоки

Реактивные потоки Java 9

1. обзор

В этой статье мы рассмотрим реактивные потоки Java 9. Проще говоря, мы сможем использовать классFlow, который включает в себя основные строительные блоки для построения логики обработки реактивного потока.

Reactive Streams - это стандарт для асинхронной обработки потока с неблокирующим обратным давлением. Эта спецификация определена вReactive Manifesto,, и существуют различные ее реализации, например,RxJava илиAkka-Streams..

2. Обзор реактивного API

Чтобы построитьFlow, мы можем использовать три основные абстракции и скомпоновать их в логику асинхронной обработки.

Every Flow needs to process events that are published to it by a Publisher instance; Publisher имеет один метод -subscribe().

Если какой-либо из подписчиков хочет получать публикуемые им события, им необходимо подписаться на данныйPublisher.

The receiver of messages needs to implement the Subscriber interface. Обычно это конец каждой обработкиFlow, потому что его экземпляр больше не отправляет сообщения.

Мы можем представитьSubscriber какSink.. У него есть четыре метода, которые необходимо переопределить -onSubscribe(), onNext(), onError(), иonComplete().. Мы рассмотрим их в следующем разделе.

If we want to transform incoming message and pass it further to the next Subscriber, we need to implement the Processor interface. Это действует какSubscriber, потому что он получает сообщения, так и какPublisher, потому что он обрабатывает эти сообщения и отправляет их для дальнейшей обработки.

3. Публикация и использование сообщений

Предположим, мы хотим создать простойFlow,, в котором у нас естьPublisher, публикующие сообщения, и простойSubscriber, получающий сообщения по мере их поступления - по одному за раз.

Давайте создадим классEndSubscriber. Нам нужно реализовать интерфейсSubscriber. Затем мы переопределим необходимые методы.

МетодonSubscribe() вызывается перед началом обработки. ЭкземплярSubscription передается в качестве аргумента. Это класс, который используется для управления потоком сообщений междуSubscriber иPublisher:.

public class EndSubscriber implements Subscriber {
    private Subscription subscription;
    public List consumedElements = new LinkedList<>();

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }
}

Мы также инициализировали пустойList изconsumedElements, который будет использоваться в тестах.

Теперь нам нужно реализовать оставшиеся методы из интерфейсаSubscriber. Главный метод здесь - onNext () - он вызывается всякий раз, когдаPublisher публикует новое сообщение:

@Override
public void onNext(T item) {
    System.out.println("Got : " + item);
    subscription.request(1);
}

Обратите внимание, что когда мы начали подписку в методеonSubscribe() и когда мы обработали сообщение, нам нужно вызвать методrequest() наSubscription, чтобы сообщить, что текущийSubscriber готов потреблять больше сообщений.

Наконец, нам нужно реализоватьonError() - который вызывается всякий раз, когда при обработке будет выброшено какое-то исключение, а такжеonComplete() –, вызываемый при закрытииPublisher:

@Override
public void onError(Throwable t) {
    t.printStackTrace();
}

@Override
public void onComplete() {
    System.out.println("Done");
}

Давайте напишем тест для ProcessingFlow.. Мы будем использовать классSubmissionPublisher - конструкцию изjava.util.concurrent - которая реализует интерфейсPublisher.

Мы собираемся отправить элементыN вPublisher, которые получит нашEndSubscriber:

@Test
public void whenSubscribeToIt_thenShouldConsumeAll()
  throws InterruptedException {

    // given
    SubmissionPublisher publisher = new SubmissionPublisher<>();
    EndSubscriber subscriber = new EndSubscriber<>();
    publisher.subscribe(subscriber);
    List items = List.of("1", "x", "2", "x", "3", "x");

    // when
    assertThat(publisher.getNumberOfSubscribers()).isEqualTo(1);
    items.forEach(publisher::submit);
    publisher.close();

    // then
     await().atMost(1000, TimeUnit.MILLISECONDS)
       .until(
         () -> assertThat(subscriber.consumedElements)
         .containsExactlyElementsOf(items)
     );
}

Обратите внимание, что мы вызываем методclose() в экземпляреEndSubscriber.. Он будет вызывать обратный вызовonComplete() внизу на каждомSubscriber заданногоPublisher..

Запуск этой программы даст следующий результат:

Got : 1
Got : x
Got : 2
Got : x
Got : 3
Got : x
Done

4. Преобразование сообщений

Допустим, мы хотим построить аналогичную логику междуPublisher иSubscriber, но также применить некоторые преобразования.

Мы создадим классTransformProcessor, который реализуетProcessor и расширяетSubmissionPublisher –, поскольку это будет какPublisher, так и Subscriber..

Мы передадимFunction, который преобразует входные данные в выходные данные:

public class TransformProcessor
  extends SubmissionPublisher
  implements Flow.Processor {

    private Function function;
    private Flow.Subscription subscription;

    public TransformProcessor(Function function) {
        super();
        this.function = function;
    }

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(T item) {
        submit(function.apply(item));
        subscription.request(1);
    }

    @Override
    public void onError(Throwable t) {
        t.printStackTrace();
    }

    @Override
    public void onComplete() {
        close();
    }
}

Давайте теперьwrite a quick test с потоком обработки, в которомPublisher публикует элементыString.

НашTransformProcessor будет анализироватьString какInteger - это означает, что здесь должно произойти преобразование:

@Test
public void whenSubscribeAndTransformElements_thenShouldConsumeAll()
  throws InterruptedException {

    // given
    SubmissionPublisher publisher = new SubmissionPublisher<>();
    TransformProcessor transformProcessor
      = new TransformProcessor<>(Integer::parseInt);
    EndSubscriber subscriber = new EndSubscriber<>();
    List items = List.of("1", "2", "3");
    List expectedResult = List.of(1, 2, 3);

    // when
    publisher.subscribe(transformProcessor);
    transformProcessor.subscribe(subscriber);
    items.forEach(publisher::submit);
    publisher.close();

    // then
     await().atMost(1000, TimeUnit.MILLISECONDS)
       .until(() ->
         assertThat(subscriber.consumedElements)
         .containsExactlyElementsOf(expectedResult)
     );
}

Обратите внимание, что вызов методаclose() на основеPublisher вызовет вызов методаonComplete() наTransformProcessor.

Имейте в виду, что все издатели в цепочке обработки должны быть закрыты таким образом.

5. Управление спросом на сообщения с помощьюSubscription

Допустим, мы хотим использовать только первый элемент из подписки, применить некоторую логику и завершить обработку. Для этого мы можем использовать методrequest().

Давайте изменим нашEndSubscriber, чтобы он потреблял только N сообщений. Мы передадим это число в качестве аргумента конструктораhowMuchMessagesConsume:

public class EndSubscriber implements Subscriber {

    private AtomicInteger howMuchMessagesConsume;
    private Subscription subscription;
    public List consumedElements = new LinkedList<>();

    public EndSubscriber(Integer howMuchMessagesConsume) {
        this.howMuchMessagesConsume
          = new AtomicInteger(howMuchMessagesConsume);
    }

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(T item) {
        howMuchMessagesConsume.decrementAndGet();
        System.out.println("Got : " + item);
        consumedElements.add(item);
        if (howMuchMessagesConsume.get() > 0) {
            subscription.request(1);
        }
    }
    //...

}

Мы можем запрашивать элементы столько, сколько хотим.

Давайте напишем тест, в котором мы хотим использовать только один элемент из заданногоSubscription:

@Test
public void whenRequestForOnlyOneElement_thenShouldConsumeOne()
  throws InterruptedException {

    // given
    SubmissionPublisher publisher = new SubmissionPublisher<>();
    EndSubscriber subscriber = new EndSubscriber<>(1);
    publisher.subscribe(subscriber);
    List items = List.of("1", "x", "2", "x", "3", "x");
    List expected = List.of("1");

    // when
    assertThat(publisher.getNumberOfSubscribers()).isEqualTo(1);
    items.forEach(publisher::submit);
    publisher.close();

    // then
    await().atMost(1000, TimeUnit.MILLISECONDS)
      .until(() ->
        assertThat(subscriber.consumedElements)
       .containsExactlyElementsOf(expected)
    );
}

Хотяpublisher публикует шесть элементов, нашEndSubscriber будет потреблять только один элемент, потому что он сигнализирует о необходимости обработки только этого единственного элемента.

Используя методrequest() дляSubscription,, мы можем реализовать более сложный механизм противодавления для управления скоростью потребления сообщения.

6. Заключение

В этой статье мы рассмотрели реактивные потоки Java 9.

Мы увидели, как создать обработкуFlow, состоящую изPublisher иSubscriber.. Мы создали более сложный поток обработки с преобразованием элементов с помощьюProcessors.

Наконец, мы использовалиSubscription для управления спросом на элементы с помощьюSubscriber.

Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.