2025-11-05
La spécification CDI (Contexts and Dependency Injection), 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.
Principe Fondamental : Couplage faible avec un typage fort
Un exemple de code simple utilisant CDI : https://github.com/ebpro/sample-cdi
CDI résout des problèmes récurrents dans le développement d’applications Java :
Sans CDI
Avec CDI
@Inject@RequestScoped, @ApplicationScoped, etc.)@Qualifier) et alternatives (@Alternative)Avant CDI
Évolution CDI
@ObservesAsync)javax.* → jakarta.*Impact : CDI a standardisé l’injection de dépendances dans l’écosystème Jakarta EE, offrant une alternative officielle à Spring.
Un Bean CDI est un composant géré par le conteneur CDI qui respecte certaines règles et conventions.
Un bean CDI est défini par un contrat d’injection comprenant :
class PayPalService implements PaymentService@Inject PaymentService service;@PayPal, @CreditCard@Inject @PayPal PaymentService service;@ApplicationScoped, @RequestScoped@Named("paypal") → #{paypal} dans JSFCDI définit techniquement un bean comme :
✅ Bean valide si :
@Inject@Produces❌ Pas un bean si :
@Produces)@Produces)Bean CDI = Types + Qualifiers + Scope + Implémentation
[+ Nom EL] [+ Intercepteurs] [+ Alternative]
Les scopes définissent la durée de vie et la visibilité des beans CDI.
| 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 |
@Dependent@Dependent n’est jamais partagé, chaque injection crée une nouvelle instance// 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
}
// Bean lié à la session utilisateur
@SessionScoped
public class ShoppingCart implements Serializable {
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
}Note
Les beans @SessionScoped et @ConversationScoped peuvent être passivés (sérialisés sur disque) pour économiser la mémoire, puis activés (désérialisés) à la demande.
Prérequis : Le bean doit implémenter Serializable
L’injection de dépendances est le mécanisme central de CDI, permettant de fournir automatiquement les dépendances d’un bean.
1. Injection par Champ (Field)
2. Injection par Constructeur
3. Injection par Setter
L’annotation @Named permet de donner un nom à un bean pour le référencer dans Expression Language (EL), notamment dans les pages JSF.
Utilisation dans JSF :
Nom par défaut :
@Named sans valeur : nom = nom de la classe avec première lettre minuscule@Named sur ShoppingCartBean → nom = shoppingCartBeanCDI 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
Lorsqu’on injecte une dépendance avec @Inject, CDI recherche un bean correspondant au contrat d’injection :
Contrat d’injection = Type + Qualifiers
Le bean injecté doit : 1. ✅ Implémenter ou étendre le type spécifié (PaymentService) 2. ✅ Posséder tous les qualifiers spécifiés (@PayPal) 3. ✅ Avoir un scope approprié 4. ✅ Être activé (pas désactivé par @Alternative non activé)
Cas particulier : Si aucun qualifier n’est spécifié - CDI utilise le qualifier implicite @Default - @Inject private PaymentService service; ≡ @Inject @Default private PaymentService service;
Les initializer methods sont des méthodes annotées @Inject permettant d’initialiser un bean après sa création, avec injection de dépendances dans les paramètres.
@ApplicationScoped
public class ReportService {
private Logger logger;
private Database database;
// Méthode d'initialisation avec injection
@Inject
public void initialize(Logger logger, Database database) {
this.logger = logger;
this.database = database;
logger.info("ReportService initialized");
}
}Différence avec @PostConstruct :
@Inject sur méthode : Permet l’injection de paramètres@PostConstruct : Callback après injection complète, pas de paramètres injectablesOrdre d’exécution :
@Inject sur fields)@Inject sur méthodes)@PostConstructConstruisons progressivement une application de gestion de commandes.
public interface NotificationService {
void notify(String message);
}
@ApplicationScoped
public class EmailNotificationService implements NotificationService {
@Override
public void notify(String message) {
System.out.println("📧 Email : " + message);
}
}
@ApplicationScoped
public class OrderService {
private final Logger logger;
private final NotificationService notificationService;
@Inject
public OrderService(Logger logger, NotificationService notificationService) {
this.logger = logger;
this.notificationService = notificationService;
}
public void createOrder(String productId) {
logger.log("Commande créée pour : " + productId);
notificationService.notify("Votre commande est confirmée");
}
}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; // ❌ ERREUR : Ambiguïté!
}CDI lève une exception : AmbiguousResolutionException
PaymentServiceLes qualifiers permettent de distinguer différentes implémentations d’une même interface.
@PayPal
@ApplicationScoped
public class PayPalPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("💳 Paiement PayPal : " + amount);
}
}
@CreditCard
@ApplicationScoped
public class CreditCardPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("💳 Paiement Carte : " + amount);
}
}@ApplicationScoped
public class OrderService {
@Inject
@PayPal
private PaymentService paypalService;
@Inject
@CreditCard
private PaymentService creditCardService;
public void processPayment(String method, double amount) {
if ("paypal".equals(method)) {
paypalService.processPayment(amount);
} else {
creditCardService.processPayment(amount);
}
}
}Lorsqu’aucun qualifier n’est spécifié, CDI utilise le qualifier implicite @Default.
Règle importante :
@PayPal), il n’a pas @Default@Default, il ne doit avoir aucun qualifier expliciteLes qualifiers peuvent avoir des valeurs pour plus de flexibilité :
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD, PARAMETER})
public @interface PaymentMethod {
PaymentType value();
enum PaymentType {
CREDIT_CARD, PAYPAL, BANK_TRANSFER
}
}
@PaymentMethod(PaymentType.PAYPAL)
@ApplicationScoped
public class PayPalPaymentService implements PaymentService {
// ...
}
@ApplicationScoped
public class OrderService {
@Inject
@PaymentMethod(PaymentType.PAYPAL)
private PaymentService paypalService;
}Plusieurs qualifiers peuvent être combinés pour une injection encore plus précise :
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD})
public @interface Secure {}
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD})
public @interface FastProcessing {}
@PayPal
@Secure
@FastProcessing
@ApplicationScoped
public class SecureFastPayPalService implements PaymentService {
// ...
}
@ApplicationScoped
public class PremiumOrderService {
@Inject
@PayPal
@Secure
@FastProcessing
private PaymentService paymentService; // Injection précise!
}Les beans alternatifs permettent de remplacer une implémentation par une autre selon l’environnement (développement, test, production) sans modifier le code source.
// Bean par défaut (production)
@ApplicationScoped
public class ProductionPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
// Appel réel à l'API de paiement
realPaymentGateway.charge(amount);
}
}
// Bean alternatif (test/développement)
@Alternative
@ApplicationScoped
public class MockPaymentService implements PaymentService {
@Override
public void processPayment(double amount) {
System.out.println("🧪 [MOCK] Paiement simulé : " + amount);
}
}Pour activer un bean alternatif, déclarez-le dans beans.xml :
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
version="4.0" bean-discovery-mode="annotated">
<alternatives>
<class>com.example.MockPaymentService</class>
</alternatives>
</beans>Résultat : MockPaymentService remplace ProductionPaymentService partout
CDI 4.0+ permet d’utiliser @Priority pour activer les alternatives sans beans.xml :
Plus la priorité est élevée, plus l’alternative est prioritaire
Les producers permettent de créer des beans de manière programmatique, utile pour : - Objets non-CDI (bibliothèques tierces) - Logique de création complexe - Configuration dynamique
@ApplicationScoped
public class LoggerProducer {
@Produces
public Logger createLogger(InjectionPoint injectionPoint) {
// Création personnalisée basée sur le point d'injection
Class<?> targetClass = injectionPoint.getMember().getDeclaringClass();
return Logger.getLogger(targetClass.getName());
}
}
@ApplicationScoped
public class OrderService {
@Inject
private Logger logger; // Logger créé par producer avec le bon nom
public void createOrder() {
logger.info("Commande créée"); // [OrderService] Commande créée
}
}Pour les cas simples, un champ peut produire un bean :
@ApplicationScoped
public class ConfigurationProducer {
@Produces
private final String apiUrl = System.getProperty("api.url", "http://localhost:8080");
@Produces
@Named("appVersion")
private final String version = "1.0.0";
}
@ApplicationScoped
public class ApiClient {
@Inject
private String apiUrl; // Injecté depuis producer field
}Un disposer permet de nettoyer les ressources créées par un producer :
@ApplicationScoped
public class DatabaseProducer {
@Produces
@ApplicationScoped
public EntityManager createEntityManager() {
return Persistence.createEntityManagerFactory("myPU")
.createEntityManager();
}
public void closeEntityManager(@Disposes EntityManager em) {
if (em.isOpen()) {
em.close();
}
}
}Les intercepteurs permettent d’ajouter des comportements transversaux (cross-cutting concerns) à des méthodes sans modifier leur code source.
L’interception insère du code avant, après ou autour de l’exécution d’une méthode :
Client → Intercepteur → Méthode Cible → Intercepteur → Client
[Avant] [Exécution] [Après]
Cas d’usage typiques : - 📝 Journalisation (logging) - 🔒 Sécurité (vérification permissions) - 💾 Transactions - ⏱️ Mesure de performance - ✅ Validation
Étape 1 : Définir l’annotation d’interception
Étape 2 : Implémenter l’intercepteur
@Interceptor
@Logged
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor {
@Inject
private Logger logger;
@AroundInvoke
public Object logMethod(InvocationContext ctx) throws Exception {
String methodName = ctx.getMethod().getName();
logger.info("→ Entrée dans " + methodName);
long start = System.currentTimeMillis();
try {
Object result = ctx.proceed(); // Appel méthode cible
return result;
} finally {
long duration = System.currentTimeMillis() - start;
logger.info("← Sortie de " + methodName + " (" + duration + "ms)");
}
}
}Étape 3 : Appliquer l’intercepteur
Résultat :
→ Entrée dans createOrder
Création commande : ABC123
← Sortie de createOrder (15ms)
CDI offre plusieurs points d’interception :
| Annotation | Point d’interception | Usage |
|---|---|---|
@AroundConstruct |
Autour de la construction | Initialisation custom |
@PostConstruct |
Après construction | Setup initial |
@AroundInvoke |
Autour d’un appel de méthode | Logging, transactions |
@PreDestroy |
Avant destruction | Nettoyage |
@AroundTimeout |
Autour d’un timeout (EJB) | Gestion timeouts |
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Transactional {}
@Interceptor
@Transactional
@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200)
public class TransactionInterceptor {
@Inject
private EntityManager em;
@AroundInvoke
public Object manageTransaction(InvocationContext ctx) throws Exception {
EntityTransaction tx = em.getTransaction();
boolean newTx = !tx.isActive();
if (newTx) {
tx.begin();
}
try {
Object result = ctx.proceed();
if (newTx) {
tx.commit();
}
return result;
} catch (Exception e) {
if (newTx && tx.isActive()) {
tx.rollback();
}
throw e;
}
}
}Plusieurs intercepteurs peuvent être appliqués :
Ordre d’exécution déterminé par @Priority (plus petit = plus tôt)
Les décorateurs permettent d’étendre dynamiquement le comportement d’un bean en implémentant la même interface.
Le décorateur enveloppe l’objet original et peut modifier son comportement :
Client → Décorateur → Bean Original
[Logique +] [Logique métier]
Différence avec les intercepteurs :
| Aspect | Intercepteur | Décorateur |
|---|---|---|
| Implémente interface | ❌ Non | ✅ Oui |
| Peut modifier params/résultat | ⚠️ Limité | ✅ Oui |
| Application | Annotation sur méthode/classe | Délégation explicite |
| Use case | Aspects transversaux (logging, sécurité) | Extension comportement métier |
public interface PaymentService {
boolean processPayment(double amount);
}
@ApplicationScoped
public class BasicPaymentService implements PaymentService {
@Override
public boolean processPayment(double amount) {
System.out.println("💳 Paiement de " + amount + "€");
return true;
}
}
@Decorator
@Priority(Interceptor.Priority.APPLICATION)
public abstract class PaymentLoggingDecorator implements PaymentService {
@Inject
@Delegate
@Any
private PaymentService delegate; // Bean original
@Inject
private Logger logger;
@Override
public boolean processPayment(double amount) {
logger.info("🔔 Tentative de paiement : " + amount + "€");
boolean result = delegate.processPayment(amount); // Appel bean original
if (result) {
logger.info("✅ Paiement réussi");
} else {
logger.warning("❌ Paiement échoué");
}
return result;
}
}Avec beans.xml :
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
version="4.0" bean-discovery-mode="annotated">
<decorators>
<class>com.example.PaymentLoggingDecorator</class>
</decorators>
</beans>Ou avec @Priority (CDI 2.0+) :
@Decorator
@Priority(Interceptor.Priority.APPLICATION + 10)
public abstract class PaymentValidationDecorator implements PaymentService {
@Inject
@Delegate
@Any
private PaymentService delegate;
@Override
public boolean processPayment(double amount) {
// Validation avant délégation
if (amount <= 0) {
throw new IllegalArgumentException("Montant invalide : " + amount);
}
if (amount > 10000) {
throw new IllegalArgumentException("Montant trop élevé : " + amount);
}
return delegate.processPayment(amount);
}
}Plusieurs décorateurs peuvent être chaînés :
Client → ValidationDecorator → LoggingDecorator → BasicPaymentService
[Valide] [Log] [Traite]
Ordre déterminé par @Priority ou ordre dans beans.xml
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.
Émetteur → Event<T> → CDI Container → @Observes/@ObservesAsync → Observateurs
Avantages : - ✅ Couplage faible (émetteur ≠ observateurs) - ✅ Extensibilité (ajouter observateurs sans modifier émetteur) - ✅ Communication asynchrone possible
// 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
System.out.println("Création commande " + orderId);
// Émission événement
orderEvent.fire(new OrderCreatedEvent(orderId, amount));
}
}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());
}
}
@ApplicationScoped
public class InventoryService {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("📦 Mise à jour stock 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.
Les observateurs asynchrones sont invoqués dans un thread séparé :
Émission asynchrone :
@Inject
private Event<OrderCreatedEvent> orderEvent;
public void createOrder(String orderId, double amount) {
// Émission asynchrone
orderEvent.fireAsync(new OrderCreatedEvent(orderId, amount));
// Continue immédiatement (ne bloque pas)
System.out.println("Commande créée, traitement en cours en arrière-plan");
}Les événements peuvent être qualifiés pour filtrer les observateurs :
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, PARAMETER})
public @interface Important {}
// Émission qualifiée
@Inject
@Important
private Event<OrderCreatedEvent> importantOrderEvent;
public void createVIPOrder(String orderId, double amount) {
importantOrderEvent.fire(new OrderCreatedEvent(orderId, amount));
}
// Observation qualifiée
public void onImportantOrder(@Observes @Important OrderCreatedEvent event) {
System.out.println("🔔 Commande VIP importante!");
}Les observateurs peuvent filtrer les événements :
Dans un contexte transactionnel (EJB/JTA), les événements peuvent être observés à différentes phases :
Les stereotypes permettent de combiner plusieurs annotations CDI en une seule annotation réutilisable, simplifiant le code et standardisant les patterns.
Au lieu de :
On peut écrire :
Les stereotypes peuvent inclure interceptors :
CDI définit quelques stereotypes standards :
@Model = @Named + @RequestScoped (pour controllers MVC)Weld est l’implémentation de référence de la spécification CDI, développée par Red Hat.
Fonctionnalités : - ✅ Implémentation complète de CDI 4.0+ - ✅ Support Java SE et Jakarta EE - ✅ Utilisé par WildFly, Payara, GlassFish
Fichier beans.xml dans META-INF/ :
Point d’entrée :
public class Main {
public static void main(String[] args) {
// Démarrage conteneur Weld
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
// Récupération bean
OrderService orderService = container.select(OrderService.class).get();
orderService.createOrder("ORD-123", 99.99);
}
}
}Avec écouteur d’événement :
WEB-INF/ (applications web) ou META-INF/ (modules EJB)Bean Discovery Modes :
all : Tous les types sont des beans (même sans annotations)annotated : Seulement les classes avec annotations CDI (recommandé)none : CDI désactivéArC (Quarkus DI) est une implémentation CDI optimisée pour Quarkus, axée sur la performance et la compilation native.
Différences avec Weld :
| Aspect | Weld (Jakarta EE) | ArC (Quarkus) |
|---|---|---|
| Initialisation | Runtime (réflexion) | Build-time (code généré) |
| beans.xml | Requis (ou annotations) | ❌ Pas nécessaire |
| Performance | Standard | ⚡ Optimisée |
| Compilation native | ⚠️ Limitée | ✅ Complète (GraalVM) |
| Scope support | Tous les scopes | Subset (pas de @ConversationScoped) |
| Découverte beans | Runtime | Build-time |
Configuration Quarkus :
Exemple Quarkus :
@ApplicationScoped
public class GreetingService {
public String greet(String name) {
return "Hello, " + name;
}
}
@Path("/hello")
public class GreetingResource {
@Inject
GreetingService greetingService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return greetingService.greet("Quarkus");
}
}Dev Mode Quarkus :
Optimisations ArC : - 🚀 Build-time DI : Résolution des dépendances à la compilation - 🔥 Code generation : Pas de réflexion, code natif généré - 📦 Removal unused beans : Beans non utilisés supprimés automatiquement - ⚡ Fast startup : <0.1s avec GraalVM native
Limitations ArC : - ❌ Pas de @ConversationScoped (pas de notion de conversation en microservices) - ❌ Pas de Portable Extensions (remplacé par Build Compatible Extensions) - ⚠️ Decorators limités
Quand utiliser Quarkus CDI ? - ✅ Microservices cloud-native - ✅ Performance critique (startup, mémoire) - ✅ Compilation native GraalVM - ✅ Containers/Kubernetes
Quand utiliser Weld ? - ✅ Applications Jakarta EE complètes - ✅ Besoin de tous les scopes CDI - ✅ Portable Extensions - ✅ Compatibilité maximale
@Inject (field, constructor, setter)@ApplicationScoped, @RequestScoped, @SessionScoped, @Dependent@Qualifier)@Alternative)@Produces)@Disposes)@Interceptor, @AroundInvoke)@Decorator, @Delegate)@Observes, @ObservesAsync)@Stereotype)@Inject sur méthode)// ✅ BON
@ApplicationScoped
public class OrderService {
private final PaymentService paymentService;
@Inject
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// ❌ ÉVITER
@ApplicationScoped
public class OrderService {
@Inject
private PaymentService paymentService; // Pas final, difficile à tester
}// ✅ BON : Service partagé = @ApplicationScoped
@ApplicationScoped
public class ConfigurationService { }
// ✅ BON : Données requête = @RequestScoped
@RequestScoped
public class OrderFormBean { }
// ❌ ÉVITER : Service en @RequestScoped (recréé à chaque requête!)
@RequestScoped
public class EmailService { } // Devrait être @ApplicationScopedE. Bruno - Inversion de contrôle en Java