DEV Community

Cover image for Capítulo 3: Injeção de Dependências com CDI no Quarkus
Eduardo R. Ferreira
Eduardo R. Ferreira

Posted on

Capítulo 3: Injeção de Dependências com CDI no Quarkus

📚 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; } } 
Enter fullscreen mode Exit fullscreen mode

🎯 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; } } 
Enter fullscreen mode Exit fullscreen mode

@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; } } 
Enter fullscreen mode Exit fullscreen mode

@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--; } } } 
Enter fullscreen mode Exit fullscreen mode

@Dependent - O Dependente

Vive e morre junto com quem o utiliza.

@Dependent public class UtilService { public String formatarTexto(String texto) { return texto.toUpperCase().trim(); } } 
Enter fullscreen mode Exit fullscreen mode

🔍 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 } 
Enter fullscreen mode Exit fullscreen mode

💉 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); }; } } 
Enter fullscreen mode Exit fullscreen mode

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()); } } 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

🎭 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 } } 
Enter fullscreen mode Exit fullscreen mode

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"; } } 
Enter fullscreen mode Exit fullscreen mode

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 { } 
Enter fullscreen mode Exit fullscreen mode
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 { } 
Enter fullscreen mode Exit fullscreen mode

Agora marcamos nossas implementações:

@Email @ApplicationScoped public class EmailNotificationService implements NotificationService { // ... implementação } @Sms @ApplicationScoped public class SmsNotificationService implements NotificationService { // ... implementação } 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

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!"; } } 
Enter fullscreen mode Exit fullscreen mode

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!"; } } 
Enter fullscreen mode Exit fullscreen mode

🧪 Testando Nossa Aplicação

Adicione a extensão "rest-jsonb" ao seu projeto:

mvn quarkus:add-extension -Dextensions="rest-jsonb" 
Enter fullscreen mode Exit fullscreen mode

Execute a aplicação em modo de desenvolvimento:

mvn compile quarkus:dev 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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!"}' 
Enter fullscreen mode Exit fullscreen mode

🧪 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")); } } 
Enter fullscreen mode Exit fullscreen mode

🎓 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 } 
Enter fullscreen mode Exit fullscreen mode

Vantagens das Interfaces no CDI

  1. 🧪 Testabilidade Suprema: Fácil criação de mocks
  2. 🔄 Flexibilidade: Troque implementações sem alterar código cliente
  3. 📏 SOLID Principles: Dependency Inversion Principle na prática
  4. 🎭 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: @ApplicationScoped e @Singleton precisam 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 } } 
Enter fullscreen mode Exit fullscreen mode

🔍 Debugging CDI

Se você encontrar problemas com injeção, use:

mvn compile quarkus:dev -Dquarkus.arc.debug=true 
Enter fullscreen mode Exit fullscreen mode

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