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á.