Aula 4: CDI — Injeção de Dependências e Escopos
Objetivos
- Compreender o CDI (Contexts and Dependency Injection) e seus mecanismos de injeção de dependências
- Aplicar escopos de beans (
@ApplicationScoped,@RequestScoped,@SessionScoped,@Dependent) - Implementar a separação em camadas (Resource → Service → Repository) com CDI
- Utilizar CDI para desacoplar componentes e facilitar a evolução do sistema
- Construir uma aplicação com múltiplas camadas integradas via injeção de dependências
Contexto e Motivação
O CDI (Contexts and Dependency Injection) foi criado para resolver problemas clássicos de acoplamento e complexidade em sistemas Java. Ele permite que o container (Quarkus, Jakarta EE) crie e conecte os objetos automaticamente, usando anotações simples. Isso facilita a manutenção, a evolução e principalmente os testes automatizados.
Antes do CDI, desenvolvedores Java precisavam criar e conectar manualmente os objetos do sistema, usando o operador new e gerenciando dependências por conta própria. Isso tornava o código rígido, difícil de testar e de evoluir. Imagine uma orquestra sem maestro: cada músico toca por conta própria, sem coordenação. O CDI surge como o "maestro" que organiza, injeta e controla os objetos (beans) do sistema, tornando tudo mais flexível, modular e testável.
Por que não usar apenas new? Porque isso cria dependências rígidas e dificulta a troca de implementações, a simulação de cenários de teste e a evolução do sistema. O CDI permite que você troque implementações sem mudar o código principal.
Testes automatizados são o "airbag" do desenvolvimento profissional de software. Assim como um carro moderno não é seguro sem sistemas de proteção, um sistema sem testes está vulnerável a falhas inesperadas e regressões. Testar não é apenas encontrar erros, mas garantir que o software evolua com confiança e qualidade.
Testes automatizados são essenciais para:
- Prevenir regressões: Mudanças futuras não quebram funcionalidades existentes
- Facilitar refatorações: Permite melhorar o código sem medo de introduzir bugs
- Documentar comportamento: Testes servem como exemplos vivos de uso das APIs
- Aumentar a confiança: Equipes podem entregar mais rápido e com menos erros
Dúvida comum: "Se eu usar CDI, preciso aprender tudo de uma vez?" Não! Você pode começar usando apenas @Inject e escopos básicos, evoluindo aos poucos para producers, qualifiers e cenários avançados.
No contexto do Quarkus 3.31, o suporte a testes é nativo e integrado, utilizando ferramentas modernas como JUnit 5 e REST Assured. Além disso, o CDI potencializa os testes ao permitir a injeção de dependências e mocks, tornando o código mais flexível e testável.
Conteúdo
O que é CDI e por que ele existe?
CDI foi criado para resolver o problema do acoplamento excessivo e da dificuldade de testar sistemas Java. Ele implementa o padrão de Inversão de Controle (IoC), onde o container injeta as dependências automaticamente, em vez de você instanciá-las manualmente.
Inversão de Controle significa que você entrega o "controle" da criação dos objetos para o container. Isso permite trocar implementações, simular cenários de teste e evoluir o sistema sem mexer no código principal.
Como CDI funciona na prática?
- Bean: Qualquer classe gerenciada pelo CDI. Pode ser um serviço, recurso REST, utilitário, etc.
- @Inject: Anotação que pede ao CDI para injetar uma dependência automaticamente
- Escopo: Define quanto tempo o bean existe (application, request, session, etc.)
- Producer: Método ou campo que cria beans personalizados para injeção
- Qualifier: Permite diferenciar beans do mesmo tipo
| Anotação | Função | Exemplo |
|---|---|---|
@Inject | Injeta dependência | @Inject ProdutoService service; |
@ApplicationScoped | Bean único para toda a aplicação | @ApplicationScoped class ProdutoService {} |
@RequestScoped | Bean novo a cada requisição | @RequestScoped class PedidoResource {} |
@Produces | Cria bean customizado | @Produces ProdutoService createService() {...} |
@Qualifier | Diferencia beans do mesmo tipo | @Qualifier @Premium ProdutoService |
Exemplo Prático: Injeção de Dependência:
// Serviço simples gerenciado pelo CDI
@ApplicationScoped
public class ProdutoService {
public String buscarNomeProduto(Long id) {
return "Produto Exemplo";
}
}
// Recurso REST que recebe o serviço via CDI
@RequestScoped
public class ProdutoResource {
@Inject
ProdutoService produtoService;
public String getNome(Long id) {
return produtoService.buscarNomeProduto(id);
}
}
Dúvida comum: "Preciso de configuração extra para CDI?" Não! No Quarkus, basta usar as anotações e o container faz todo o trabalho.
Ciclo de Vida dos Beans e Escopos
| Escopo | Duração | Uso Típico |
|---|---|---|
@ApplicationScoped | Toda a vida da aplicação | Serviços compartilhados, caches |
@RequestScoped | Uma requisição HTTP | Recursos REST, dados temporários |
@SessionScoped | Sessão do usuário | Dados de login, carrinho de compras |
@Dependent | Igual ao alvo da injeção | Utilitários, beans transitórios |
O ciclo de vida dos beans CDI é gerenciado automaticamente pelo container. Quando um bean é necessário, o container o instancia, injeta suas dependências, mantém ele ativo enquanto seu escopo existir e o destrói quando apropriado.
CDI e Testabilidade
Uma das grandes vantagens do CDI é facilitar testes automatizados. Quando você injeta dependências em vez de instanciá-las com new, pode facilmente trocar implementações reais por versões de teste (mocks).
| Tipo de Teste | Como CDI ajuda |
|---|---|
| Teste Unitário | Permite injetar dependências simuladas, testando cada classe isoladamente |
| Teste de Integração | Facilita a troca de implementações para testar cenários reais e simulados |
Na próxima aula de testes automatizados, aprofundaremos as ferramentas e técnicas de teste. Aqui, o importante é entender que o CDI é o que torna possível testar camadas isoladamente.
Laboratório 3 — Arquitetura em Camadas com CDI
Neste laboratório, vamos refatorar a aplicação do Laboratório 2 para demonstrar o poder do CDI na construção de uma arquitetura em camadas desacoplada.
Objetivo
Separar a aplicação em três camadas (Resource → Service → Repository) conectadas exclusivamente via CDI, demonstrando o desacoplamento e a flexibilidade que a injeção de dependências proporciona.
Pré-requisitos
- Projeto do Laboratório 2 funcionando (ProdutoService e ProdutoResource). Copie o projeto e modifique o nome do artefato para
lab3-cdinopom.xml - JDK 21 ou superior instalado
- Quarkus CLI ou Maven configurado
Visão Geral da Arquitetura
Passo 1: Criando a Camada de Repositório
Por que uma Camada de Repositório?
Em aplicações reais, os dados geralmente vêm de bancos de dados, APIs externas ou arquivos de configuração. A camada de repositório isola a lógica de acesso a dados, permitindo que:
- A camada de serviço foque apenas na lógica de negócio
- Possamos trocar fontes de dados sem afetar outras camadas
- Testemos cada camada independentemente
Passo 1.1: Implementando ProdutoRepository
Crie o pacote repository em src/main/java/br/upf/ads175:
package br.upf.ads175.repository;
import br.upf.ads175.dto.CategoriaDTO;
import br.upf.ads175.dto.ProdutoDTO;
import jakarta.enterprise.context.ApplicationScoped;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
@ApplicationScoped // Bean CDI gerenciado como singleton
public class ProdutoRepository {
/**
* Simula nossa "base de dados" — em uma aplicação real,
* isso seria substituído por JPA/Hibernate
*/
private List<ProdutoDTO> obterTodosProdutos() {
return List.of(
new ProdutoDTO(1L, "Notebook Gamer", new BigDecimal("8500.00"), true, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(2L, "Cadeira de Escritório", new BigDecimal("1200.50"), true, new CategoriaDTO("Móveis")),
new ProdutoDTO(3L, "Monitor 4K", new BigDecimal("2300.00"), false, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(4L, "Mesa de Escritório", new BigDecimal("850.00"), true, new CategoriaDTO("Móveis")),
new ProdutoDTO(5L, "Teclado Mecânico", new BigDecimal("450.00"), true, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(6L, "Smartphone Premium", new BigDecimal("3500.00"), true, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(7L, "Luminária LED", new BigDecimal("150.00"), false, new CategoriaDTO("Iluminação")),
new ProdutoDTO(8L, "Cabo USB-C", new BigDecimal("45.00"), true, new CategoriaDTO("Eletrônicos"))
);
}
/** Equivalente a SELECT * FROM produtos */
public List<ProdutoDTO> findAll() {
return obterTodosProdutos();
}
/** Equivalente a SELECT * FROM produtos WHERE id = ? */
public Optional<ProdutoDTO> findById(Long id) {
return obterTodosProdutos().stream()
.filter(produto -> produto.id().equals(id))
.findFirst();
}
/** Equivalente a SELECT * FROM produtos WHERE ativo = ? */
public List<ProdutoDTO> findByAtivo(boolean ativo) {
return obterTodosProdutos().stream()
.filter(produto -> produto.ativo() == ativo)
.toList();
}
/** Equivalente a SELECT * FROM produtos p JOIN categorias c ON... */
public List<ProdutoDTO> findByCategoria(String nomeCategoria) {
return obterTodosProdutos().stream()
.filter(produto -> produto.categoria().nome().equalsIgnoreCase(nomeCategoria))
.toList();
}
/** Equivalente a SELECT * WHERE preco BETWEEN ? AND ? */
public List<ProdutoDTO> findByPrecoEntre(BigDecimal precoMin, BigDecimal precoMax) {
return obterTodosProdutos().stream()
.filter(produto -> produto.preco().compareTo(precoMin) >= 0
&& produto.preco().compareTo(precoMax) <= 0)
.toList();
}
/** Equivalente a SELECT COUNT(*) WHERE categoria = ? */
public long countByCategoria(String nomeCategoria) {
return obterTodosProdutos().stream()
.filter(produto -> produto.categoria().nome().equalsIgnoreCase(nomeCategoria))
.count();
}
}
Vantagens da Separação: O ProdutoService foca na lógica de negócio, o ProdutoRepository isola o acesso a dados. Se mudarmos de dados mock para banco real, só mexemos no repositório.
Passo 1.2: Refatorando o ProdutoService
package br.upf.ads175.service;
import br.upf.ads175.dto.ProdutoDTO;
import br.upf.ads175.repository.ProdutoRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@ApplicationScoped
public class ProdutoService {
@Inject // CDI injeta automaticamente o repositório
ProdutoRepository repository;
public List<ProdutoDTO> buscarProdutosAtivosOrdenadosPorNome() {
return repository.findByAtivo(true)
.stream()
.sorted(Comparator.comparing(ProdutoDTO::nome))
.toList();
}
public Optional<ProdutoDTO> buscarPorId(Long id) {
return repository.findById(id);
}
public Map<String, List<String>> buscarNomesProdutosAgrupadosPorCategoria() {
return repository.findAll()
.stream()
.collect(Collectors.groupingBy(
produto -> produto.categoria().nome(),
Collectors.mapping(ProdutoDTO::nome, Collectors.toList())
));
}
public List<ProdutoDTO> buscarProdutosPremium() {
return repository.findByAtivo(true)
.stream()
.filter(ProdutoDTO::isPremium)
.sorted(Comparator.comparing(ProdutoDTO::preco).reversed())
.toList();
}
public Map<String, Map<String, Object>> obterEstatisticasProdutosPorCategoria() {
return repository.findByAtivo(true)
.stream()
.collect(Collectors.groupingBy(
produto -> produto.categoria().nome(),
Collectors.collectingAndThen(
Collectors.toList(),
produtos -> Map.of(
"quantidade", produtos.size(),
"precoMedio", produtos.stream()
.map(ProdutoDTO::preco)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(produtos.size()), RoundingMode.HALF_UP),
"precoMaximo", produtos.stream()
.map(ProdutoDTO::preco)
.max(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO)
)
)
));
}
public List<ProdutoDTO> buscarProdutosPorCategoria(String categoria) {
return repository.findByCategoria(categoria)
.stream()
.filter(ProdutoDTO::ativo)
.sorted(Comparator.comparing(ProdutoDTO::nome))
.toList();
}
public List<ProdutoDTO> buscarProdutosPorFaixaPreco(BigDecimal precoMin, BigDecimal precoMax) {
return repository.findByPrecoEntre(precoMin, precoMax)
.stream()
.filter(ProdutoDTO::ativo)
.sorted(Comparator.comparing(ProdutoDTO::preco))
.toList();
}
}
Note como a separação em camadas torna o código mais limpo: o Serviço focou na lógica de negócio (ordenação, filtros, cálculos) enquanto o Repositório cuida do acesso aos dados. Isso facilitará muito nossos testes!
Passo 2: Verificando o CDI em Ação
Execute a aplicação e verifique que as camadas estão funcionando integradas via CDI:
quarkus dev
Teste os endpoints — eles devem continuar funcionando exatamente como antes, mas agora a arquitetura interna está desacoplada:
curl -s http://localhost:8080/produtos | jq
curl -s http://localhost:8080/produtos/1 | jq
curl -s http://localhost:8080/produtos/premium | jq
A beleza da injeção de dependências é que o ProdutoResource não sabe (e nem precisa saber) como os dados são obtidos. Se amanhã trocarmos o repositório de dados mock para um banco de dados real, o Resource e o Service continuam exatamente iguais.
Passo 3: Experimentando Escopos
Adicione logging aos construtores para observar o comportamento dos escopos:
@ApplicationScoped
public class ProdutoRepository {
public ProdutoRepository() {
System.out.println(">>> ProdutoRepository criado: " + System.identityHashCode(this));
}
// ... resto do código
}
Faça múltiplas requisições e observe nos logs: o ProdutoRepository (@ApplicationScoped) é criado uma única vez, enquanto um bean @RequestScoped seria recriado a cada requisição.
Exercícios (Checkpoints)
- Explique com suas palavras por que usar CDI (
@Inject) em vez denew ProdutoService()torna o código mais flexível. - Descreva a diferença entre
@ApplicationScopede@RequestScoped. Em que cenário cada um é mais adequado? - Refatore a aplicação para adicionar uma camada de
ProdutoRepository(caso não tenha feito no laboratório) e injete-a noProdutoService. - Crie um novo serviço
CategoriaServicecom escopo@ApplicationScopedque retorne as categorias únicas a partir do repositório de produtos, e injete-o em um novo endpointGET /categorias. - Adicione logging aos construtores dos beans e verifique quantas instâncias são criadas após 5 requisições consecutivas.