Qualificação de beans — @Qualifier, @Primary, @Profile
TL;DR
Quando o Spring encontra mais de um bean do mesmo tipo no contexto, ele lança
NoUniqueBeanDefinitionException.@Qualifieraponta qual bean usar pelo nome ou por anotação customizada;@Primaryelege um candidato padrão;@Profileregistra o bean apenas quando um perfil de ambiente está ativo. Os três mecanismos colaboram:@Profilefiltra quais beans existem,@Primarydefine o padrão entre os restantes, e@Qualifierescolhe um específico na injeção.
O que é
Qualificação de beans é o conjunto de mecanismos que o Spring usa para resolver ambiguidade de tipo durante a injeção de dependências. Quando o contêiner precisa injetar uma interface e há dois ou mais beans que a implementam, ele precisa de uma dica extra. Essa dica pode vir de:
@Qualifier— indicação direta no ponto de injeção ou no bean.@Primary— marcação no bean que deve ser preferido por padrão.@Profile— restrição que faz o bean existir apenas em certos ambientes.
Todos operam sobre o mesmo conceito central: o mapa de beans do ApplicationContext.
Por que importa
Aplicações reais raramente têm uma única implementação por interface. É comum ter:
- uma implementação real e um stub para testes;
- duas implementações de
PaymentGateway(ex.: Stripe e PayPal); - um
DataSourcede desenvolvimento (H2 em memória) e outro de produção (PostgreSQL).
Sem qualificação, o Spring falha em tempo de startup com NoUniqueBeanDefinitionException. Entender esses três mecanismos é obrigatório para qualquer código Spring moderadamente complexo.
Como funciona
Ambiguidade de tipo (NoUniqueBeanDefinitionException)
O erro ocorre quando o BeanFactory encontra mais de um bean compatível com o tipo requerido e não tem como decidir qual usar:
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.example.PaymentGateway' available:
expected single matching bean but found 2: stripeGateway, paypalGateway
O Spring não escolhe aleatoriamente — ele falha explicitamente. A resolução passa por um dos mecanismos abaixo.
@Qualifier (custom e por nome)
@Qualifier restringe a seleção de candidatos por tipo a um subconjunto identificado por um rótulo (string ou anotação customizada).
Por nome de bean — o valor do @Qualifier corresponde ao nome do bean (atributo value de @Component ou @Bean):
@Component("stripeGateway")
public class StripeGateway implements PaymentGateway { ... }
@Component("paypalGateway")
public class PaypalGateway implements PaymentGateway { ... }
@Service
public class CheckoutService {
private final PaymentGateway gateway;
public CheckoutService(@Qualifier("stripeGateway") PaymentGateway gateway) {
this.gateway = gateway;
}
}Qualifier customizado — cria uma anotação meta-anotada com @Qualifier para desacoplar o código do nome literal do bean:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Stripe {}
// No bean:
@Component
@Stripe
public class StripeGateway implements PaymentGateway { ... }
// No ponto de injeção:
@Service
public class CheckoutService {
public CheckoutService(@Stripe PaymentGateway gateway) { ... }
}Qualifiers customizados são preferíveis em código de produção porque uma renomeação de bean não quebra os pontos de injeção.
@Primary e @Profile como condicionais
@Primary elege um bean como candidato padrão quando nenhum @Qualifier está presente no ponto de injeção. É útil para cenários onde existe uma implementação “principal” e opções alternativas:
@Component
@Primary
public class StripeGateway implements PaymentGateway { ... }
@Component
public class PaypalGateway implements PaymentGateway { ... }
@Service
public class CheckoutService {
// Injeta StripeGateway sem necessidade de @Qualifier
public CheckoutService(PaymentGateway gateway) { ... }
}Dois
@Primarycausam o mesmoNoUniqueBeanDefinitionExceptionMarcar dois beans com
@Primarynão resolve a ambiguidade — ela apenas se desloca para outro nível. O contêiner ainda não sabe qual dos dois primários preferir.
@Profile é um mecanismo condicional: o bean só é registrado no contexto se o perfil indicado estiver ativo. Ele não resolve ambiguidade diretamente — ele evita que a ambiguidade surja ao controlar quais beans existem em cada ambiente:
@Configuration
public class DataSourceConfig {
@Bean
@Profile("development")
public DataSource h2DataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("production")
public DataSource postgresDataSource() {
// configura DataSource PostgreSQL
return ...;
}
}O perfil ativo é definido via spring.profiles.active (propriedade, variável de ambiente ou argumento de linha de comando). Expressões booleanas são suportadas: @Profile("production & us-east"), @Profile("!production").
Injeção de coleções (List<T>, Map<String,T>) e @Order
Quando o ponto de injeção declara uma coleção do tipo T, o Spring injeta todos os beans daquele tipo registrados no contexto:
@Service
public class NotificationService {
private final List<NotificationChannel> channels;
// Injeta todos os beans do tipo NotificationChannel
public NotificationService(List<NotificationChannel> channels) {
this.channels = channels;
}
}Para controlar a ordem dos elementos na lista, use @Order (ou implemente Ordered):
@Component
@Order(1)
public class EmailChannel implements NotificationChannel { ... }
@Component
@Order(2)
public class SmsChannel implements NotificationChannel { ... }Com Map<String, T>, o Spring usa o nome do bean como chave:
// Resulta em: {"emailChannel" -> EmailChannel, "smsChannel" -> SmsChannel}
private final Map<String, NotificationChannel> channelsByName;Esse padrão permite despachar para implementações pelo nome sem if/switch.
Na prática
Dois beans implementam PaymentGateway; o serviço usa @Primary como padrão e @Qualifier para o caso alternativo:
public interface PaymentGateway {
void charge(String customerId, long amountCents);
}
@Component
@Primary // padrão quando não há @Qualifier
public class StripeGateway implements PaymentGateway {
@Override
public void charge(String customerId, long amountCents) {
System.out.println("Stripe: charging " + customerId);
}
}
@Component("paypal") // nome explícito para @Qualifier
public class PaypalGateway implements PaymentGateway {
@Override
public void charge(String customerId, long amountCents) {
System.out.println("PayPal: charging " + customerId);
}
}
@Service
public class CheckoutService {
private final PaymentGateway defaultGateway; // injeta StripeGateway (@Primary)
private final PaymentGateway paypal; // injeta PaypalGateway (@Qualifier)
public CheckoutService(
PaymentGateway defaultGateway,
@Qualifier("paypal") PaymentGateway paypal) {
this.defaultGateway = defaultGateway;
this.paypal = paypal;
}
}Em testes, basta criar um bean @TestConfiguration com @Primary para sobrescrever o padrão sem alterar código de produção.
Armadilhas
1. Dois @Primary no mesmo tipo
O contêiner não elege um dos dois — ele lança NoUniqueBeanDefinitionException igualmente. Use @Primary em no máximo um bean por tipo.
// PROBLEMA: dois @Primary para o mesmo tipo
@Component @Primary
public class StripeGateway implements PaymentGateway { ... }
@Component @Primary // ← segundo @Primary para PaymentGateway
public class PaypalGateway implements PaymentGateway { ... }
// Resultado em startup:
// NoUniqueBeanDefinitionException: expected single matching bean but found 2Fix: remova @Primary de um dos beans; deixe apenas um candidato padrão.
2. @Qualifier com nome errado
O Spring lança NoSuchBeanDefinitionException em startup se o nome do qualifier não corresponder a nenhum bean registrado. Qualifiers customizados (anotações) evitam esse risco.
@Component("stripeGateway")
public class StripeGateway implements PaymentGateway { ... }
@Service
public class CheckoutService {
// PROBLEMA: nome do qualifier difere do nome do bean
public CheckoutService(@Qualifier("stripe") PaymentGateway gw) { ... }
// ^^^^^^ deveria ser "stripeGateway"
}
// Resultado: NoSuchBeanDefinitionException em startupFix: use o nome exato do bean ou, melhor, crie uma @Qualifier customizada para desacoplar do nome literal.
3. List<T> injeta todos os beans do tipo
Ao injetar uma coleção, o Spring coleta todos os candidatos compatíveis, ignorando @Qualifier no ponto de injeção. Se apenas um subconjunto for desejado, use qualifiers customizados nas implementações e no ponto de injeção.
// PROBLEMA: intenção é apenas canais de email, mas injeta todos
@Service
public class AlertService {
public AlertService(List<NotificationChannel> channels) {
// channels conterá EmailChannel, SmsChannel, PushChannel...
}
}Fix: crie uma @Qualifier customizada (ex. @EmailOnly) e anote tanto as implementações desejadas quanto o ponto de injeção.
4. @Profile sem perfil ativo levanta NoSuchBeanDefinitionException
Se todos os beans de um tipo estiverem sob @Profile e nenhum perfil correspondente estiver ativo, a injeção falha em startup.
@Bean @Profile("development")
public DataSource h2DataSource() { ... }
@Bean @Profile("production")
public DataSource postgresDataSource() { ... }
// Se nenhum perfil estiver ativo → NoSuchBeanDefinitionException:
// No qualifying bean of type 'DataSource'Fix: forneça um bean default (@Profile("default")) ou garanta que pelo menos um perfil esteja sempre ativo (ex.: via spring.profiles.default=development).
Em entrevista
Frase pronta (inglês)
- “When multiple beans of the same type exist in the context, Spring throws
NoUniqueBeanDefinitionException.@Qualifierresolves the ambiguity at the injection point by matching a bean name or a custom qualifier annotation.@Primarymarks the default candidate so you don’t need a qualifier everywhere.” - “
@Profileis a conditional mechanism — it controls whether a bean is registered at all based on the active environment profile, so it prevents ambiguity rather than resolving it.” - “Injecting
List<T>collects all beans of that type in one shot; combine it with@Orderto control the iteration order, and use aMap<String, T>when you need to dispatch by bean name at runtime.”
Vocabulário
| Termo PT | Termo EN |
|---|---|
| Ambiguidade de tipo | Type ambiguity |
| Qualificação de bean | Bean qualification |
| Candidato primário | Primary candidate |
| Perfil de ambiente | Environment profile |
| Ponto de injeção | Injection point |
| Qualifier customizado | Custom qualifier annotation |
| Injeção de coleção | Collection injection |
| Ativação de perfil | Profile activation |
Veja também
- Beans e estereótipos — como os beans são declarados e nomeados
- Configuração e profiles —
@Profileem detalhe,application.propertiespor ambiente - CDI — qualifiers, producers e eventos — qualifiers na spec Jakarta (para comparação, sem re-explicar aqui)
- Spring Core e Boot (MOC do galho)
- Trilha Java
- @Qualifier (Spring) — verbete no dicionário
- @Primary — verbete no dicionário