Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Interface: NotificadorService"] --> B["Impl 1: EmailNotificador (@Qualifier)"]
    A --> C["Impl 2: SmsNotificador (@Qualifier)"]
    B --> D["Injeção por Construtor no Consumidor"]
    C --> D
    D --> E["Bean Scope: singleton (padrão) vs prototype"]
    E --> F["Ciclo de Vida: @PostConstruct / @PreDestroy"]

    subgraph ARQ["Arquitetura do Capítulo"]
        G["Constructor Injection > Field Injection"]
        H["Desambiguação: @Primary e @Qualifier"]
        I["Escopos e Ciclo de Vida de Beans"]
    end

    D --> ARQ

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px

🏛️ 1. Fundamentos Técnicos: Injeção de Dependências, Desambiguação e Escopos

Por que Constructor Injection é a prática recomendada. O Spring oferece três formas de injeção: por campo (@Autowired direto no atributo), por setter e por construtor. A injeção por campo é a mais comum em tutoriais, mas é a pior prática em produção: torna a classe impossível de instanciar sem o container (quebra testes unitários puros), esconde dependências obrigatórias e permite objetos em estado inconsistente (o campo fica null até o Spring processar a reflexão). A injeção por construtor resolve tudo isso — o campo pode ser final, a dependência é explícita na assinatura, e o objeto nasce sempre completo:

@Service
public class PedidoService {
    private final PedidoRepository repository; // final: imutável e obrigatório

    public PedidoService(PedidoRepository repository) {
        this.repository = repository;
    }
}

Quando há um único construtor, o @Autowired é opcional — o Spring o injeta automaticamente desde a versão 4.3.

Desambiguação com @Primary e @Qualifier. Quando duas ou mais classes implementam a mesma interface (NotificacaoEmailService e NotificacaoSmsService implementando NotificadorService), o Spring não sabe qual injetar e lança NoUniqueBeanDefinitionException. Duas soluções:

  • @Primary na implementação padrão — usada quando um dos Beans deve ser a escolha na maioria dos casos.
  • @Qualifier("nomeDoBean") no ponto de injeção — explícito e preferível quando ambas as implementações são usadas ativamente em contextos diferentes.
public PedidoService(@Qualifier("smsNotificador") NotificadorService notificador) { ... }

Bean Scopes. Por padrão, todo @Bean/@Component é singleton: uma única instância compartilhada por todo o ApplicationContext, criada uma vez e reutilizada — essencial para Services e Repositories, que devem ser stateless. O escopo prototype cria uma nova instância a cada injeção/getBean() — usado para objetos com estado mutável por execução (ex.: builders de contexto, objetos de sessão de negócio). Um erro comum é injetar um Bean prototype dentro de um singleton por construtor: como o singleton só é construído uma vez, ele recebe apenas uma instância do prototype para sempre — é preciso usar ObjectProvider<T> ou um scoped-proxy para obter uma nova instância a cada chamada.

Ciclo de vida com @PostConstruct/@PreDestroy. Após a injeção de todas as dependências, o Spring invoca métodos anotados com @PostConstruct — o lugar correto para pré-carregar caches ou validar configuração, nunca no construtor (que roda antes da injeção terminar). Simetricamente, @PreDestroy executa antes do Bean ser removido do contexto (shutdown da aplicação), ideal para fechar conexões e liberar recursos externos.


💻 2. Código de Demonstração Corporativo

package com.empresa.api.notificacao;

public interface NotificadorService {
    void enviar(String destinatario, String mensagem);
}
package com.empresa.api.notificacao;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Primary
@Service("emailNotificador")
public class EmailNotificadorService implements NotificadorService {
    @Override
    public void enviar(String destinatario, String mensagem) {
        System.out.println("[EMAIL] Para " + destinatario + ": " + mensagem);
    }
}
package com.empresa.api.notificacao;

import org.springframework.stereotype.Service;

@Service("smsNotificador")
public class SmsNotificadorService implements NotificadorService {
    @Override
    public void enviar(String destinatario, String mensagem) {
        System.out.println("[SMS] Para " + destinatario + ": " + mensagem);
    }
}
package com.empresa.api.pedido;

import com.empresa.api.notificacao.NotificadorService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class PedidoService {

    private final NotificadorService notificador;

    public PedidoService(@Qualifier("smsNotificador") NotificadorService notificador) {
        this.notificador = notificador;
    }

    @PostConstruct
    void aoIniciar() {
        System.out.println("PedidoService pronto — cache de regras carregado.");
    }

    public void finalizarPedido(Long id, String contatoCliente) {
        notificador.enviar(contatoCliente, "Pedido " + id + " confirmado!");
    }

    @PreDestroy
    void aoEncerrar() {
        System.out.println("PedidoService encerrando — liberando recursos.");
    }
}
// Bean com escopo prototype: uma instância nova a cada injeção/getBean()
package com.empresa.api.pedido;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ContextoDeCalculoPedido {
    private double totalAcumulado;

    public void somar(double valor) {
        this.totalAcumulado += valor;
    }

    public double getTotalAcumulado() {
        return totalAcumulado;
    }
}