📚 Série: Quarkus: Desvendando o Desenvolvimento Moderno com Java
Este é o terceiro capítulo de uma série completa sobre Quarkus. Prepare-se para uma jornada que vai transformar sua visão sobre desenvolvimento Java moderno!
Se você chegou até aqui, já sabe que o Quarkus é um framework poderoso para construir aplicações Java modernas. Mas o que realmente faz a diferença entre uma aplicação bem estruturada e um "código espaguete"? A resposta está na Injeção de Dependências.
Hoje vamos mergulhar no CDI (Contexts and Dependency Injection), o coração que faz toda aplicação Quarkus bater de forma organizada e eficiente. 🚀
🎯 O que é Injeção de Dependências?
Imagine que você está construindo uma casa. Em vez de você mesmo fabricar cada tijolo, cada porta e cada janela, você recebe esses componentes prontos de fornecedores especializados. A Injeção de Dependências funciona de forma similar: em vez de sua classe criar suas próprias dependências, ela recebe objetos prontos de um "fornecedor" (o contêiner CDI).
Por que isso é revolucionário?
- 🧪 Testabilidade: Fácil de criar mocks e testes unitários
- 🔧 Manutenibilidade: Código mais limpo e modular
- ♻️ Reusabilidade: Componentes podem ser reutilizados facilmente
- 🔄 Flexibilidade: Troque implementações sem quebrar o código
📚 CDI: Contexts and Dependency Injection Explicado
O CDI é a especificação padrão do Jakarta EE (antigo Java EE) para gerenciar dependências, e o Quarkus a implementa de forma nativa e otimizada. Vamos entender os conceitos fundamentais:
🫘 Beans: Os Protagonistas do CDI
Beans são objetos gerenciados pelo contêiner CDI. Pense neles como "funcionários especializados" da sua aplicação - cada um tem uma função específica e o CDI se encarrega de coordená-los.
@ApplicationScoped public class CalculadoraService { public double somar(double a, double b) { return a + b; } } 🎯 Escopos: Definindo o Tempo de Vida
Os escopos determinam quando e por quanto tempo um bean existe na memória:
@ApplicationScoped - O Eterno
Uma única instância para toda a aplicação. Perfeito para serviços stateless.
@ApplicationScoped public class ConfiguracaoService { private String versaoApp = "1.0.0"; public String getVersaoApp() { return versaoApp; } } @RequestScoped - O Efêmero
Nova instância a cada requisição HTTP. Ideal para dados específicos da requisição.
@RequestScoped public class ContadorRequisicaoService { private int contador = 0; public void incrementar() { contador++; } public int getContador() { return contador; } } @Singleton - O Único Verdadeiro
Garante uma única instância em toda a JVM, independente do contexto. Mais restritivo que @ApplicationScoped.
@Singleton public class DatabaseConnectionPool { private final int maxConnections = 10; private int activeConnections = 0; public synchronized boolean acquireConnection() { if (activeConnections < maxConnections) { activeConnections++; return true; } return false; } public synchronized void releaseConnection() { if (activeConnections > 0) { activeConnections--; } } } @Dependent - O Dependente
Vive e morre junto com quem o utiliza.
@Dependent public class UtilService { public String formatarTexto(String texto) { return texto.toUpperCase().trim(); } } 🔍 ApplicationScoped vs Singleton: Qual a Diferença?
Esta é uma dúvida muito comum! Ambos criam uma única instância, mas há diferenças importantes:
@ApplicationScoped
- Contexto: Ligado ao contexto da aplicação CDI
- Proxy: Cria um proxy para lazy loading
- Flexibilidade: Pode ser desabilitado/reabilitado em contextos específicos
- Performance: Pequeno overhead do proxy
@Singleton
- Contexto: Independente de qualquer contexto CDI
- Instanciação: Criação direta, sem proxy
- Rigidez: Sempre ativa, não pode ser desabilitada
- Performance: Acesso direto, sem overhead
Quando usar cada um?
// Use @ApplicationScoped para serviços de negócio @ApplicationScoped public class PedidoService { // Lógica de negócio que pode precisar ser mockada em testes } // Use @Singleton para recursos compartilhados críticos @Singleton public class MetricsCollector { // Recursos que NUNCA devem ter múltiplas instâncias } 💉 Injeção Básica: Primeiros Passos
Vamos começar com exemplos simples para entender como a injeção funciona na prática:
1. Criando o Serviço de Saudação
package com.example.service; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class GreetingService { public String criarSaudacao(String nome) { if (nome == null || nome.trim().isEmpty()) { return "Olá, visitante anônimo! 👋"; } return String.format("Olá, %s! Bem-vindo ao mundo Quarkus! 🚀", nome); } public String criarSaudacaoPersonalizada(String nome, String idioma) { if (nome == null || nome.trim().isEmpty()) { nome = "visitante"; } return switch (idioma.toLowerCase()) { case "en" -> String.format("Hello, %s! Welcome to Quarkus world! 🚀", nome); case "es" -> String.format("¡Hola, %s! ¡Bienvenido al mundo Quarkus! 🚀", nome); case "fr" -> String.format("Bonjour, %s! Bienvenue dans le monde Quarkus! 🚀", nome); default -> String.format("Olá, %s! Bem-vindo ao mundo Quarkus! 🚀", nome); }; } } 2. Criando o Contador de Requisições
package com.example.service; import jakarta.enterprise.context.RequestScoped; @RequestScoped public class RequestCounterService { private int contador = 0; private final long timestampInicializacao = System.currentTimeMillis(); public void incrementar() { contador++; } public int getContador() { return contador; } public long getTempoVida() { return System.currentTimeMillis() - timestampInicializacao; } public String getEstatisticas() { return String.format("Requisições: %d | Tempo de vida: %d ms", contador, getTempoVida()); } } 3. Usando Injeção no Resource
package com.example.resource; import com.example.service.GreetingService; import com.example.service.RequestCounterService; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; @Path("/hello") public class GreetingResource { @Inject GreetingService greetingService; @Inject RequestCounterService requestCounterService; @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { requestCounterService.incrementar(); String saudacao = greetingService.criarSaudacao("Mundo Quarkus"); String estatisticas = requestCounterService.getEstatisticas(); return String.format("%s\n📊 %s", saudacao, estatisticas); } @GET @Path("/personalizada") @Produces(MediaType.TEXT_PLAIN) public String helloPersonalizada( @QueryParam("nome") String nome, @QueryParam("idioma") String idioma) { requestCounterService.incrementar(); if (idioma == null) { idioma = "pt"; } String saudacao = greetingService.criarSaudacaoPersonalizada(nome, idioma); String estatisticas = requestCounterService.getEstatisticas(); return String.format("%s\n📊 %s", saudacao, estatisticas); } } 🎭 Interfaces e CDI: Programação Contra Abstrações
Agora que entendemos a injeção básica, vamos para um nível mais avançado: usando interfaces para criar código verdadeiramente desacoplado.
Por que usar Interfaces com CDI?
// ❌ Acoplamento forte - difícil de testar e modificar @ApplicationScoped public class EmailService { public void enviarEmail(String destinatario, String mensagem) { // Implementação específica de email } } // ✅ Baixo acoplamento - flexível e testável public interface NotificationService { void enviarNotificacao(String destinatario, String mensagem); } @ApplicationScoped public class EmailNotificationService implements NotificationService { @Override public void enviarNotificacao(String destinatario, String mensagem) { // Implementação específica de email } } Exemplo Avançado: Sistema de Notificações
Vamos criar um sistema que pode enviar notificações por diferentes canais:
// Interface base package com.example.service; public interface NotificationService { void enviarNotificacao(String destinatario, String mensagem); String getTipoNotificacao(); } // Implementação para Email package com.example.service; import com.example.qualifier.Email; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class EmailNotificationService implements NotificationService { @Override public void enviarNotificacao(String destinatario, String mensagem) { System.out.println("📧 Enviando email para: " + destinatario); System.out.println("Mensagem: " + mensagem); } @Override public String getTipoNotificacao() { return "EMAIL"; } } // Implementação para SMS package com.example.service; import com.example.qualifier.Sms; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class SmsNotificationService implements NotificationService { @Override public void enviarNotificacao(String destinatario, String mensagem) { System.out.println("📱 Enviando SMS para: " + destinatario); System.out.println("Mensagem: " + mensagem); } @Override public String getTipoNotificacao() { return "SMS"; } } Resolvendo Ambiguidade com Qualifiers
Quando você tem múltiplas implementações da mesma interface, o CDI precisa saber qual usar. Aqui entram os Qualifiers:
package com.example.qualifier; import jakarta.inject.Qualifier; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Qualifier @Retention(RUNTIME) @Target({METHOD, FIELD, PARAMETER, TYPE}) public @interface Email { } package com.example.qualifier; import jakarta.inject.Qualifier; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Qualifier @Retention(RUNTIME) @Target({METHOD, FIELD, PARAMETER, TYPE}) public @interface Sms { } Agora marcamos nossas implementações:
@Email @ApplicationScoped public class EmailNotificationService implements NotificationService { // ... implementação } @Sms @ApplicationScoped public class SmsNotificationService implements NotificationService { // ... implementação } Criando o DTO para Requisições
package com.example.dto; public class NotificationRequest { private String destinatario; private String mensagem; // Construtores public NotificationRequest() {} public NotificationRequest(String destinatario, String mensagem) { this.destinatario = destinatario; this.mensagem = mensagem; } // Getters e Setters public String getDestinatario() { return destinatario; } public void setDestinatario(String destinatario) { this.destinatario = destinatario; } public String getMensagem() { return mensagem; } public void setMensagem(String mensagem) { this.mensagem = mensagem; } } Injetando Implementações Específicas
package com.example.resource; import com.example.dto.NotificationRequest; import com.example.qualifier.Email; import com.example.qualifier.Sms; import com.example.service.NotificationService; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.MediaType; @Path("/notifications") public class NotificationResource { @Inject @Email NotificationService emailService; @Inject @Sms NotificationService smsService; @POST @Path("/email") @Consumes(MediaType.APPLICATION_JSON) public String enviarEmail(NotificationRequest request) { emailService.enviarNotificacao(request.getDestinatario(), request.getMensagem()); return "Email enviado com sucesso!"; } @POST @Path("/sms") @Consumes(MediaType.APPLICATION_JSON) public String enviarSms(NotificationRequest request) { smsService.enviarNotificacao(request.getDestinatario(), request.getMensagem()); return "SMS enviado com sucesso!"; } } Descoberta Dinâmica com Any e Instance
O CDI oferece uma forma elegante de trabalhar com múltiplas implementações:
package com.example.resource; import com.example.dto.NotificationRequest; import com.example.qualifier.Email; import com.example.qualifier.Sms; import com.example.service.NotificationService; import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import java.util.List; import java.util.stream.Collectors; @Path("/notifications") public class NotificationResource { @Inject @Any Instance<NotificationService> notificationServices; @GET @Path("/tipos") @Produces(MediaType.APPLICATION_JSON) public List<String> listarTiposNotificacao() { return notificationServices.stream() .map(NotificationService::getTipoNotificacao) .collect(Collectors.toList()); } @POST @Path("/all") @Consumes(MediaType.APPLICATION_JSON) public String enviarParaTodos(NotificationRequest request) { notificationServices.forEach(service -> service.enviarNotificacao(request.getDestinatario(), request.getMensagem()) ); return "Notificação enviada por todos os canais!"; } } 🧪 Testando Nossa Aplicação
Adicione a extensão "rest-jsonb" ao seu projeto:
mvn quarkus:add-extension -Dextensions="rest-jsonb" Execute a aplicação em modo de desenvolvimento:
mvn compile quarkus:dev Teste os endpoints básicos:
# Saudação simples curl http://localhost:8080/hello # Saudação personalizada em português curl "http://localhost:8080/hello/personalizada?nome=João&idioma=pt" # Saudação personalizada em inglês curl "http://localhost:8080/hello/personalizada?nome=John&idioma=en" Teste os endpoints de notificação:
# Listar tipos de notificação disponíveis curl http://localhost:8080/notifications/tipos # Enviar email curl -X POST http://localhost:8080/notifications/email \ -H "Content-Type: application/json" \ -d '{"destinatario":"joao@email.com","mensagem":"Olá do Quarkus!"}' # Enviar SMS curl -X POST http://localhost:8080/notifications/sms \ -H "Content-Type: application/json" \ -d '{"destinatario":"11999999999","mensagem":"Olá do Quarkus!"}' # Enviar para todos os canais curl -X POST http://localhost:8080/notifications/all \ -H "Content-Type: application/json" \ -d '{"destinatario":"contato","mensagem":"Mensagem broadcast!"}' 🧪 Testando com Interfaces: A Mágica dos Mocks
Com interfaces, criar testes fica muito mais simples:
package com.example.resource; import io.quarkus.test.junit.QuarkusTest; import jakarta.ws.rs.core.MediaType; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.containsString; @QuarkusTest class NotificationResourceTest { @Test void testEmailNotification() { // O Quarkus facilita a criação de mocks para interfaces String json = """ { "destinatario": "test@email.com", "mensagem": "Teste" } """; given() .contentType(MediaType.APPLICATION_JSON) .body(json) .when() .post("/notifications/email") .then() .statusCode(200) .body(containsString("Email enviado com sucesso")); } } 🎓 O Poder da Arquitetura CDI
O que acabamos de construir demonstra os princípios fundamentais de uma arquitetura bem estruturada:
Separação de Responsabilidades
-
GreetingService: Especializado em criar saudações -
RequestCounterService: Especializado em contar requisições -
NotificationService: Interface para diferentes tipos de notificação -
GreetingResource: Especializado em expor endpoints REST
Baixo Acoplamento
Cada classe depende apenas de abstrações (interfaces), não de implementações concretas. Se precisarmos mudar de email para webhook, alteramos apenas a implementação da interface NotificationService.
Alta Testabilidade
Com CDI, criar testes unitários fica muito mais simples. Podemos facilmente criar mocks dos serviços:
@Test void testGreetingResource() { // O CDI permite injetar mocks facilmente em testes GreetingService mockService = Mockito.mock(GreetingService.class); // ... resto do teste } Vantagens das Interfaces no CDI
- 🧪 Testabilidade Suprema: Fácil criação de mocks
- 🔄 Flexibilidade: Troque implementações sem alterar código cliente
- 📏 SOLID Principles: Dependency Inversion Principle na prática
- 🎭 Polimorfismo: Uma interface, múltiplas implementações
🚀 Otimizações do Quarkus
Uma das grandes vantagens do Quarkus é como ele otimiza o CDI:
- Build Time Processing: Muito do trabalho de injeção acontece em tempo de build, não em runtime
- Startup Rápido: Menos processamento na inicialização = startup mais rápido
- Menor Consumo de Memória: Footprint reduzido comparado a frameworks tradicionais
💡 Dicas Importantes
⚠️ Cuidados com Escopos
-
@ApplicationScoped: Use para serviços stateless ou com estado thread-safe -
@RequestScoped: Perfeito quando precisar de estado por requisição -
@Singleton: Reserve para recursos críticos que devem ter instância única - Thread Safety:
@ApplicationScopede@Singletonprecisam ser thread-safe se mantiverem estado
Exemplo de Thread Safety:
@ApplicationScoped public class ContadorGlobalService { private final AtomicInteger contador = new AtomicInteger(0); public int incrementar() { return contador.incrementAndGet(); // Thread-safe } } 🔍 Debugging CDI
Se você encontrar problemas com injeção, use:
mvn compile quarkus:dev -Dquarkus.arc.debug=true 🎯 Próximos Passos
No próximo capítulo, vamos aprender como configurar nossas aplicações Quarkus de forma profissional, explorando arquivos de propriedades, profiles e configurações externalizadas.
🔗 Continue a Jornada
👉 Capítulo 4: Configuração de Aplicações Quarkus - Aprenda a configurar sua aplicação como um profissional
🤝 Vamos Conversar!
O que você achou da injeção de dependências com CDI? Já teve experiência com outros frameworks de DI? Compartilhe sua experiência nos comentários!
Se este conteúdo foi útil para você:
- 👍 Deixe seu like
- 💬 Comente suas dúvidas ou sugestões
- 🔄 Compartilhe com outros desenvolvedores
- 👥 Me siga para não perder os próximos capítulos
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.