No Spring, @Qualifier não é um detalhe. É parte da modelagem
Existe um momento comum em aplicações Spring:
o projeto tem mais de uma implementação para a mesma interface, o container não sabe qual Bean injetar e alguém resolve com @Qualifier.
A aplicação volta a subir.
O erro desaparece.
O serviço recebe a implementação esperada.
Mas a pergunta importante continua ali:
por que aquele ponto de injeção precisava daquela implementação específica?
Essa pergunta não é detalhe técnico.
Ela é modelagem.
Quando existem várias formas válidas de cumprir um contrato, escolher uma delas faz parte do desenho da aplicação.
@Qualifier não deveria ser tratado como fita isolante para o container.
Ele deveria tornar explícita uma decisão que já existe no domínio, no caso de uso ou na infraestrutura.
O problema aparece quando o tipo não conta a história inteira
Imagine uma aplicação que precisa calcular frete.
Existe uma interface:
public interface CalculadoraFrete {
BigDecimal calcular(Pedido pedido);
}
E duas implementações:
@Service
public class CalculadoraFreteCorreios implements CalculadoraFrete {
@Override
public BigDecimal calcular(Pedido pedido) {
// calcula usando tabela dos Correios
}
}
@Service
public class CalculadoraFreteTransportadora implements CalculadoraFrete {
@Override
public BigDecimal calcular(Pedido pedido) {
// calcula usando contrato da transportadora
}
}
Agora um serviço depende de CalculadoraFrete:
@Service
public class FecharPedidoService {
private final CalculadoraFrete calculadoraFrete;
public FecharPedidoService(CalculadoraFrete calculadoraFrete) {
this.calculadoraFrete = calculadoraFrete;
}
}
Para o Java, o tipo parece suficiente.
Para o Spring, não é.
Existem dois Beans compatíveis.
O ponto de injeção diz apenas:
"preciso de uma calculadora de frete".
Mas a aplicação tem duas.
O container não consegue adivinhar qual delas representa a decisão correta para aquele caso de uso.
E ele não deveria adivinhar mesmo.
Quando o tipo abstrato é amplo demais, a escolha da implementação passa a carregar significado.
Esse significado precisa aparecer em algum lugar.
@Qualifier dá nome para uma escolha
Uma forma direta de resolver a ambiguidade é nomear os Beans e declarar qual deles o serviço quer:
@Service("freteCorreios")
public class CalculadoraFreteCorreios implements CalculadoraFrete {
@Override
public BigDecimal calcular(Pedido pedido) {
// calcula usando tabela dos Correios
}
}
@Service("freteTransportadora")
public class CalculadoraFreteTransportadora implements CalculadoraFrete {
@Override
public BigDecimal calcular(Pedido pedido) {
// calcula usando contrato da transportadora
}
}
E então:
@Service
public class FecharPedidoService {
private final CalculadoraFrete calculadoraFrete;
public FecharPedidoService(
@Qualifier("freteCorreios") CalculadoraFrete calculadoraFrete
) {
this.calculadoraFrete = calculadoraFrete;
}
}
Agora o Spring sabe qual Bean usar.
Mas a parte mais importante não é o Spring saber.
É o leitor saber.
O construtor deixou de dizer apenas "preciso de uma calculadora".
Ele passou a dizer:
"este serviço fecha pedido usando a calculadora dos Correios".
Isso muda a leitura do código.
A dependência deixa de ser uma abstração genérica e passa a revelar uma variante concreta do processo.
Se essa escolha é relevante para o comportamento do caso de uso, ela não é ruído.
Ela é parte do contrato daquele componente.
O erro é usar @Qualifier para esconder uma modelagem fraca
O problema começa quando @Qualifier vira uma correção automática para qualquer ambiguidade.
O time vê o erro:
required a single bean, but 2 were found
E responde assim:
public FecharPedidoService(
@Qualifier("calculadoraFreteCorreios") CalculadoraFrete calculadoraFrete
) {
this.calculadoraFrete = calculadoraFrete;
}
Sem discutir se o serviço deveria depender dessa implementação.
Sem discutir se existem dois casos de uso diferentes.
Sem discutir se a escolha deveria ser feita em tempo de execução.
Sem discutir se a interface está genérica demais.
O código fica tecnicamente correto, mas conceitualmente pobre.
Às vezes o problema não é falta de @Qualifier.
É uma abstração que juntou coisas diferentes sob o mesmo nome.
Por exemplo:
public interface Notificador {
void enviar(Mensagem mensagem);
}
Se existem NotificadorEmail, NotificadorSms, NotificadorPush e NotificadorWhatsApp, talvez o ponto principal não seja escolher um Bean.
Talvez a pergunta real seja:
qual canal este caso de uso representa?
Ou:
o canal é uma regra fixa do fluxo ou uma decisão calculada a partir do cliente, do pedido ou da configuração?
Essas duas respostas levam a desenhos diferentes.
@Qualifier resolve apenas uma delas.
Quando a escolha é fixa, o Qualifier pode deixar o contrato mais honesto
Se um caso de uso sempre precisa de uma implementação específica, @Qualifier pode ser uma boa forma de explicitar isso.
Imagine um serviço que envia um código de recuperação de senha apenas por e-mail:
public interface EnviadorMensagem {
void enviar(Destinatario destinatario, String texto);
}
@Service("email")
public class EnviadorEmail implements EnviadorMensagem {
@Override
public void enviar(Destinatario destinatario, String texto) {
// envia e-mail
}
}
@Service("sms")
public class EnviadorSms implements EnviadorMensagem {
@Override
public void enviar(Destinatario destinatario, String texto) {
// envia SMS
}
}
O caso de uso pode ser explícito:
@Service
public class RecuperarSenhaService {
private final EnviadorMensagem enviadorEmail;
public RecuperarSenhaService(
@Qualifier("email") EnviadorMensagem enviadorEmail
) {
this.enviadorEmail = enviadorEmail;
}
}
Aqui o @Qualifier não está escondendo uma escolha.
Ele está documentando uma escolha fixa.
O serviço não quer "qualquer enviador".
Ele quer o enviador de e-mail.
Nomear o atributo como enviadorEmail reforça a mesma ideia.
Isso parece pequeno, mas evita uma leitura falsa do código.
Se o atributo se chama enviadorMensagem, parece que qualquer canal serve.
Se o atributo se chama enviadorEmail, o próprio objeto declara sua expectativa.
Esse cuidado é importante porque injeção de dependência também comunica desenho.
Quando a escolha é dinâmica, Qualifier no construtor pode estar no lugar errado
Agora imagine outro caso:
o usuário escolhe se quer receber notificação por e-mail, SMS ou WhatsApp.
Nesse cenário, o serviço não deveria injetar uma implementação fixa com @Qualifier.
Porque a escolha não pertence ao bootstrap da aplicação.
Ela pertence ao fluxo de execução.
Um desenho melhor pode usar um resolvedor:
public enum CanalNotificacao {
EMAIL,
SMS,
WHATSAPP
}
public interface EnviadorMensagem {
CanalNotificacao canal();
void enviar(Destinatario destinatario, String texto);
}
@Component
public class EnviadorMensagemResolver {
private final Map<CanalNotificacao, EnviadorMensagem> enviadores;
public EnviadorMensagemResolver(List<EnviadorMensagem> enviadores) {
this.enviadores = enviadores.stream()
.collect(Collectors.toMap(EnviadorMensagem::canal, Function.identity()));
}
public EnviadorMensagem resolver(CanalNotificacao canal) {
EnviadorMensagem enviador = enviadores.get(canal);
if (enviador == null) {
throw new IllegalArgumentException("Canal não suportado: " + canal);
}
return enviador;
}
}
E o caso de uso decide com base na entrada:
@Service
public class EnviarNotificacaoService {
private final EnviadorMensagemResolver resolver;
public EnviarNotificacaoService(EnviadorMensagemResolver resolver) {
this.resolver = resolver;
}
public void enviar(SolicitacaoNotificacao solicitacao) {
EnviadorMensagem enviador = resolver.resolver(solicitacao.canal());
enviador.enviar(solicitacao.destinatario(), solicitacao.texto());
}
}
Perceba a diferença.
Aqui não existe uma implementação certa para o serviço inteiro.
Existe uma implementação certa para cada solicitação.
Colocar @Qualifier("email") no construtor seria uma mentira de modelagem.
O serviço não depende de e-mail.
Ele depende de uma regra de seleção de canal.
Quando a escolha é dinâmica, o código precisa modelar a escolha.
Não apenas escolher um Bean na largada.
@Qualifier também pode revelar nomes ruins
Um sinal de alerta aparece quando o nome do qualifier precisa explicar demais:
@Qualifier("clienteHttpPedidoComTimeoutMaiorParaIntegracaoLegada")
Esse tipo de nome costuma indicar que a variação é mais rica do que uma simples etiqueta.
Talvez exista uma política de timeout.
Talvez exista um gateway para sistema legado.
Talvez exista um cliente específico para um bounded context.
Talvez o problema não seja "qual cliente HTTP injetar", mas "qual colaboração este caso de uso realmente tem".
Compare:
public ProcessarPedidoService(
@Qualifier("clienteHttpPedidoComTimeoutMaiorParaIntegracaoLegada")
PedidoClient pedidoClient
) {
this.pedidoClient = pedidoClient;
}
Com:
public ProcessarPedidoService(PedidoLegadoGateway pedidoLegadoGateway) {
this.pedidoLegadoGateway = pedidoLegadoGateway;
}
No segundo caso, o tipo já carrega intenção.
O Spring não precisa de uma etiqueta para diferenciar duas coisas que o modelo tratou como iguais.
Isso não significa criar interfaces para tudo.
Significa perceber quando duas dependências têm responsabilidades diferentes o suficiente para merecer nomes diferentes.
Às vezes o melhor @Qualifier é um tipo mais específico.
Annotation customizada pode ser melhor que string solta
Outro problema comum é espalhar strings pelo código:
@Qualifier("email")
@Qualifier("sms")
@Qualifier("whatsapp")
Funciona.
Mas strings são frágeis.
Um typo vira erro de configuração.
Uma renomeação pode passar batida.
Em cenários importantes, uma annotation customizada deixa a intenção mais forte:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
public @interface CanalEmail {
}
Uso na implementação:
@Service
@CanalEmail
public class EnviadorEmail implements EnviadorMensagem {
}
Uso no ponto de injeção:
public RecuperarSenhaService(@CanalEmail EnviadorMensagem enviadorEmail) {
this.enviadorEmail = enviadorEmail;
}
Agora a escolha virou um conceito nomeado no código.
Não é só uma string que precisa coincidir.
É uma marca explícita da aplicação.
Esse recurso deve ser usado com critério.
Se cada bean ganhar uma annotation própria sem motivo, o projeto só troca confusão por cerimônia.
Mas quando a variação é central e aparece em vários pontos, uma annotation semântica pode ser mais clara do que repetir nomes soltos.
@Primary e @Qualifier contam histórias diferentes
No post sobre @Primary, a ideia principal era que @Primary escolhe um vencedor padrão.
@Qualifier faz outra coisa.
Ele escolhe explicitamente um candidato.
Com @Primary, o ponto de injeção continua dizendo:
"me dê o padrão".
Com @Qualifier, o ponto de injeção diz:
"me dê este".
Essa diferença é grande.
Use @Primary quando existe um padrão real para a aplicação.
Use @Qualifier quando aquele componente precisa de uma variante específica.
Use outro desenho quando a escolha depende do fluxo, do usuário, do pedido, do tenant, do país ou de qualquer dado que só aparece em tempo de execução.
O erro é tratar tudo como se fosse a mesma decisão.
Não é.
Ambiguidade de bean pode ser sintoma de várias coisas:
- existe um padrão técnico;
- existe uma dependência específica;
- existe uma decisão dinâmica;
- existe uma abstração genérica demais;
- existe uma responsabilidade mal nomeada.
Cada caso pede uma resposta diferente.
O que observar antes de adicionar um Qualifier
Antes de colocar @Qualifier, vale fazer algumas perguntas:
- este serviço precisa sempre desta implementação?
- o nome do atributo deixa essa escolha clara?
- a escolha pertence ao bootstrap ou ao fluxo de execução?
- existe uma regra de negócio escolhendo entre variantes?
- um tipo mais específico comunicaria melhor a intenção?
- estou usando
@Qualifierpara revelar uma decisão ou para esconder uma ambiguidade?
Essas perguntas evitam um erro comum:
resolver o container antes de entender o modelo.
Spring consegue injetar quase qualquer coisa quando você dá nomes suficientes.
Mas fazer o container encontrar um Bean não significa que o desenho ficou claro.
Clareza vem de dependências que dizem o que são, por que existem e qual papel cumprem no caso de uso.
@Qualifier pode ajudar nisso.
Mas só quando ele é consequência de uma decisão bem entendida.
O ponto principal
@Qualifier não é apenas um detalhe para satisfazer o Spring.
Ele é uma declaração de intenção.
Quando um componente pede uma implementação específica, essa escolha passa a fazer parte do contrato daquele componente.
Se a escolha é fixa, deixe isso explícito.
Se a escolha é dinâmica, modele a seleção.
Se o qualifier parece carregar uma frase inteira, talvez falte um tipo ou conceito melhor.
O Spring não está só reclamando de dois Beans.
Ele está mostrando que o seu modelo tem mais de uma resposta possível.
E quando existem várias respostas possíveis, a pior decisão é fingir que isso é só configuração.
