Criando programaticamente seqüências com o Project Reactor

Criando programaticamente seqüências com o Project Reactor

*1. Visão geral *

Neste tutorial, usaremos https://www..com/reactor-core [fundamentos do Project Reactor] para aprender algumas técnicas para criar https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html [ Fluxes].

===* 2. Dependências do Maven *

Vamos começar com algumas dependências. Precisamos de https://search.maven.org/search? Q = g: io.projectreactor% 20AND% 20a: núcleo do reator & núcleo = gav [núcleo do reator] e https://search.maven.org/search ? q = g: io.projectreactor% 20AND% 20a: teste do reator [reactor-test]:

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <version>3.2.6.RELEASE</version>
    <scope>test</scope>
</dependency>

===* 3. Emissão síncrona *

A maneira mais simples de criar um Flux é Flux # generate.* Este método depende de uma função de gerador para produzir uma sequência de itens. *

Mas primeiro, vamos definir uma classe para conter nossos métodos ilustrando o método generate:

public class SequenceGenerator {
   //methods that will follow
}

====* 3.1 Gerador com novos estados *

Vamos ver como podemos gerar a Fibonacci sequence com o Reactor:

public Flux<Integer> generateFibonacciWithTuples() {
    return Flux.generate(
            () -> Tuples.of(0, 1),
            (state, sink) -> {
                sink.next(state.getT1());
                return Tuples.of(state.getT2(), state.getT1() + state.getT2());
            }
    );
}

Não é difícil ver este https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#generate-java.util.concurrent.Callable-java.util.function.BiFunction- O método [generate] assume duas funções como argumentos - um Callable e um BiFunction:

  • A função Callable configura o estado inicial do gerador - nesse caso, é um Tuples com os elementos 0 e 1 *A função BiFuntion é um gerador, consumindo um SynchronousSink, emitindo um item em cada rodada com o método next do coletor e o estado atual

Como o próprio nome sugere, um objeto SynchronousSink funciona de forma síncrona. No entanto,* observe que não podemos chamar o método next deste objeto mais de uma vez por chamada de gerador. *

Vamos verificar a sequência gerada com https://www..com/reactive-streams-step-verifier-test-publisher [StepVerifier]:

@Test
public void whenGeneratingNumbersWithTuplesState_thenFibonacciSequenceIsProduced() {
    SequenceGenerator sequenceGenerator = new SequenceGenerator();
    Flux<Integer> fibonacciFlux = sequenceGenerator.generateFibonacciWithTuples().take(5);

    StepVerifier.create(fibonacciFlux)
      .expectNext(0, 1, 1, 2, 3)
      .expectComplete()
      .verify();
}

Neste exemplo, o assinante solicita apenas cinco itens, portanto, a sequência gerada termina com o número 3.

Como podemos ver,* o gerador retorna um novo objeto de estado a ser usado na próxima passagem. Porém, não é necessário fazê-lo. *Em vez disso, podemos reutilizar uma instância de estado para todas as invocações do gerador.

====* 3.2 Gerador com estado mutável *

Suponha que desejamos gerar a sequência de Fibonacci com um estado reciclado. Para demonstrar esse caso de uso, vamos primeiro definir uma classe:

public class FibonacciState {
    private int former;
    private int latter;

   //constructor, getters and setters
}

Usaremos uma instância dessa classe para manter o estado do gerador. As duas propriedades desta instância, former e latter, armazenam dois números consecutivos na sequência.

Se modificarmos nosso exemplo inicial, agora usaremos o estado mutável com generate:

public Flux<Integer> generateFibonacciWithCustomClass(int limit) {
    return Flux.generate(
      () -> new FibonacciState(0, 1),
      (state, sink) -> {
        sink.next(state.getFormer());
        if (state.getLatter() > limit) {
            sink.complete();
        }
        int temp = state.getFormer();
        state.setFormer(state.getLatter());
        state.setLatter(temp + state.getLatter());
        return state;
    });
}

Semelhante ao exemplo anterior, este https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#generate-java.util.concurrent.Callable-java.util.function.BiFunction -java.util.function.Consumer - [_ generate_ variant] possui parâmetros de fornecedor e gerador de estado.

