CDI : Inversion de contrôle et injection de dépendances

Java
I311
Lecture
IoC
CDI
Introduction conceptuelle à CDI : le pattern d’inversion de contrôle, les beans gérés, les scopes, l’injection de dépendances, les qualifiers et les événements.
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2026-11-07

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 :

  1. Un ensemble de types — les classes et interfaces implémentées/étendues (ex. class PayPalService implements PaymentService), qui permettent l’injection par type.
  2. Un ensemble de qualifiers — des annotations qui distinguent les implémentations (ex. @PayPal, @CreditCard), permettant une injection spécifique.
  3. Un scope (contexte) — définit le cycle de vie du bean (ex. @ApplicationScoped, @RequestScoped) et gère automatiquement sa création/destruction.
  4. Une implémentation — le code métier concret du bean.
  5. Optionnellement : un nom — pour la référence dans Expression Language (EL) (ex. @Named("paypal") → #{paypal} dans JSF).
  6. 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 @Dependent n’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 :

  1. ✅ Implémenter ou étendre le type spécifié
  2. ✅ Posséder tous les qualifiers spécifiés
  3. ✅ Avoir un scope approprié
  4. ✅ Être activé (pas désactivé par un @Alternative non 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 :

  1. Qualifiers : distinguer les implémentations (recommandé — voir ci-dessous).
  2. Alternatives : activer/désactiver des implémentations via configuration.
  3. 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).

Réutilisation