CDI : Inversion de contrôle et injection de dépendances
Objectifs
À la fin de cette leçon, vous serez capable de :
- Comprendre l’inversion de contrôle (IoC) et le rôle de CDI dans l’écosystème Jakarta EE.
- Identifier un bean géré par le conteneur CDI, ainsi que les règles qui le définissent.
- Choisir le scope approprié à un bean selon son cycle de vie.
- Injecter des dépendances entre beans, en privilégiant l’injection par constructeur.
- Créer et consommer des qualifiers pour distinguer des implémentations d’un même type.
- Émettre et observer des événements pour un découplage des composants.
Comment suivre cette leçon : ce document est le cœur conceptuel. Il s’accompagne d’un TP pratique (
02_java_CDI_P_cdi-injection.qmd) et d’une partie avancée optionnelle (02_java_CDI_L_advanced.qmd) pour les approfondissements.
Introduction à CDI
CDI (Contexts and Dependency Injection) repose sur le design pattern d’inversion de contrôle (IoC) : c’est le conteneur, et non le code applicatif, qui crée, assemble et gère le cycle de vie des objets.
La spécification CDI, au cœur de Jakarta EE, fournit un cadre puissant pour la gestion déclarative des dépendances et des cycles de vie des objets au sein des applications Java.
Objectifs de CDI
- Injection de Dépendances : éliminer le code de configuration explicite en injectant automatiquement les dépendances entre composants.
- Gestion des Contextes : contrôler finement le cycle de vie des objets selon leur portée (application, session, requête, etc.).
- Programmation Orientée Aspect : ajouter des fonctionnalités transversales (logging, sécurité, transactions) via intercepteurs et décorateurs.
- Communication Découplée : permettre aux composants de communiquer via des événements, sans couplage direct.
Principe Fondamental : couplage faible avec un typage fort.
Un exemple de code simple utilisant CDI : sample-cdi
Problèmes Résolus par CDI
CDI résout des problèmes récurrents dans le développement d’applications Java :
Sans CDI
- Accès aux objets : références manuelles, couplage fort entre composants
- Gestion du cycle de vie : création/destruction manuelle des objets
- Multi-threading : gestion complexe de la concurrence
- Mémoire : risque de fuites mémoire par gestion manuelle des références
- Implémentations alternatives : configuration manuelle et rigide au déploiement
- Partage d’objets : gestion manuelle des références partagées
Avec CDI
- Accès aux objets : injection automatique avec
@Inject - Gestion du cycle de vie : scopes automatiques (
@RequestScoped,@ApplicationScoped, etc.) - Multi-threading : gestion automatique par les contextes
- Mémoire : destruction automatique selon le scope
- Implémentations alternatives : qualifiers (
@Qualifier) et alternatives (@Alternative) - Partage d’objets : les scopes facilitent le partage sécurisé
Avantages Clés
- ✅ Faible couplage : composants indépendants et interchangeables
- ✅ Testabilité : injection de mocks/stubs simplifiée
- ✅ Extensibilité : ajout de fonctionnalités sans modifier le code source
- ✅ Maintenabilité : code plus clair et mieux organisé
Historique de CDI
CDI naît en 2009 avec la version 1.0 (Java EE 6, JSR 299), qui standardise l’injection de dépendances en s’inspirant de Spring. La version 2.0 (2017, Java EE 8) introduit les événements asynchrones (@ObservesAsync) et le support Java SE. La version 4.0 (2022, Jakarta EE 10) apporte CDI Lite, un sous-ensemble optimisé pour la compilation native, ainsi que des extensions compatibles au build.
Qu’est-ce qu’un Bean CDI ?
Un bean CDI est un composant géré par le conteneur CDI qui respecte certaines règles et conventions.
Vue Conceptuelle
Un bean CDI est défini par un contrat d’injection comprenant :
- Un ensemble de types — les classes et interfaces implémentées/étendues (ex.
class PayPalService implements PaymentService), qui permettent l’injection par type. - Un ensemble de qualifiers — des annotations qui distinguent les implémentations (ex.
@PayPal,@CreditCard), permettant une injection spécifique. - Un scope (contexte) — définit le cycle de vie du bean (ex.
@ApplicationScoped,@RequestScoped) et gère automatiquement sa création/destruction. - Une implémentation — le code métier concret du bean.
- Optionnellement : un nom — pour la référence dans Expression Language (EL) (ex.
@Named("paypal")→#{paypal}dans JSF). - Optionnellement : des intercepteurs — des comportements transversaux (logging, sécurité).
Vue Technique
CDI définit techniquement un bean comme :
✅ Bean valide si :
- Classe avec un constructeur sans paramètre :
public class MyService {
public MyService() {}
}- Ou classe avec un constructeur annoté
@Inject:
public class MyService {
@Inject
public MyService(Logger logger) {}
}❌ Pas un bean si :
- Classe abstraite (sans
@Produces) - Interface (sans
@Produces) - Classe sans constructeur approprié
- Inner class non statique
- Classe avec plusieurs constructeurs non annotés
Résumé : contrat d’un bean CDI
Bean CDI = Types + Qualifiers + Scope + Implémentation
[+ Nom EL] [+ Intercepteurs] [+ Alternative]
Scopes et Cycle de Vie
Les scopes définissent la durée de vie et la visibilité des beans CDI.
Scopes Principaux
| Scope | Durée de vie | Cas d’usage | Exemple |
|---|---|---|---|
@Dependent |
Même durée que le bean consommateur | Bean utilitaire, pas de partage | Calculateur, validateur |
@RequestScoped |
Une requête HTTP | Données formulaire, requête REST | Formulaire JSF, DTO REST |
@SessionScoped |
Session utilisateur | Données utilisateur persistantes | Panier e-commerce, préférences |
@ApplicationScoped |
Application entière | Configuration, services partagés | Cache, configuration globale |
@ConversationScoped |
Conversation utilisateur multi-requêtes | Processus métier multi-étapes | Tunnel de commande, wizard |
Scope par Défaut
- Si aucun scope n’est spécifié, le bean est
@Dependent. - Un bean
@Dependentn’est jamais partagé : chaque injection crée une nouvelle instance.
Exemple de Scopes
// Service partagé dans toute l'application
@ApplicationScoped
public class ConfigurationService {
private Properties config;
@PostConstruct
public void init() {
config = loadConfiguration();
}
}// Bean lié à une requête HTTP
@RequestScoped
public class OrderFormBean {
private String productId;
private int quantity;
// getters/setters
}Passivation et Activation
La passivation/activation (@PrePassivate/@PostActivate) des beans @SessionScoped/@ConversationScoped est abordée en partie avancée.
Injection de Dépendances
L’injection de dépendances est le mécanisme central de CDI : le conteneur fournit automatiquement les dépendances d’un bean.
Injection par Constructeur (recommandée)
@ApplicationScoped
public class OrderService {
private final PaymentService paymentService;
private final Logger logger;
@Inject
public OrderService(PaymentService paymentService, Logger logger) {
this.paymentService = paymentService;
this.logger = logger;
}
}- ✅ Dépendances finales (immutabilité)
- ✅ Facilite les tests (injection manuelle)
- ✅ Dépendances explicites
- ✅ Recommandée
Injection par Champ
@ApplicationScoped
public class OrderService {
@Inject
private PaymentService paymentService;
@Inject
private Logger logger;
}- ✅ Syntaxe concise
- ❌ Difficile à tester (pas d’accès direct)
- ❌ Dépendances non finales
L’injection par setter (@Inject sur un setter) permet des dépendances optionnelles, mais le bean peut se trouver dans un état incomplet ; elle est rarement utilisée.
Injection par Type
CDI injecte par défaut en se basant sur le type de l’attribut ou du paramètre.
public interface PaymentService {
void processPayment(double amount);
}
@ApplicationScoped
public class CreditCardPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("Paiement par carte : " + amount);
}
}
@ApplicationScoped
public class OrderService {
@Inject
private PaymentService paymentService; // Type = PaymentService
public void checkout(double amount) {
paymentService.processPayment(amount);
}
}Règle CDI : le conteneur cherche un bean dont le type correspond à PaymentService.
Annotation @Named
L’annotation @Named permet de donner un nom à un bean pour le référencer dans Expression Language (EL), notamment dans les pages JSF.
@Named("cart") // Accessible via #{cart} dans JSF
@SessionScoped
public class ShoppingCartBean implements Serializable {
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
public List<Item> getItems() {
return items;
}
}Utilisation dans JSF :
<h:dataTable value="#{cart.items}" var="item">
<h:column>#{item.name}</h:column>
</h:dataTable>Nom par défaut : si @Named est sans valeur, le nom est celui de la classe avec la première lettre minuscule (ex. ShoppingCartBean → shoppingCartBean).
Ce qui est Injecté : le contrat d’injection
Lorsqu’on injecte une dépendance avec @Inject, CDI recherche un bean correspondant au contrat d’injection : contrat = Type + Qualifiers + @Default. Le bean injecté doit :
- ✅ Implémenter ou étendre le type spécifié
- ✅ Posséder tous les qualifiers spécifiés
- ✅ Avoir un scope approprié
- ✅ Être activé (pas désactivé par un
@Alternativenon activé — voir la leçon avancée, beans alternatifs)
Cas particulier : si aucun qualifier n’est spécifié, toute injection utilise par défaut le qualifier implicite @Default (détaillé dans la section Qualifiers ci-dessous).
Initializer Methods
Les initializer methods sont des méthodes annotées @Inject : @Inject peut donc cibler non seulement les champs et le constructeur, mais aussi des méthodes. CDI établit un ordre d’initialisation fixé (constructeur, puis injection des champs et des méthodes, enfin @PostConstruct) ; l’ordre complet et ses détails sont traités en partie avancée.
TP : la mise en place de bout en bout d’une première application CDI (projet Quarkus, bean, injection par constructeur, scopes, qualifiers, événements) fait l’objet du TP dédié : « Premiers pas avec CDI » (
02_java_CDI_P_cdi-injection.qmd).
Conflits de Dépendances
Que se passe-t-il si plusieurs implémentations d’une même interface existent ?
public interface PaymentService {
void processPayment(double amount);
}
@ApplicationScoped
public class PayPalPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("💳 Paiement PayPal : " + amount);
}
}
@ApplicationScoped
public class CreditCardPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("💳 Paiement Carte : " + amount);
}
}
@ApplicationScoped
public class OrderService {
@Inject
private PaymentService paymentService; // ❌ Ambiguïté !
}CDI lève une exception : AmbiguousResolutionException — deux beans correspondent au type PaymentService, CDI ne sait pas lequel injecter.
Trois solutions :
- Qualifiers : distinguer les implémentations (recommandé — voir ci-dessous).
- Alternatives : activer/désactiver des implémentations via configuration.
- Producers : contrôler finement la création.
Qualifiers
Les qualifiers permettent de distinguer différentes implémentations d’une même interface.
Création d’un Qualifier
Un qualifier est une annotation custom, déclarée avec @Qualifier :
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD, PARAMETER})
public @interface PayPal {}
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD, PARAMETER})
public @interface CreditCard {}Application du Qualifier
@PayPal
@ApplicationScoped
public class PayPalPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("💳 Paiement PayPal : " + amount);
}
}Injection Qualifiée
@ApplicationScoped
public class OrderService {
@Inject
@PayPal
private PaymentService paypalService;
@Inject
@CreditCard
private PaymentService creditCardService;
}Qualifier @Default
Lorsqu’aucun qualifier n’est spécifié, CDI utilise le qualifier implicite @Default : @Inject private PaymentService service; est équivalent à @Inject @Default private PaymentService service;.
Règle importante : un bean portant un qualifier personnalisé (ex. @PayPal) n’a pas @Default ; pour qu’un bean ait @Default, il ne doit porter aucun qualifier explicite.
Les qualifiers avec valeurs (String/
value) et les combinaisons de qualifiers → partie avancée.
Événements
Les événements permettent une communication découplée entre composants : un émetteur envoie un événement, des observateurs réagissent, sans que l’émetteur connaisse les observateurs.
Principe
Émetteur → Event<T> → CDI Container → @Observes/@ObservesAsync → Observateurs
Avantages : ✅ couplage faible (émetteur ≠ observateurs) · ✅ extensibilité (ajouter des observateurs sans modifier l’émetteur) · ✅ communication asynchrone possible.
Émission d’Événements
// Classe d'événement (POJO)
public class OrderCreatedEvent {
private final String orderId;
private final double amount;
public OrderCreatedEvent(String orderId, double amount) {
this.orderId = orderId;
this.amount = amount;
}
// getters
}
@ApplicationScoped
public class OrderService {
@Inject
private Event<OrderCreatedEvent> orderEvent;
public void createOrder(String orderId, double amount) {
// Logique métier...
orderEvent.fire(new OrderCreatedEvent(orderId, amount));
}
}Observation Synchrone (@Observes)
Les observateurs synchrones sont invoqués dans le même thread que l’émetteur :
@ApplicationScoped
public class EmailNotificationService {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("📧 Envoi email pour commande " + event.getOrderId());
}
}Résultat :
Création commande ORD-123
📧 Envoi email pour commande ORD-123
📦 Mise à jour stock pour commande ORD-123
⚠️ Attention : si un observateur lève une exception, les suivants ne sont pas exécutés.
Observation Asynchrone (@ObservesAsync)
Les observateurs asynchrones sont invoqués dans un thread séparé (on peut aussi émettre de façon asynchrone avec fireAsync) :
@ApplicationScoped
public class AnalyticsService {
public void onOrderCreated(@ObservesAsync OrderCreatedEvent event) {
// Traitement long (thread séparé)
System.out.println("📊 Traitement analytics");
}
}Les événements qualifiés et l’observation conditionnelle → partie avancée. L’observation d’événements à des phases transactionnelles (
TransactionPhase) relève de la leçon 03 (Transactions) et de la partie avancée.
Résumé
Concepts Fondamentaux
- Bean CDI = Types + Qualifiers + Scope + Implémentation
- Injection =
@Inject(constructeur, field, setter) - Scopes =
@ApplicationScoped,@RequestScoped,@SessionScoped,@Dependent
Gestion des Dépendances
- Qualifiers : distinguer les implémentations (
@Qualifier) @Default: qualifier implicite si aucun qualifier personnalisé@Named: nommer un bean pour l’EL (JSF)
Événements
- Événements : communication découplée (
@Observes,@ObservesAsync)
Les concepts avancés (stereotypes, producers, intercepteurs, décorateurs) et les implémentations (Weld, Quarkus ArC) → partie avancée.
Bonnes Pratiques
1. Préférez l’Injection par Constructeur
Dépendances finales, explicites et faciles à tester — voir la section « Injection de Dépendances ».
2. Utilisez les Scopes Appropriés
// ✅ Service partagé = @ApplicationScoped
@ApplicationScoped
public class ConfigurationService { }
// ✅ Données de requête = @RequestScoped
@RequestScoped
public class OrderFormBean { }3. Nommez les Qualifiers de Manière Descriptive
// ✅ Descriptif
public @interface FastProcessing { }
// ❌ Peu clair
public @interface Q1 { }4. Évitez les Cycles de Dépendances
// ❌ ERREUR : Cycle A → B → A
@ApplicationScoped
public class ServiceA {
@Inject
private ServiceB serviceB; // B dépend de A !
}
// ✅ SOLUTION : refactorer le découplage (ou produire le bean ailleurs)Les bonnes pratiques avancées (
@Alternative, producers/@Disposes, documentation des stereotypes) → partie avancée.
Ressources
Documentation Officielle
Exemples
Tutoriels
Outils
- 🔧 CDI TCK — Test Compatibility Kit
- 🛠️ Arquillian — Testing CDI applications
Partie avancée : les approfondissements (qualifiers avec valeurs, beans alternatifs, producers, intercepteurs, décorateurs, événements avancés, stereotypes) sont traités dans la leçon optionnelle (
02_java_CDI_L_advanced.qmd).