📚 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:
@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
}
}
🔍 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.