Introdução ao Spring Reactor
1. Visão geral
Neste artigo rápido, apresentaremos o projeto Spring Reactor. Vamos configurar um cenário da vida real para um aplicativo reativo e orientado a eventos.
2. O básico do Spring Reactor
2.1. Por que o reator?
O design reativopattern é uma arquitetura baseada em eventos para manipulação assíncrona de um grande volume de solicitações de serviço simultâneas provenientes de um ou vários manipuladores de serviço.
E o projeto Spring Reactor é baseado neste padrão e tem o objetivo claro e ambicioso de construir aplicativos assíncronos e reativos naJVM.
2.2. Cenários de Exemplo
Antes de começar, aqui estão alguns cenários interessantes em que aproveitar o estilo de arquitetura reativa fará sentido, apenas para ter uma idéia de onde você pode aplicá-lo:
-
Serviço de notificação de grandes aplicativos de compras on-line como a Amazon
-
Serviços de processamento de transações enormes do setor bancário
-
Compartilhar negócios comerciais em que os preços das ações mudam simultaneamente
Uma observação rápida a ser observada é que a implementação do barramento de eventos não oferece persistência de eventos; assim como o barramento Spring Event padrão, é uma implementação na memória.
3. Dependências do Maven
Vamos começar a usar Spring Reactor adicionando a seguinte dependência em nossopom.xml:
io.projectreactor
reactor-bus
2.0.8.RELEASE
Você pode verificar a versão mais recente do barramento do reator emCentral Maven Repository.
4. Construindo um aplicativo de demonstração
Para entender melhor os benefícios da abordagem baseada em reator,let’s look at a practical example.
Vamos construir um aplicativo de notificação simples, que notifica os usuários por e-mail e SMS - depois que eles concluem o pedido em uma loja online.
Uma implementação síncrona típica seria naturalmente vinculada à taxa de transferência do serviço SMS. Picos no tráfego, tais feriados geralmente seriam problemáticos.
Com uma abordagem reativa, o sistema pode ser mais flexível e se adaptar melhor a falhas ou tempos limite nesses tipos de sistemas externos, como SMS ou servidores de email.
Vamos dar uma olhada no aplicativo - começando com os aspectos mais tradicionais e passando para as construções mais reativas.
4.1. POJO simples
Primeiro, vamos criar uma classePOJO para representar os dados de notificação:
public class NotificationData {
private long id;
private String name;
private String email;
private String mobile;
// getter and setter methods
}
4.2. A Camada de Serviço
Agora vamos configurar uma camada de serviço simples:
public interface NotificationService {
void initiateNotification(NotificationData notificationData)
throws InterruptedException;
}
E a implementação, simulando uma operação longa aqui:
@Service
public class NotificationServiceimpl implements NotificationService {
@Override
public void initiateNotification(NotificationData notificationData)
throws InterruptedException {
System.out.println("Notification service started for "
+ "Notification ID: " + notificationData.getId());
Thread.sleep(5000);
System.out.println("Notification service ended for "
+ "Notification ID: " + notificationData.getId());
}
}
Observe que, para ilustrar o cenário da vida real de envio de mensagens via gateway de SMS ou gateway de e-mail, estamos introduzindo intencionalmente um atraso de 5 segundos no métodoinitiateNotification porThread.sleep(5000).
E assim, quando o encadeamento atingir o serviço - ele será bloqueado por 5 segundos.
4.3. O consumidor
Vamos agora pular para os aspectos mais reativos de nosso aplicativo e implementar um consumidor - que então mapearemos parareactor event bus:
@Service
public class NotificationConsumer implements
Consumer> {
@Autowired
private NotificationService notificationService;
@Override
public void accept(Event notificationDataEvent) {
NotificationData notificationData = notificationDataEvent.getData();
try {
notificationService.initiateNotification(notificationData);
} catch (InterruptedException e) {
// ignore
}
}
}
Como você pode ver, o consumidor está simplesmente implementando a interfaceConsumer<T> - com um único métodoaccept. É esta implementação simples que executa a lógica principal, assim comoa typical Spring listener.
4.4. O controlador
Finalmente, agora que podemos consumir os eventos, vamos gerá-los também.
Faremos isso em um controlador simples:
@Controller
public class NotificationController {
@Autowired
private EventBus eventBus;
@GetMapping("/startNotification/{param}")
public void startNotification(@PathVariable Integer param) {
for (int i = 0; i < param; i++) {
NotificationData data = new NotificationData();
data.setId(i);
eventBus.notify("notificationConsumer", Event.wrap(data));
System.out.println(
"Notification " + i + ": notification task submitted successfully");
}
}
}
Isso é bastante autoexplicativo - estamos enviando eventos por meio deEventBus aqui - usando uma chave exclusiva.
Portanto, basta colocar - quando um cliente acessa a URL com o valor do parâmetro 10, um total de 10 eventos será enviado através do barramento.
4.5. O Java Config
Estamos quase terminando; vamos apenas juntar tudo com o Java Config e criar nosso aplicativo de inicialização:
import static reactor.bus.selector.Selectors.$;
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application implements CommandLineRunner {
@Autowired
private EventBus eventBus;
@Autowired
private NotificationConsumer notificationConsumer;
@Bean
Environment env() {
return Environment.initializeIfEmpty().assignErrorJournal();
}
@Bean
EventBus createEventBus(Environment env) {
return EventBus.create(env, Environment.THREAD_POOL);
}
@Override
public void run(String... args) throws Exception {
eventBus.on($("notificationConsumer"), notificationConsumer);
}
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
}
É aqui que estamos criando o beanEventBus por meio da APIcreate estática emEventBus.
Em nosso caso, estamos instanciando o barramento de eventos com um pool de threads padrão disponível no ambiente.
Se quiséssemos um pouco mais de controle sobre o barramento, também poderíamos fornecer uma contagem de threads para a implementação:
EventBus evBus = EventBus.create(
env,
Environment.newDispatcher(
REACTOR_THREAD_COUNT,REACTOR_THREAD_COUNT,
DispatcherType.THREAD_POOL_EXECUTOR));
Em seguida - observe também como estamos usando a importação estática do atributo$ aqui.
O recurso fornece um mecanismo de segurança de tipo para incluir constantes (no nosso caso, é o atributo $) no código sem ter que fazer referência à classe que originalmente definiu o campo.
Estamos usando essa funcionalidade em nossa implementação de métodorun -where we’re registering our consumer to be triggered when the matching notification.
Isso é baseado ema unique selector key que permite que cada consumidor seja identificado.
5. Teste o aplicativo
Depois de executar uma compilação do Maven, agora podemos simplesmente executarjava -jar name_of_the_application.jar para executar o aplicativo.
Vamos agora criar uma pequena classe de teste JUnit para testar o aplicativo. UsaríamosSpringJUnit4ClassRunner do Spring Boot para criar o caso de teste:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {Application.class})
public class DataLoader {
@Test
public void exampleTest() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject(
"http://localhost:8080/startNotification/10", String.class);
}
}
Agora, vamos executar este caso de teste para testar o aplicativo:
Notification 0: notification task submitted successfully
Notification 1: notification task submitted successfully
Notification 2: notification task submitted successfully
Notification 3: notification task submitted successfully
Notification 4: notification task submitted successfully
Notification 5: notification task submitted successfully
Notification 6: notification task submitted successfully
Notification 7: notification task submitted successfully
Notification 8: notification task submitted successfully
Notification 9: notification task submitted successfully
Notification service started for Notification ID: 1
Notification service started for Notification ID: 2
Notification service started for Notification ID: 3
Notification service started for Notification ID: 0
Notification service ended for Notification ID: 1
Notification service ended for Notification ID: 0
Notification service started for Notification ID: 4
Notification service ended for Notification ID: 3
Notification service ended for Notification ID: 2
Notification service started for Notification ID: 6
Notification service started for Notification ID: 5
Notification service started for Notification ID: 7
Notification service ended for Notification ID: 4
Notification service started for Notification ID: 8
Notification service ended for Notification ID: 6
Notification service ended for Notification ID: 5
Notification service started for Notification ID: 9
Notification service ended for Notification ID: 7
Notification service ended for Notification ID: 8
Notification service ended for Notification ID: 9
Como você pode ver, assim que o terminal atinge, todas as 10 tarefas são enviadas instantaneamente sem criar nenhum bloqueio. E, uma vez enviados, os eventos de notificação são processados em paralelo.
Lembre-se de que, em nosso cenário, não há necessidade de processar esses eventos em qualquer ordem.
6. Conclusão
Nesse pequeno aplicativo, definitivamente obtemos um aumento na taxa de transferência, juntamente com um aplicativo mais bem comportado em geral.
No entanto, esse cenário está apenas arranhando a superfície e representa apenas uma boa base para começar a entender o paradigma reativo.
Como sempre, o código-fonte está disponívelover on GitHub.