Fluxos Reativos Java 9
1. Visão geral
Neste artigo, veremos os Java 9 Reactive Streams. Simplificando, seremos capazes de usar a classeFlow, que inclui os blocos de construção primários para construir a lógica de processamento de fluxo reativo.
Reactive Streams é um padrão para processamento de fluxo assíncrono com contrapressão sem bloqueio. Esta especificação é definida emReactive Manifesto,e existem várias implementações dela, por exemplo,RxJava ouAkka-Streams.
2. Visão geral da API reativa
Para construir umFlow, podemos usar três abstrações principais e compô-las na lógica de processamento assíncrono.
Every Flow needs to process events that are published to it by a Publisher instance; oPublisher tem um método -subscribe().
Se algum dos assinantes deseja receber eventos publicados por ele, ele precisa se inscrever noPublisher. fornecido
The receiver of messages needs to implement the Subscriber interface. Normalmente, este é o fim de cada processamento deFlow porque a instância dele não envia mais mensagens.
Podemos pensar emSubscriber como umSink.. Isso tem quatro métodos que precisam ser substituídos -onSubscribe(), onNext(), onError(),eonComplete(). Veremos esses na próxima seção.
If we want to transform incoming message and pass it further to the next Subscriber, we need to implement the Processor interface. Isso atua comoSubscriber porque recebe mensagens e comoPublisher porque processa essas mensagens e as envia para processamento posterior.
3. Publicação e consumo de mensagens
Digamos que queremos criar umFlow, simples no qual temos umPublisher publicando mensagens e umSubscriber simples consumindo mensagens conforme elas chegam - uma de cada vez.
Vamos criar uma classeEndSubscriber. Precisamos implementar a interfaceSubscriber. A seguir, substituiremos os métodos necessários.
O métodoonSubscribe() é chamado antes do início do processamento. A instância deSubscription é passada como argumento. É uma classe que é usada para controlar o fluxo de mensagens entreSubscriberePublisher:
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);
}
}
Também inicializamos umList vazio deconsumedElements que será utilizado nos testes.
Agora, precisamos implementar os métodos restantes da interfaceSubscriber. O método principal aqui é onNext () - é chamado sempre quePublisher publica uma nova mensagem:
@Override
public void onNext(T item) {
System.out.println("Got : " + item);
subscription.request(1);
}
Observe que quando iniciamos a inscrição no métodoonSubscribe()e processamos uma mensagem, precisamos chamar o métodorequest() noSubscription para sinalizar que oSubscriber atual é pronto para consumir mais mensagens.
Por último, precisamos implementaronError() - que é chamado sempre que alguma exceção for lançada no processamento, bem comoonComplete() – chamado quandoPublisher é fechado:
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done");
}
Vamos escrever um teste para o processamentoFlow.. Estaremos usando a classeSubmissionPublisher - uma construção dejava.util.concurrent - que implementa a interfacePublisher.
Vamos enviarN elementos paraPublisher - que nossoEndSubscriber receberá:
@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)
);
}
Observe que estamos chamando o métodoclose() na instância doEndSubscriber.. Ele invocaráonComplete() callback abaixo de cadaSubscriber doPublisher. fornecido
A execução desse programa produzirá a seguinte saída:
Got : 1
Got : x
Got : 2
Got : x
Got : 3
Got : x
Done
4. Transformação de Mensagens
Digamos que queremos construir uma lógica semelhante entre aPublisher eSubscriber, mas também aplicar alguma transformação.
Vamos criar a classeTransformProcessor que implementaProcessore estendeSubmissionPublisher –, pois seráPublishere Subscriber.
Vamos passar umFunction que transformará as entradas em saídas:
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();
}
}
Vamos agorawrite a quick test com um fluxo de processamento em quePublisher está publicandoString elementos.
NossoTransformProcessor analisaráString comoInteger - o que significa que uma conversão precisa acontecer aqui:
@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)
);
}
Observe que chamar o métodoclose() na basePublisher fará com que o métodoonComplete() emTransformProcessor seja chamado.
Lembre-se de que todos os editores da cadeia de processamento precisam ser fechados dessa maneira.
5. Controlando a demanda por mensagens usando oSubscription
Digamos que queremos consumir apenas o primeiro elemento da assinatura, aplicar alguma lógica e terminar o processamento. Podemos usar o métodorequest() para conseguir isso.
Vamos modificar nossoEndSubscriber para consumir apenas N número de mensagens. Vamos passar esse número como o argumento do construtorhowMuchMessagesConsume:
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);
}
}
//...
}
Podemos solicitar elementos contanto que desejemos.
Vamos escrever um teste no qual queremos consumir apenas um elemento de determinadoSubscription:
@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)
);
}
Emborapublisher esteja publicando seis elementos, nossoEndSubscriber consumirá apenas um elemento porque sinaliza a demanda para o processamento de apenas aquele único.
Usando o métodorequest() emSubscription,, podemos implementar um mecanismo de contrapressão mais sofisticado para controlar a velocidade de consumo da mensagem.
6. Conclusão
Neste artigo, vimos o Java 9 Reactive Streams.
Vimos como criar umFlow de processamento consistindo emPublishereSubscriber. Criamos um fluxo de processamento mais complexo com a transformação de elementos usandoProcessors.
Finalmente, usamos oSubscription para controlar a demanda por elementos porSubscriber.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.