Olá! Meu nome é João Victor, e serei o instrutor deste curso de Quarkus.
Audiodescrição: João Victor é um homem branco, com cabelo castanho curto e barba. Ele veste uma camisa azul e está em um ambiente de escritório com uma estante ao fundo.
Se temos interesse em trabalhar com aplicações Quarkus reativas e, além disso, desejamos entender como criar uma aplicação que funcione como um producer (produtor) enviando mensagens para o Kafka, e uma aplicação que funcione como um consumer (consumidor) recebendo mensagens do Kafka, além de melhorar a resiliência das nossas aplicações Quarkus com o fault tolerance (tolerância a falhas), estamos no lugar certo.
Neste curso, abordaremos diferentes recursos que tornam nossa aplicação mais robusta e escalável. No final, conseguiremos realizar o deploy (implantação) dessas aplicações, pois trabalharemos com duas aplicações em um ambiente clusterizado Kubernetes, utilizando o K3D como recurso Kubernetes local.
Vamos utilizar uma aplicação que foi usada no curso anterior de Quarkus, onde ela operava no modelo imperativo e realizava algumas operações básicas de CRUD, fazendo chamadas para uma API externa. Fizemos um refactoring (refatoração) para que agora ela trabalhe com multithreading (multifilamento), tornando-se reativa.
Teremos métodos muito mais elegantes, com uma grande melhoria nos recursos das nossas threads (linhas de execução). Também utilizaremos recursos como circuit breaker (disjuntor), fallback (alternativa) e rate limit (limite de taxa), que são da biblioteca de fault tolerance, melhorando a resiliência da nossa aplicação Quarkus.
Além disso, teremos um consumer que utilizará a biblioteca do Kafka reativa do Quarkus. Esse consumer ficará responsável por consumir mensagens de um tópico Kafka, que serão produzidas por meio de nossa outra aplicação. Essa aplicação foi utilizada no primeiro curso e agora expandimos suas operações. Ela atuará como um producer de mensagens, com uma regra de negócio para enviar mensagens da aplicação Banking Validation para a Banking Service, com o objetivo de excluir agências que serão inativadas na Banking Validation.
Voltando à nossa Banking Service, além de criar os producers e consumers, também desenvolveremos manifestos Kubernetes para realizar o deploy dessa aplicação em um cluster K3D, que é um Kubernetes local. Dessa forma, conseguiremos criar manifestos para o nosso Kafka, Postgres e nossa aplicação, permitindo a comunicação entre as aplicações em um cluster Kubernetes.
No final, poderemos utilizar esses manifestos em outros ambientes, como OpenShift e AWS, que possuem seus próprios Kubernetes, além do Google. Isso é bastante proveitoso para quem deseja entender como funciona o deploy de aplicações em um ambiente clusterizado.
Se tudo isso é interessante, convidamos a nos acompanhar neste curso. Temos certeza de que será muito enriquecedor e proporcionará muitos recursos para aprimorar suas habilidades como pessoa desenvolvedora. Nos vemos no próximo vídeo. Até lá!
No primeiro curso de Quarkus que gravamos, desenvolvemos uma aplicação chamada Banking Service, que, em teoria, segue o fluxo de uma aplicação MVC. Ela possui uma controller, uma service e um repository. Para quem não se lembra ou não fez o primeiro curso, estamos com ela aberta aqui, pois será a base para este segundo curso. Vamos continuar trabalhando em cima do que já construímos anteriormente.
Vamos começar revisando a estrutura da nossa aplicação. Basicamente, temos uma controller de agência. Esta aplicação realiza operações CRUD, ou seja, post, put, delete e get. Na nossa service, especificamente na Agência Service, temos uma regra de negócio, principalmente no método de cadastro. No arquivo perf-agências
, encontramos os métodos mencionados. Na controller, não há regras específicas, apenas os rest-responses com os quais já estamos acostumados.
Para ilustrar, vamos ver como a AgenciaController
está estruturada. Primeiro, definimos a classe e injetamos o serviço necessário:
package com.alura.agencias.controller;
import ...
@Path("/agencias")
public class AgenciaController {
private final AgenciaService agenciaService;
AgenciaController(AgenciaService agenciaService) {
this.agenciaService = agenciaService;
}
Agora, vamos adicionar o método para cadastrar uma nova agência. Este método utiliza a anotação @POST
e é transacional, garantindo que a operação seja concluída com sucesso ou revertida em caso de falha:
@POST
@Transactional
public RestResponse<Void> cadastrar(Agencia agencia, @Context UriInfo uriInfo) {
this.agenciaService.cadastrar(agencia);
return RestResponse.created(uriInfo.getAbsolutePathBuilder().build());
}
Na Agência Service, encontramos a regra de negócio, que inclui algumas condições. Fazemos uma requisição HTTP para uma aplicação chamada Banking Validation, que simula o cadastro da agência para verificar se a situação cadastral está ativa ou não. Dependendo do status, conseguimos adicionar ou não, com tratativas para cada caso. Essa é a regra de negócio que criamos durante o curso.
Vamos ver como a AgenciaService
está estruturada, começando pela injeção dos componentes necessários:
@ApplicationScoped
public class AgenciaService {
private final AgenciaRepository agenciaRepository;
private final MeterRegistry meterRegistry;
AgenciaService(AgenciaRepository agenciaRepository, MeterRegistry meterRegistry) {
this.agenciaRepository = agenciaRepository;
this.meterRegistry = meterRegistry;
}
@RestClient
SituacaoCadastralHttpService situacaoCadastralHttpService;
O método cadastrar
é onde a lógica de verificação da situação cadastral é implementada:
public void cadastrar(Agencia agencia) {
AgenciaHttp agenciaHttp = situacaoCadastralHttpService.buscarPorCnpj(agencia.getCnpj());
if(agenciaHttp != null && agenciaHttp.getSituacaoCadastral().equals(SituacaoCadastral.ATIVO)) {
this.meterRegistry.counter("agencia_adicionada_count").increment();
Log.info("Agencia com CNPJ " + agencia.getCnpj() + " foi adicionada");
agenciaRepository.persist(agencia);
} else {
Log.info("Agencia com CNPJ " + agencia.getCnpj() + " não ativa ou não encontrada");
this.meterRegistry.counter("agencia_nao_adicionada_count").increment();
throw new AgenciaNaoAtivaOuNaoEncontradaException();
}
}
O restante do processo é simples, apenas conectando com o repository. Utilizamos o Panache, implementando o PanacheRepository
, que nos fornece métodos como findById
, persist
, update
, entre outros. Ao implementar essa interface, ganhamos todos os métodos que o Quarkus nos oferece.
Nossa aplicação está funcional. Por exemplo, ao acessar localhost:8080
no navegador, uma página padrão do Quarkus é exibida, mostrando os endpoints disponíveis, como agências-id
para delete, agências-post
, agências-id-get
, entre outros. A aplicação está 100% funcional.
No entanto, ao falarmos de aplicações em ambientes produtivos, precisamos otimizar nossos recursos e melhorar a performance, pois isso impacta nos custos. Atualmente, nossa aplicação foi desenvolvida no paradigma imperativo, o que significa que não aproveitamos os recursos, como threads, da melhor maneira possível.
Quando um usuário faz uma requisição, todo o fluxo da aplicação é seguido. Por exemplo, ao cadastrar uma agência via Postman ou outra ferramenta, a requisição atinge a service, que faz uma chamada para um servidor externo. No paradigma imperativo, a requisição espera pela resposta desse servidor. O mesmo ocorre ao chamar o repository, que se comunica com um banco de dados em outro servidor. Essa espera impacta os recursos da aplicação, pois as threads da JVM estão ligadas às do sistema operacional, que são limitadas.
Se a aplicação for para produção dessa forma, ela funcionará, mas ao escalar para muitos usuários, surgem complicações. Quando o número de threads do servidor é atingido, as requisições ficam enfileiradas, causando problemas de performance e aumentando o tempo de espera para o usuário. Isso pode levar à necessidade de escalar recursos do servidor, o que é caro.
Felizmente, existem recursos na programação que nos ajudam com isso. A primeira melhoria que faremos na aplicação é mudar do paradigma imperativo para o paradigma reativo.
Com o paradigma reativo, ao lidar com recursos, como quando chamamos uma API em outro servidor, o Quarkus identifica momentos de chamadas bloqueantes. Graças aos recursos que utiliza, como o servidor de aplicação Vertex, que é reativo por natureza, o Quarkus consegue liberar a thread da JVM quando ocorre um bloqueio. Quando a resposta do outro servidor chega, o Quarkus, através do Mutiny, uma biblioteca de desenvolvimento de paradigma reativo, lança um evento indicando que a requisição foi concluída. Outra thread pode então receber esse evento, fazer o subscribe e retornar a requisição para a pessoa usuária que está aguardando.
Com essa abordagem, conseguimos um melhor aproveitamento das threads, pois elas não ficam bloqueadas enquanto aguardam a resposta de uma API. A thread é liberada para atender outras requisições, como um GET de outro usuário. Isso resulta em um melhor aproveitamento de recursos, graças ao Vertex, servidor de aplicação do Quarkus, e ao Mutiny, que vamos explorar a seguir.
Para implementar essas alterações, precisamos modificar nossa aplicação. Vamos abrir o arquivo .xml
e ressaltar que, para uma aplicação ser reativa, ela deve ser reativa do início ao fim, desde o controle até o banco de dados. Isso implica que o driver da aplicação também deve ser reativo. Se estamos usando um banco de dados Postgres, que pode ter um driver reativo, o Quarkus já fornece recursos para controle, service e repository reativos, permitindo transformar a aplicação em 100% reativa sem maiores problemas.
Vamos começar as alterações. No Quarkus Hibernate ORM Panache, substituímos pela Reactive Panache. Também mudamos o JDBC PostgreSQL para Quarkus Reactive PG Client. Essas mudanças são necessárias para que o banco de dados seja reativo. Vamos alterar primeiro o nosso repository, depois a service e, por fim, o controle.
No canto superior direito, temos um M do Maven, que usamos para processar e fazer o reload da aplicação, baixando as dependências necessárias. Enquanto isso, podemos mudar o import na classe repository de io.quarkus.hibernate.orm.panache.panache.repository
para Reactive. Após baixar as dependências, a mudança estará completa.
No arquivo de configuração Application Properties
, alteramos quarkus.datasource.jdbc.url
para Reactive, ajustando a string de conexão para começar com PostgreSQL, sem o prefixo jdbc:
. Isso transforma o repository em reativo com um driver de banco de dados reativo.
É importante que toda a aplicação seja reativa, pois, se apenas parte dela for, o sistema continuará operando de forma imperativa, bloqueando threads e não aproveitando os benefícios do reativo. Algumas alterações podem fazer com que certos recursos não compilem mais, mas vamos ajustando a aplicação para alcançar 100% de reatividade.
Nos encontramos no próximo vídeo para continuar as modificações. Até lá!
Vamos continuar o processo de migração da nossa aplicação do paradigma imperativo para o paradigma reativo. Já fizemos as alterações no nosso repository, e agora vamos continuar trabalhando no nosso service. Observamos que o service, especialmente o agência service, não estava compilando, pois alteramos o repository. Vamos entender por que isso está acontecendo. De qualquer forma, vamos focar primeiro no método cadastrar
, que será o mais trabalhoso para adaptar ao modelo reativo. Os outros métodos serão mais simples de ajustar.
A primeira coisa a ser feita é mudar o método cadastrar
para o modelo reativo, o que também requer a alteração da classe situação cadastral HTTP service
, especificamente no método buscar por CNPJ
. Atualmente, ele ainda retorna uma agência HTTP
, mas precisará ter um retorno diferente, pois agora estamos utilizando o modelo reativo. Quando fazemos uma requisição para a API, não queremos bloquear a thread até obtermos a resposta; queremos que ela esteja no modelo reativo.
Para essa classe, a única modificação necessária é no retorno. Em vez de retornar uma agência HTTP
, ela passará a retornar um UNI agência HTTP
. O UNI
representa uma promessa de valor. Quando fazemos a requisição e liberamos a thread, o UNI
indica que, em algum momento, haverá um valor, que pode ser uma agência HTTP
ou nulo. A validação é feita em outro ponto. O UNI
é simplesmente a representação de uma promessa de valor. No import, ele vem do multini, a biblioteca que usamos no Quarkus para o modelo reativo.
import io.smallrye.mutiny.Uni;
Uni<AgenciaHttp> buscarPorCnpj(String cnpj);
No repository, tivemos que adicionar novas dependências, mas aqui não. Isso ocorre porque o Quarkus já trabalha com extensões nativamente reativas. Podemos trabalhar no paradigma imperativo, mas as extensões são nativamente reativas. Todas as dependências que já temos, como HashClient e RashJackson, já possuem recursos reativos. Por isso, não precisamos fazer alterações no nosso .xml
.
A partir de agora, temos uma promessa de valor da nossa API, que será uma agência HTTP
. Isso muda a forma como trabalhamos com esse retorno na classe cadastrar
, no método cadastrar
. O método buscar por CNPJ
não retornará mais uma agência HTTP
, mas sim um UNI agência HTTP
. Portanto, precisamos alterar o tipo de retorno da nossa agência e importar no método.
Uni<AgenciaHttp> agenciaHttp = situacaoCadastralHttpService.buscarPorCnpj(agencia.getCnpj());
Agora, a linha do método buscar por CNPJ
parou de apresentar erros, mas o GET situação cadastral
está com problemas. Com o UNI
, não conseguimos acessar diretamente o objeto agência HTTP
, pois é uma promessa e pode ser nulo. No modelo reativo, precisamos tratar isso de forma diferente do modelo imperativo, onde usávamos um simples if-else
.
A primeira diferença é que, ao acessar agência.http
, diversos métodos são apresentados. Não podemos pegar diretamente a agência HTTP
; precisamos usar os métodos do UNI
. Primeiramente, verificamos se há um item na agência HTTP
. Se for nulo, lançamos uma exceção agência não ativa ou não encontrada, exception
. Isso substitui a verificação agência HTTP diferente de nulo
no if
.
agenciaHttp.onItem().ifNull().failWith(new AgenciaNaoAtivaOuNaoEncontradaException());
Após validar se o item não é nulo, precisamos transformar a promessa de valor no valor real, que é a agência HTTP
. Ao encadear as chamadas, o modelo reativo se torna mais funcional. Se o item não falhar, transformamos a promessa de valor em um valor real. No entanto, ao usar transform
, ele transforma o valor do UNI agência HTTP
em um UNI de UNI agência HTTP
, o que ainda não nos permite acessar a agência HTTP
, que é o que queremos.
transformToUnit
Para implementar a funcionalidade desejada, precisamos utilizar o transformToUnit
. Essa função é semelhante ao flatMap
das collections do Java ou Kotlin, que combina duas listas em uma única lista, permitindo trabalhar com o objeto resultante. O transformToUnit
segue essa lógica, evitando que tenhamos um unit
de unit
de agência HTTP
. Assim, conseguimos acessar diretamente o item, que será a agência HTTP
.
agenciaHttp
.onItem().ifNull().failWith(new AgenciaNaoAtivaOuNaoEncontradaException())
.onItem().transformToUni(item -> persistirSeAtiva(agencia, item));
Dentro dessa função, utilizamos uma expressão lambda, nomeando o item como item
, que representa a agência HTTP
. Embora seja possível utilizar estruturas condicionais como if
e else
dentro da lambda, isso pode tornar o código desorganizado. Portanto, optamos por isolar a validação em uma nova função chamada persistirSeAtiva
.
persistirSeAtiva
A função persistirSeAtiva
recebe a agência
, que já é um parâmetro do método cadastrar
, e o item
, que é a agência HTTP
. Transferimos a lógica do if
e else
para essa nova função, que será um método privado dentro do contexto da agênciaService
.
private Uni<Void> persistirSeAtiva(Agencia agencia, AgenciaHttp agenciaHttp) {
if(agenciaHttp.getSituacaoCadastral().equals(SituacaoCadastral.ATIVO)) {
this.meterRegistry.counter("name: agencia_adicionada_count").increment();
Log.info("Agencia com CNPJ " + agencia.getCnpj() + " foi adicionada");
return agenciaRepository.persist(agencia).replaceWithVoid();
} else {
Log.info("Agencia com CNPJ " + agencia.getCnpj() + " não ativa ou não encontrada");
this.meterRegistry.counter("name: agencia_nao_adicionada_count").increment();
return Uni.createFrom().failure(new AgenciaNaoAtivaOuNaoEncontradaException());
}
}
Os métodos agora não retornam mais um void
tradicional, mas sim um void
encapsulado em um uni
, pois, mesmo sem retornar valores, precisamos finalizar a requisição de forma reativa. O void
é representado por Uni<Void>
. Chamamos o método persistirSeAtiva
, passando a agência
e a agência HTTP
.
cadastrar
Ao implementar o método, percebemos que ele ainda não está completamente funcional. Precisamos garantir que, ao cair no if
, retornamos o método persist
, e, no else
, lançamos uma exceção, pois a agência
não está ativa. O método persist
retorna a própria agência
adicionada, mas, como o método cadastrar
deve retornar um void
, utilizamos replaceWithVoid
para resolver essa discrepância.
No caso do else
, o lançamento de exceção precisa ser adaptado para o modelo reativo. Utilizamos Uni.createFrom().failure()
para retornar a exceção de forma não bloqueante, prometendo à camada de controle que uma exceção será retornada.
Com essas alterações, o método persistirSeAtiva
está funcional, seguindo as mesmas regras anteriores, mas sem a validação de nulabilidade, que é feita previamente. No método cadastrar
, retornamos o agência HTTP
com as funções alinhadas, validando e retornando o resultado de persistirSeAtiva
.
@WithTransaction
public Uni<Void> cadastrar(Agencia agencia) {
Uni<AgenciaHttp> agenciaHttp = situacaoCadastralHttpService.buscarPorCnpj(agencia.getCnpj());
return agenciaHttp
.onItem().ifNull().failWith(new AgenciaNaoAtivaOuNaoEncontradaException())
.onItem().transformToUni(item -> persistirSeAtiva(agencia, item));
}
Ainda enfrentamos um erro de compilação na classe, mas não mais no método cadastrar
. Além disso, no controle, o método cadastrar
foi anotado como transactional
, o que funciona bem no modelo imperativo. No modelo reativo, precisamos manter a transação aberta explicitamente, anotando o método com @WithTransaction
, garantindo que a transação iniciada no controle permaneça aberta até o repository
.
Essas foram as alterações necessárias no método cadastrar
. Realizamos o refactor e, no próximo passo, continuaremos ajustando os outros métodos para que a classe compile corretamente. Nos vemos na próxima aula!
O curso Java e Quarkus: desenvolva aplicações reativas e resilientes possui 214 minutos de vídeos, em um total de 48 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.
Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.