O fornecedor de estado do tipo Callable simplesmente cria um objeto FibonacciState com as propriedades iniciais de 0 e 1. Esse objeto de estado será reutilizado durante todo o ciclo de vida do gerador.

Assim como o SynchronousSink no exemplo Fibonacci-with-Tuples, o coletor aqui produz itens um por um. No entanto, diferente do exemplo,* o gerador retorna o mesmo objeto de estado toda vez que é chamado. *

Observe também desta vez,* para evitar uma sequência infinita, *instruímos o coletor a concluir quando o valor produzido atingir um limite.

E, novamente, vamos fazer um teste rápido para confirmar se funciona:

@Test
public void whenGeneratingNumbersWithCustomClass_thenFibonacciSequenceIsProduced() {
    SequenceGenerator sequenceGenerator = new SequenceGenerator();

    StepVerifier.create(sequenceGenerator.generateFibonacciWithCustomClass(10))
      .expectNext(0, 1, 1, 2, 3, 5, 8)
      .expectComplete()
      .verify();
}

====* 3.3 Variante sem estado *

O método generate possui outra outra variante com apenas um parâmetro do tipo _Consumidor <SynchronousSink> _. Essa variante é adequada apenas para produzir uma sequência predeterminada, portanto, não é tão poderosa. Nós não vamos cobrir isso em detalhes, então.

===* 4. Emissão assíncrona *

A emissão síncrona não é a única solução para a criação programática de um Flux.

Em vez disso,* podemos usar os operadores create e push para produzir vários itens em uma rodada de emissão de maneira assíncrona. *

====* 4.1 O método create *

  • Usando o create, podemos produzir itens de vários threads. *Neste exemplo, coletaremos elementos de duas fontes diferentes em uma sequência.

Primeiro, vamos ver como create é um pouco diferente de generate:

public class SequenceCreator {
    public Consumer<List<Integer>> consumer;

    public Flux<Integer> createNumberSequence() {
        return Flux.create(sink -> SequenceCreator.this.consumer = items -> items.forEach(sink::next));
    }
}

Diferentemente do operador generate,* o método create não mantém um estado. E, em vez de gerar itens por si só, o emissor passado para esse método recebe elementos de uma fonte externa. *

Além disso, podemos ver que o operador create solicita um FluxSink em vez de um SynchronousSink. Com um FluxSink,* podemos chamar next () quantas vezes for necessário. *

No nosso caso, chamaremos next () _ para cada item que temos na lista de _items, emitindo cada um por um. Vamos ver como preencher items em apenas um momento.

Nossa fonte externa, nesse caso, é um campo imaginário consumer, embora isso possa ser uma API observável.

Vamos colocar o operador create em ação, começando com duas sequências de números:

@Test
public void whenCreatingNumbers_thenSequenceIsProducedAsynchronously() throws InterruptedException {
    SequenceGenerator sequenceGenerator = new SequenceGenerator();
    List<Integer> sequence1 = sequenceGenerator.generateFibonacciWithTuples().take(3).collectList().block();
    List<Integer> sequence2 = sequenceGenerator.generateFibonacciWithTuples().take(4).collectList().block();

   //other statements described below
}

Essas seqüências, sequence1 e sequence2, servirão como fontes de itens para a sequência gerada.

A seguir, vêm dois objetos Thread que colocarão elementos no editor:

SequenceCreator sequenceCreator = new SequenceCreator();
Thread producingThread1 = new Thread(
  () -> sequenceCreator.consumer.accept(sequence1)
);
Thread producingThread2 = new Thread(
  () -> sequenceCreator.consumer.accept(sequence2)
);

Quando o operador accept é chamado, os elementos começam a fluir para a origem da sequência.

E então, podemos ouvir, ou assinar, nossa nova sequência consolidada:

List<Integer> consolidated = new ArrayList<>();
sequenceCreator.createNumberSequence().subscribe(consolidated::add);

