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.