🎯 OBJECTIF
Comprendre comment :
@Primary et @Qualifier résolvent les ambiguïtés entre plusieurs beans du même typeList et Map permet des architectures extensibles sans modification🧠 MODÈLE MENTAL
Spring gère un registre de beans. Quand il doit injecter une dépendance, il cherche dans ce registre un bean du type attendu. Si un seul existe : injection directe. Si plusieurs existent : ambiguïté → NoUniqueBeanDefinitionException. Si aucun n'existe : NoSuchBeanDefinitionException.
Les annotations @Primary et @Qualifier sont deux façons de lever l'ambiguïté quand plusieurs beans du même type coexistent. La première dit "choisis-moi par défaut", la seconde dit "choisis le bean avec ce nom précis". L'injection par List<T> dit "donne-moi tous les beans de ce type" — pattern clé pour les architectures extensibles.
flowchart TD
A["Dépendance demandée\n(type T)"] --> B{Combien de beans\nde type T ?}
B -->|0| E["NoSuchBeanDefinitionException\nau démarrage"]
B -->|1| OK["Injection directe ✓"]
B -->|N| C{Un @Primary\nexiste ?}
C -->|Oui| OK
C -->|Non| D{@Qualifier\nprécisé ?}
D -->|Oui| OK
D -->|Non| F["NoUniqueBeanDefinitionException\nau démarrage"]mermaid🔑 Conclusion clé
La résolution échoue au démarrage — jamais en runtime. Spring ne découvre pas les ambiguïtés sous charge.
@Service
public class OrderService {
private final PaymentGateway gateway;
@Autowired
public OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}javaSpring cherche un bean de type PaymentGateway. Un seul existe → injection directe. Deux existent → NoUniqueBeanDefinitionException.
@Component
@Primary // ✓ choisi quand plusieurs beans du même type existent
public class StripeGateway implements PaymentGateway { ... }
@Component
public class PaypalGateway implements PaymentGateway { ... }javaSans @Qualifier, Spring choisit StripeGateway partout. PaypalGateway reste disponible mais n'est pas sélectionné automatiquement.
@Component("stripe")
public class StripeGateway implements PaymentGateway { ... }
@Component("paypal")
public class PaypalGateway implements PaymentGateway { ... }
@Service
public class OrderService {
@Autowired
@Qualifier("stripe") // ✓ sélection explicite par nom
private PaymentGateway gateway;
}java@Qualifier prend le dessus sur @Primary. Utiliser @Primary pour le cas d'usage dominant, @Qualifier pour les exceptions.
// Spring injecte TOUS les beans de type Validator
@Service
public class ValidationService {
private final List<Validator> validators;
@Autowired
public ValidationService(List<Validator> validators) {
this.validators = validators; // ✓ tous les Validator enregistrés
}
public void validate(Order order) {
validators.forEach(v -> v.validate(order));
}
}javaAjouter un nouveau Validator = créer un @Component qui implémente Validator. Aucune modification de ValidationService. C'est le Open/Closed Principle appliqué via Spring.
// Map<String, T> — accès par nom de bean
@Autowired
Map<String, PaymentGateway> gateways;
// gateways.get("stripe") → StripeGateway
// gateways.get("paypal") → PaypalGatewayjava// ❌ Injection par champ — difficile à tester, cache les dépendances
@Service
public class OrderService {
@Autowired
private PaymentGateway gateway; // champ privé, pas de constructeur
}
// ✓ Injection par constructeur — testable, dépendances visibles, final possible
@Service
@RequiredArgsConstructor // Lombok génère le constructeur
public class OrderService {
private final PaymentGateway gateway; // ✓ final = immuable
}javaL'injection par constructeur permet de créer le service en test sans Spring : new OrderService(mockGateway). L'injection par champ exige @InjectMocks ou une reflection.
🔑 Conclusion clé
En production : toujours injection par constructeur + final. @Autowired sur le constructeur est optionnel depuis Spring 4.3 si un seul constructeur existe.
⚡ TL;DR — chaque concept en une ligne
Résolution par type
✓ Spring cherche un bean du type exact attendu dans son registre — injection directe si un seul match.
⚠ Deux beans du même type sans @Primary ni @Qualifier → NoUniqueBeanDefinitionException au démarrage.
@Primary
✓ Marque un bean comme candidat par défaut quand plusieurs beans du même type existent.
⚠ N'empêche pas les ambiguïtés si plusieurs beans sont marqués @Primary — erreur au démarrage.
@Qualifier
✓ Sélectionne un bean spécifique par nom — prend le dessus sur @Primary.
⚠ Le nom dans @Qualifier doit correspondre exactement au nom du bean (défaut = nom de la classe en camelCase).
List injection
✓ Spring injecte tous les beans du type T — extensible sans modifier le consommateur.
⚠ L'ordre d'injection n'est pas garanti sans @Order ou Ordered sur les beans.
Injection par constructeur
✓ Dépendances visibles, testable sans Spring, permet final pour l'immutabilité.
⚠ L'injection par champ @Autowired cache les dépendances et rend les tests unitaires plus lourds.
🎓 À retenir
@Qualifier par nom de bean est fragile — renommer une classe change son nom de bean par défaut, casse silencieusement le @Qualifier. Préférer @Qualifier avec un nom explicite défini dans @Component("nom-stable").List<T> avec @Order pour l'ordre — sans annotation @Order ou implémentation de Ordered, l'ordre des beans dans une List<T> injectée est non déterministe. Si l'ordre des validators ou des intercepteurs compte, annoter chaque bean avec @Order(n).Optional<T> pour les beans conditionnels — si un bean peut ne pas exister (feature flag, profil), injecter Optional<T> plutôt que T directement. T absent = NoSuchBeanDefinitionException au démarrage. Optional<T> absent = Optional.empty() silencieux.