Ao assinar nossa sequência, indicamos o que deve acontecer com cada item emitido pela sequência. Aqui, é para adicionar cada item de fontes diferentes a uma lista consolidada.

Agora, acionamos todo o processo que exibe itens movendo-se em dois segmentos diferentes:

producingThread1.start();
producingThread2.start();
producingThread1.join();
producingThread2.join();

Como sempre, a última etapa é verificar o resultado da operação:

assertThat(consolidated).containsExactlyInAnyOrder(0, 1, 1, 0, 1, 1, 2);

Os três primeiros números na sequência recebida vêm de sequence1, enquanto os últimos quatro de sequence2. Devido à natureza das operações assíncronas, a ordem dos elementos dessas seqüências não é garantida.

O método create possui https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#create-java.util.function.Consumer-reactor.core.publisher.FluxSink.OverflowStrategy- [outra variante], usando um argumento do tipo _https://projectreactor.io/docs/core/release/api/reactor/core/publisher/FluxSink.OverflowStrategy.html [OverflowStrategy] _. Como o próprio nome indica, esse argumento gerencia a contrapressão quando o downstream não consegue acompanhar o editor.* Por padrão, o editor armazena em buffer todos os elementos nesse caso. *

====* 4.2 O método push *

Além do operador create, a classe Flux possui outro método estático para emitir uma sequência de forma assíncrona, ou seja, https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#push-java .util.function.Consumer - [_ push_]. Esse método funciona exatamente como criar,* exceto que permite que apenas um encadeamento produtor emita sinais de cada vez. *

Poderíamos substituir o método create no exemplo que acabamos de usar com push, e o código ainda será compilado

No entanto, algumas vezes veríamos um erro de asserção, pois o operador push impede que FluxSink # next seja chamado simultaneamente em diferentes threads. Como resultado,* devemos usar push apenas se não pretendermos usar vários threads. *

===* 5. Sequências de manipulação *

Todos os métodos que vimos até agora são estáticos e permitem a criação de uma sequência a partir de uma determinada fonte.* A API Flux também fornece um método de instância chamado _https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#handle-java.util.function.BiConsumer- [handle] _, para manipular uma sequência produzida por um editor. *

Esse operador handle assume uma sequência, processando e possivelmente removendo alguns elementos. Nesse sentido,* podemos dizer que o operador handle funciona como um map e um _filter _. *

Vamos dar uma olhada em uma ilustração simples do método handle:

public class SequenceHandler {
    public Flux<Integer> handleIntegerSequence(Flux<Integer> sequence) {
        return sequence.handle((number, sink) -> {
            if (number % 2 == 0) {
                sink.next(number/2);
            }
        });
    }
}

Neste exemplo, o operador handle pega uma sequência de números, dividindo o valor por 2, se for o caso. Caso o valor seja um número ímpar, o operador não fará nada, o que significa que esse número é ignorado.

Outra coisa a se notar é que, como no método generate,* handle emprega um SynchronousSink e permite apenas emissões uma a uma. *

E, finalmente, precisamos testar as coisas. Vamos usar StepVerifier uma última vez para confirmar que nosso manipulador funciona:

@Test
public void whenHandlingNumbers_thenSequenceIsMappedAndFiltered() {
    SequenceHandler sequenceHandler = new SequenceHandler();
    SequenceGenerator sequenceGenerator = new SequenceGenerator();
    Flux<Integer> sequence = sequenceGenerator.generateFibonacciWithTuples().take(10);

    StepVerifier.create(sequenceHandler.handleIntegerSequence(sequence))
      .expectNext(0, 1, 4, 17)
      .expectComplete()
      .verify();
}

Existem quatro números pares entre os 10 primeiros itens na sequência de Fibonacci: 0, 2, 8 e 34, portanto, os argumentos que passamos para o método expectNext.

===* 6. Conclusão*

Neste artigo, percorremos vários métodos da API Flux que podem ser usados ​​para produzir uma sequência de maneira programática, principalmente os operadores generate e create.

O código fonte deste tutorial está disponível over no GitHub. Este é um projeto do Maven e deve poder ser executado como está.