CDI (@Inject) vs EJB (@EJB)

1. Introduction et Contextes d'Utilisation

Présentation de CDI (Contexts and Dependency Injection)

Contexts and Dependency Injection (CDI) est une spécification Java EE (JSR 346) qui révolutionne la gestion des dépendances et du cycle de vie des objets dans les applications d'entreprise. CDI fournit un modèle de programmation moderne basé sur l'injection typée et contextuelle, éliminant la nécessité de lookups JNDI et simplifiant drastiquement l'architecture des applications.

Les services fondamentaux de CDI incluent:

  • Contextes : Liaison du cycle de vie des composants à des contextes bien définis
  • Injection de dépendances : Injection typée et sécurisée des composants
  • Modèle d'événements : Communication découplée entre composants
  • Intercepteurs et décorateurs : Aspect-oriented programming intégré
  • Extensions portables : Mécanisme SPI pour intégration de frameworks tiers

Présentation des EJB (Enterprise Java Beans)

Enterprise Java Beans (EJB) constituent depuis Java EE 1.0 le modèle de composants serveur standard pour les applications d'entreprise Java. Les EJB 3.x modernes offrent un modèle déclaratif basé sur les annotations, éliminant la complexité des versions antérieures tout en conservant les services d'entreprise robustes.

Les EJB fournissent nativement:

  • Gestion transactionnelle : Support ACID complet avec JTA
  • Sécurité déclarative : Authentification et autorisation intégrées
  • Pooling et cycle de vie : Gestion optimisée des instances
  • Distribution : Interfaces remote et local
  • Concurrence : Gestion automatique des accès concurrents
  • Services système : Timers, asynchrone, messaging (MDB)

Positionnement dans l'Écosystème JBoss Wildfly

Dans JBoss Wildfly, CDI et EJB sont parfaitement intégrés et complémentaires. Wildfly utilise Weld comme implémentation de référence CDI et fournit un container EJB 3.2+ complet. Cette intégration permet:

  • Injection mixte : @Inject peut injecter des EJBs, @EJB reste spécifique aux EJBs
  • Services partagés : Les EJBs bénéficient des services CDI (événements, intercepteurs)
  • Découverte automatique : Activation CDI par défaut sans beans.xml en mode annotated
  • Modularité : Support des modules JBoss pour isolation et dépendances

2. Mécanismes d'Injection : Utilisation Pratique

Syntaxe et Annotations : @Inject vs @EJB

Injection CDI avec @Inject :

// Injection basique CDI
@RequestScoped
public class OrderController {
    @Inject
    private PaymentService paymentService;

    @Inject
    private ShippingService shippingService;

    // Injection avec qualifier
    @Inject @Premium
    private NotificationService notificationService;
}

Injection EJB avec @EJB :

// Injection EJB traditionnelle
@Stateless
public class OrderProcessorEJB {
    @EJB
    private PaymentEJB paymentEJB;

    @EJB(lookup="java:global/myapp/ShippingEJB")
    private ShippingEJB shippingEJB;

    // Injection par interface
    @EJB
    private NotificationService notificationService;
}

Déclaration des Beans et Scope de Vie

Déclaration de beans CDI :

// Bean CDI avec scope
@Named("userController")
@SessionScoped
public class UserController implements Serializable {
    private User currentUser;

    @PostConstruct
    public void init() {
        // Initialisation après injection
    }

    @PreDestroy
    public void cleanup() {
        // Nettoyage avant destruction
    }
}

// Bean CDI producteur
@ApplicationScoped
public class ServiceProducer {
    @Produces @Database
    public DataSource createDataSource() {
        // Configuration personnalisée
        return dataSourceFactory.create();
    }
}

Déclaration d'EJBs :

// EJB Stateless
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class UserServiceEJB implements UserService {
    @PersistenceContext
    private EntityManager em;

    @RolesAllowed({"admin", "manager"})
    public void createUser(User user) {
        em.persist(user);
    }
}

// EJB Singleton avec startup
@Singleton
@Startup
@DependsOn("ConfigurationService")
public class CacheManagerEJB {
    @Schedule(hour="*/6")
    public void refreshCache() {
        // Actualisation périodique
    }
}

Configuration Requise

Configuration CDI :

<!-- beans.xml optionnel en CDI 1.1+ -->
<?xml version="1.0" encoding="UTF-8"?>
<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">

    <!-- Intercepteurs globaux -->
    <interceptors>
        <class>com.example.LoggingInterceptor</class>
    </interceptors>

    <!-- Alternatives pour tests -->
    <alternatives>
        <class>com.example.MockPaymentService</class>
    </alternatives>
</beans>

Configuration EJB :

<!-- ejb-jar.xml pour configuration avancée -->
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="https://jakarta.ee/xml/ns/jakartaee"
         version="4.0">
    <enterprise-beans>
        <session>
            <ejb-name>PaymentEJB</ejb-name>
            <business-remote>com.example.PaymentService</business-remote>
            <transaction-type>Container</transaction-type>
        </session>
    </enterprise-beans>

    <assembly-descriptor>
        <method-permission>
            <role-name>payment-processor</role-name>
            <method>
                <ejb-name>PaymentEJB</ejb-name>
                <method-name>processPayment</method-name>
            </method>
        </method-permission>
    </assembly-descriptor>
</ejb-jar>

Gestion des Qualifiers et Ambiguïtés

Résolution d'ambiguïtés CDI :

// Définition de qualifiers
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PayPal {}

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface CreditCard {}

// Implémentations qualifiées
@PayPal
@ApplicationScoped
public class PayPalPaymentService implements PaymentService {
    // Implémentation PayPal
}

@CreditCard
@ApplicationScoped
public class CreditCardPaymentService implements PaymentService {
    // Implémentation carte de crédit
}

// Injection qualifiée
@RequestScoped
public class CheckoutController {
    @Inject @PayPal
    private PaymentService paypalService;

    @Inject @CreditCard
    private PaymentService creditCardService;

    // Instance dynamique pour choix runtime
    @Inject
    private Instance<PaymentService> paymentServices;

    public void processPayment(PaymentType type) {
        PaymentService service = paymentServices
            .select(getQualifier(type)).get();
        service.process();
    }
}

Résolution d'ambiguïtés EJB :

// Interface commune
@Local
public interface PaymentProcessor {
    void processPayment(Payment payment);
}

// Implémentations EJB
@Stateless
public class PayPalProcessorEJB implements PaymentProcessor {
    // Implémentation PayPal
}

@Stateless
public class CreditCardProcessorEJB implements PaymentProcessor {
    // Implémentation carte
}

// Injection spécifique par classe
@Stateless
public class OrderProcessorEJB {
    @EJB
    private PayPalProcessorEJB paypalProcessor;

    @EJB
    private CreditCardProcessorEJB creditCardProcessor;

    // Ou injection par lookup JNDI
    @EJB(lookup="java:global/myapp/PayPalProcessorEJB")
    private PaymentProcessor paymentProcessor;
}

3. Comparaison des Fonctionnalités et Comportements

Cycle de Vie des Beans

Scopes CDI :

Scope Annotation Durée de Vie Sérialisation
Application @ApplicationScoped Toute l'application Optionnelle
Session @SessionScoped Session HTTP Obligatoire
Request @RequestScoped Requête HTTP Non requise
Conversation @ConversationScoped Multi-requêtes contrôlées Obligatoire
Dependent @Dependent Liée au bean parent Selon parent

Cycle de vie EJB :

Type EJB Annotation Pool État Concurrence
Stateless @Stateless Pool partagé Sans état Thread-safe
Stateful @Stateful Instance dédiée Avec état Single-threaded
Singleton @Singleton Instance unique Partagé Configurable

Gestion des Transactions

Transactions CDI :

// CDI nécessite un gestionnaire de transactions externe
@RequestScoped
public class OrderService {
    @PersistenceContext
    private EntityManager em;

    @Transactional // JTA ou framework tiers requis
    public void createOrder(Order order) {
        em.persist(order);
        // Transaction gérée par intercepteur externe
    }
}

Transactions EJB :

// EJB fournit gestion transactionnelle native
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class OrderServiceEJB {
    @PersistenceContext
    private EntityManager em;

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void createOrder(Order order) {
        em.persist(order);
        // Transaction automatiquement gérée
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void logOrderEvent(OrderEvent event) {
        // Nouvelle transaction indépendante
        em.persist(event);
    }
}

Gestion de la Sécurité

Sécurité CDI :

// Sécurité via intercepteurs personnalisés
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface Secured {
    String[] roles() default {};
}

@Secured
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class SecurityInterceptor {
    @AroundInvoke
    public Object checkSecurity(InvocationContext ctx) throws Exception {
        // Vérification manuelle des rôles
        if (!hasRequiredRoles(ctx)) {
            throw new SecurityException("Access denied");
        }
        return ctx.proceed();
    }
}

@RequestScoped
public class UserController {
    @Secured(roles = {"admin", "manager"})
    public void deleteUser(Long userId) {
        // Action sécurisée
    }
}

Sécurité EJB :

// Sécurité déclarative intégrée
@Stateless
@DeclareRoles({"admin", "manager", "user"})
@RolesAllowed({"admin", "manager"})
public class UserManagementEJB {

    @RolesAllowed("admin")
    public void deleteUser(Long userId) {
        // Seuls les admins peuvent supprimer
    }

    @PermitAll
    public User findUser(Long userId) {
        // Accessible à tous les utilisateurs authentifiés
    }

    @DenyAll
    public void internalMethod() {
        // Méthode interne, accès interdit
    }

    @RunAs("system")
    public void systemOperation() {
        // Exécuté avec privilèges système
    }
}

Intercepteurs et Décorateurs

Intercepteurs CDI :

// Définition de l'interceptor binding
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface Monitored {}

// Implémentation de l'intercepteur
@Monitored
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class PerformanceInterceptor {
    @AroundInvoke
    public Object monitor(InvocationContext ctx) throws Exception {
        long start = System.currentTimeMillis();
        try {
            return ctx.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("Method {} took {}ms", 
                ctx.getMethod().getName(), duration);
        }
    }
}

// Décorateur CDI
@Decorator
@Priority(Interceptor.Priority.APPLICATION)
public abstract class CachingServiceDecorator implements UserService {
    @Inject @Delegate 
    private UserService delegate;

    @Override
    public User findUser(Long id) {
        User cached = cache.get(id);
        if (cached != null) return cached;

        User user = delegate.findUser(id);
        cache.put(id, user);
        return user;
    }
}

Intercepteurs EJB :

// Intercepteur EJB classique
@AroundInvoke
public Object auditMethod(InvocationContext ctx) throws Exception {
    String methodName = ctx.getMethod().getName();
    logger.info("Calling method: {}", methodName);

    try {
        Object result = ctx.proceed();
        logger.info("Method {} completed successfully", methodName);
        return result;
    } catch (Exception e) {
        logger.error("Method {} failed: {}", methodName, e.getMessage());
        throw e;
    }
}

// Application sur EJB
@Stateless
@Interceptors({AuditInterceptor.class, SecurityInterceptor.class})
public class BusinessServiceEJB {
    public void performBusinessOperation() {
        // Logique métier
    }
}

4. Analyse des Performances

Temps d'Instanciation et Overhead Mémoire

Performance CDI :

  • Instanciation : Overhead minimal, injection directe par proxy
  • Mémoire : Empreinte réduite, pas de pooling
  • Proxy : Proxies JDK ou CGLIB selon le contexte
  • Scope : Impact performance variable selon le scope choisi

Performance EJB :

  • Instanciation : Overhead du pooling et gestion lifecycle
  • Mémoire : Pool d'instances, cache de métadonnées
  • Proxy : Proxies EJB avec intercepteurs système
  • Pool : Coût initial élevé, bénéfice à long terme

Impact sur le Temps de Déploiement

D'après les retours terrain Wildfly, l'activation CDI par défaut peut impacter les temps de déploiement :

  • CDI : Scan des annotations, construction du graphe de dépendances
  • EJB : Analyse des métadonnées, configuration des pools
  • Optimisation : Mode annotated recommandé vs all pour CDI

Recommandations Performance

Choisir CDI quand :

  • Applications web avec besoins performance élevés
  • Composants légers sans services système
  • Prototypage rapide et développement agile

Choisir EJB quand :

  • Applications d'entreprise avec charge soutenue
  • Bénéfice du pooling et cache container
  • Services critiques nécessitant robustesse

5. Compatibilité et Interopérabilité

Utilisation Conjointe dans une Même Application

Injection mixte :

// Bean CDI injectant des EJBs
@RequestScoped
public class OrderController {
    @Inject  // CDI peut injecter des EJBs
    private OrderServiceEJB orderService;

    @Inject  // Injection CDI classique
    private ValidationService validationService;

    @EJB     // Injection EJB traditionnelle  
    private PaymentServiceEJB paymentService;
}

// EJB utilisant des services CDI
@Stateless
public class OrderServiceEJB {
    @Inject  // EJB peut utiliser CDI
    private NotificationService notificationService;

    @Inject @Audit
    private Event<OrderEvent> orderEvent;

    public void processOrder(Order order) {
        // Logique transactionnelle EJB
        processPayment(order);

        // Notification via CDI
        notificationService.sendConfirmation(order);

        // Événement CDI
        orderEvent.fire(new OrderEvent(order.getId()));
    }
}

Limitations et Incompatibilités dans Wildfly

Problèmes de portée de module :

// Configuration jboss-deployment-structure.xml
<jboss-deployment-structure>
    <deployment>
        <dependencies>
            <!-- Dépendance inter-EAR pour CDI -->
            <module name="deployment.ear-a.ear" meta-inf="import"/>
        </dependencies>
    </deployment>
</jboss-deployment-structure>

Limitations identifiées :

  • Cross-EAR injection : CDI complexe entre EARs différents
  • Classloader isolation : Impact sur découverte des beans
  • JTA coordination : Attention aux transactions distribuées
  • Serialization : Problèmes avec beans CDI sérialisés entre modules

Bonnes Pratiques d'Intégration

Architecture recommandée :

// Couche de services EJB pour logique métier
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class CoreBusinessServiceEJB {
    @PersistenceContext
    private EntityManager em;

    public Order processOrder(Order order) {
        // Logique transactionnelle critique
        return em.merge(order);
    }
}

// Couche CDI pour présentation et coordination
@RequestScoped
public class OrderWorkflowController {
    @Inject  // Injection du service EJB
    private CoreBusinessServiceEJB businessService;

    @Inject  // Services CDI pour UI
    private MessageService messageService;

    @Inject
    private Event<WorkflowEvent> workflowEvent;

    public String submitOrder() {
        try {
            Order order = businessService.processOrder(currentOrder);
            workflowEvent.fire(new OrderSubmitted(order.getId()));
            messageService.addSuccess("Order processed successfully");
            return "confirmation";
        } catch (Exception e) {
            messageService.addError("Order processing failed");
            return "error";
        }
    }
}

6. Guide de Décision : Quand Utiliser Quoi ?

Tableau Comparatif Synthétique

Scénarios d'Utilisation Recommandés

Utiliser CDI (@Inject) pour :

  • Applications web modernes : Controllers JSF/JAX-RS, backing beans
  • Injection de composants légers : Validators, converters, utilities
  • Architecture événementielle : Communication découplée via events
  • Tests unitaires : Facilité de mock et injection d'alternatives
  • Intégration frameworks : Spring, Hibernate, bibliothèques tierces
  • Développement rapide : Prototypage, applications non-critiques

Utiliser EJB (@EJB) pour :

  • Services métier transactionnels : Gestion ACID complexe
  • Applications distribuées : Interfaces remote, clustering
  • Sécurité d'entreprise : Authentification/autorisation robuste
  • Services système : Scheduling, messaging, services asynchrones
  • Applications legacy : Migration progressive depuis EJB 2.x
  • Haute disponibilité : Clustering, failover, persistence automatique

Architecture hybride recommandée :

  • Couche EJB : Services métier, accès données, transactions
  • Couche CDI : Présentation, coordination, gestion d'état UI
  • Intégration : @Inject pour injecter EJBs dans beans CDI

7. Diagrammes PlantUML Illustratifs

Diagramme de Séquence : Injection CDI

Séquence d'Injection et Initialisation CDI (@Inject)Séquence d'Injection et Initialisation CDI (@Inject)CDI ContainerOrderControllerPaymentServicePaymentServiceValidationServiceValidationServiceClientCDI ContainerOrderControllerPaymentServiceValidationServiceClientClientCDI ContainerCDI ContainerOrderController@RequestScopedOrderController@RequestScopedPaymentService@ApplicationScopedPaymentService@ApplicationScopedValidationService@DependentValidationService@DependentCDI ContainerOrderControllerPaymentServicePaymentServiceValidationServiceValidationServiceHTTP RequestAnalyze injection pointsResolve dependenciesalt[First request for PaymentService]Create instance@PostConstruct init()Create new instanceCreate instanceInject PaymentService proxyInject ValidationService instance@PostConstruct init()Return controller proxyCall business methodCall payment methodExecute business logicReturn resultValidate resultPerform validationReturn validation resultReturn response@RequestScoped: Instance destroyedafter request completion@Dependent: Instance destroyedwith parent controller@ApplicationScoped: Instance persistsfor application lifetime
Séquence d'Injection et Initialisation CDI (@Inject)Séquence d'Injection et Initialisation CDI (@Inject)CDI ContainerOrderControllerPaymentServicePaymentServiceValidationServiceValidationServiceClientCDI ContainerOrderControllerPaymentServiceValidationServiceClientClientCDI ContainerCDI ContainerOrderController@RequestScopedOrderController@RequestScopedPaymentService@ApplicationScopedPaymentService@ApplicationScopedValidationService@DependentValidationService@DependentCDI ContainerOrderControllerPaymentServicePaymentServiceValidationServiceValidationServiceHTTP RequestAnalyze injection pointsResolve dependenciesalt[First request for PaymentService]Create instance@PostConstruct init()Create new instanceCreate instanceInject PaymentService proxyInject ValidationService instance@PostConstruct init()Return controller proxyCall business methodCall payment methodExecute business logicReturn resultValidate resultPerform validationReturn validation resultReturn response@RequestScoped: Instance destroyedafter request completion@Dependent: Instance destroyedwith parent controller@ApplicationScoped: Instance persistsfor application lifetime

Diagramme de Séquence : Injection EJB

Séquence d'Injection et Initialisation EJB (@EJB)Séquence d'Injection et Initialisation EJB (@EJB)EJB ContainerOrderControllerEJBPaymentServiceEJBAuditServiceEJBTransaction ManagerClientEJB ContainerOrderControllerEJBPaymentServiceEJBAuditServiceEJBTransaction ManagerClientClientEJB ContainerEJB ContainerOrderControllerEJB@StatelessOrderControllerEJB@StatelessPaymentServiceEJB@StatelessPaymentServiceEJB@StatelessAuditServiceEJB@SingletonAuditServiceEJB@SingletonTransaction ManagerTransaction ManagerEJB ContainerOrderControllerEJBPaymentServiceEJBAuditServiceEJBTransaction ManagerRemote/Local callSecurity checkGet instance from poolalt[Pool empty]Create new instanceCreate/Get pooled instanceInject EJB proxiesBegin transactionInvoke business methodCall payment processingExecute business logicLog payment eventStore audit recordConfirm loggingReturn payment resultMethod completionCommit transactionTransaction committedReturn instance to poolReturn result@Stateless: Instance returnedto pool for reuse@Stateless: Pooled instanceavailable for next call@Singleton: Single instanceshared across all calls
Séquence d'Injection et Initialisation EJB (@EJB)Séquence d'Injection et Initialisation EJB (@EJB)EJB ContainerOrderControllerEJBPaymentServiceEJBAuditServiceEJBTransaction ManagerClientEJB ContainerOrderControllerEJBPaymentServiceEJBAuditServiceEJBTransaction ManagerClientClientEJB ContainerEJB ContainerOrderControllerEJB@StatelessOrderControllerEJB@StatelessPaymentServiceEJB@StatelessPaymentServiceEJB@StatelessAuditServiceEJB@SingletonAuditServiceEJB@SingletonTransaction ManagerTransaction ManagerEJB ContainerOrderControllerEJBPaymentServiceEJBAuditServiceEJBTransaction ManagerRemote/Local callSecurity checkGet instance from poolalt[Pool empty]Create new instanceCreate/Get pooled instanceInject EJB proxiesBegin transactionInvoke business methodCall payment processingExecute business logicLog payment eventStore audit recordConfirm loggingReturn payment resultMethod completionCommit transactionTransaction committedReturn instance to poolReturn result@Stateless: Instance returnedto pool for reuse@Stateless: Pooled instanceavailable for next call@Singleton: Single instanceshared across all calls

Diagramme de Classes : Architecture CDI

Architecture Typique avec CDIArchitecture Typique avec CDIPresentation LayerBusiness LayerData LayerInfrastructureOrderControllerOrder currentOrderList<OrderItem> items@RequestScopedString submitOrder()void addItem()void removeItem()@Named("orderController")ValidationController@ApplicationScopedboolean validateOrder(Order)List<String> getValidationErrors()PaymentServicePaymentResult processPayment(Payment)CreditCardPaymentService@CreditCard@ApplicationScopedPaymentResult processPayment(Payment)PayPalPaymentService@PayPal@ApplicationScopedPaymentResult processPayment(Payment)NotificationService@ApplicationScopedvoid sendConfirmation(Order)void sendError(String message)OrderRepository@ApplicationScopedOrder save(Order)Order findById(Long)List<Order> findByCustomer(Customer)ProductCatalogService@ApplicationScopedProduct findProduct(String sku)List<Product> searchProducts(String)EventManager@ApplicationScopedvoid fireOrderEvent(OrderEvent)ConfigurationService@ApplicationScopedString getProperty(String key)int getIntProperty(String key)@Qualifier @CreditCard@Qualifier @PayPal@Inject @CreditCard@Inject@Inject@Inject@Inject@Inject@Inject@Inject@Inject Event<OrderEvent>
Architecture Typique avec CDIArchitecture Typique avec CDIPresentation LayerBusiness LayerData LayerInfrastructureOrderControllerOrder currentOrderList<OrderItem> items@RequestScopedString submitOrder()void addItem()void removeItem()@Named("orderController")ValidationController@ApplicationScopedboolean validateOrder(Order)List<String> getValidationErrors()PaymentServicePaymentResult processPayment(Payment)CreditCardPaymentService@CreditCard@ApplicationScopedPaymentResult processPayment(Payment)PayPalPaymentService@PayPal@ApplicationScopedPaymentResult processPayment(Payment)NotificationService@ApplicationScopedvoid sendConfirmation(Order)void sendError(String message)OrderRepository@ApplicationScopedOrder save(Order)Order findById(Long)List<Order> findByCustomer(Customer)ProductCatalogService@ApplicationScopedProduct findProduct(String sku)List<Product> searchProducts(String)EventManager@ApplicationScopedvoid fireOrderEvent(OrderEvent)ConfigurationService@ApplicationScopedString getProperty(String key)int getIntProperty(String key)@Qualifier @CreditCard@Qualifier @PayPal@Inject @CreditCard@Inject@Inject@Inject@Inject@Inject@Inject@Inject@Inject Event<OrderEvent>

Diagramme de Classes : Architecture EJB

Architecture Typique avec EJBArchitecture Typique avec EJBSession FacadeBusiness LogicData AccessSystem ServicesMessage DrivenOrderManagementFacadeOrderResult processOrder(OrderRequest)List<Order> getOrderHistory(Customer)OrderManagementFacadeEJB@Stateless@RemoteOrderResult processOrder(OrderRequest)List<Order> getOrderHistory(Customer)@TransactionAttribute(REQUIRED)PaymentProcessorPaymentResult processPayment(Payment)PaymentProcessorEJB@Stateless@LocalPaymentResult processPayment(Payment)void refundPayment(String transactionId)@TransactionAttribute(REQUIRED)InventoryManagerboolean reserveItems(List<OrderItem>)void releaseReservation(String reservationId)InventoryManagerEJB@Stateless@Localboolean reserveItems(List<OrderItem>)void releaseReservation(String reservationId)void updateStock(String sku, int quantity)OrderDAOEJB@Stateless@LocalOrder save(Order)Order findById(Long id)List<Order> findByCustomer(Long customerId)@TransactionAttribute(MANDATORY)ProductDAOEJB@Stateless@LocalProduct findBySku(String sku)void updateStock(String sku, int quantity)AuditServiceEJB@Singleton@Startupvoid logBusinessEvent(AuditEvent)List<AuditEvent> getAuditTrail(String entityId)NotificationServiceEJB@Statelessvoid sendOrderConfirmation(Order)void sendPaymentNotification(Payment)SchedulerServiceEJB@Singletonvoid scheduleOrderProcessing()void processScheduledOrders()@Schedule(hour="*/1")OrderEventMDB@MessageDrivenvoid onMessage(Message message)PaymentEventMDB@MessageDrivenvoid onMessage(Message message)@RolesAllowed({"customer", "admin"})@RolesAllowed({"admin", "auditor"})@EJB@EJB@EJB@EJB@EJB@EJB@EJB@EJB@EJB@EJB
Architecture Typique avec EJBArchitecture Typique avec EJBSession FacadeBusiness LogicData AccessSystem ServicesMessage DrivenOrderManagementFacadeOrderResult processOrder(OrderRequest)List<Order> getOrderHistory(Customer)OrderManagementFacadeEJB@Stateless@RemoteOrderResult processOrder(OrderRequest)List<Order> getOrderHistory(Customer)@TransactionAttribute(REQUIRED)PaymentProcessorPaymentResult processPayment(Payment)PaymentProcessorEJB@Stateless@LocalPaymentResult processPayment(Payment)void refundPayment(String transactionId)@TransactionAttribute(REQUIRED)InventoryManagerboolean reserveItems(List<OrderItem>)void releaseReservation(String reservationId)InventoryManagerEJB@Stateless@Localboolean reserveItems(List<OrderItem>)void releaseReservation(String reservationId)void updateStock(String sku, int quantity)OrderDAOEJB@Stateless@LocalOrder save(Order)Order findById(Long id)List<Order> findByCustomer(Long customerId)@TransactionAttribute(MANDATORY)ProductDAOEJB@Stateless@LocalProduct findBySku(String sku)void updateStock(String sku, int quantity)AuditServiceEJB@Singleton@Startupvoid logBusinessEvent(AuditEvent)List<AuditEvent> getAuditTrail(String entityId)NotificationServiceEJB@Statelessvoid sendOrderConfirmation(Order)void sendPaymentNotification(Payment)SchedulerServiceEJB@Singletonvoid scheduleOrderProcessing()void processScheduledOrders()@Schedule(hour="*/1")OrderEventMDB@MessageDrivenvoid onMessage(Message message)PaymentEventMDB@MessageDrivenvoid onMessage(Message message)@RolesAllowed({"customer", "admin"})@RolesAllowed({"admin", "auditor"})@EJB@EJB@EJB@EJB@EJB@EJB@EJB@EJB@EJB@EJB

Diagramme de Composants : Comparaison CDI vs EJB

Comparaison d'Architecture CDI vs EJB dans WildflyComparaison d'Architecture CDI vs EJB dans WildflyWildfly Application ServerCDI Container (Weld)EJB ContainerJPA Provider (Hibernate)Application ComponentsCDI BeansEJB ComponentsBean ManagerContext ManagerEvent BusInterceptor ChainPool ManagerTransaction ManagerSecurity ManagerTimer ServiceEntity ManagerConnection Pool@RequestScopedController@ApplicationScopedService@DependentHelper@StatelessSessionBean@SingletonAppService@MessageDrivenMDBHTTP/RESTJMS QueueTimer Events"Lightweight DIType-safe injectionEvent-driven""Enterprise servicesTransaction managementDistribution support""UI ControllersRequest handlingUser interaction""Business logicTransactional operationsData processing"Manages contextsHandles eventsApplies interceptorsCoordinates transactionsEnforces securityManages timersManages lifecycleManages lifecycleManages lifecycleRequest contextApplication contextEvent notificationsMethod interceptionPool managementSingleton lifecycleMessage handlingTransaction controlTransaction controlSecurity enforcementTimer execution@Inject EJB@Inject EJB@Inject CDI bean@PersistenceContext@PersistenceContext (via producer)Database accessWeb requestsMessage deliveryScheduled execution
Comparaison d'Architecture CDI vs EJB dans WildflyComparaison d'Architecture CDI vs EJB dans WildflyWildfly Application ServerCDI Container (Weld)EJB ContainerJPA Provider (Hibernate)Application ComponentsCDI BeansEJB ComponentsBean ManagerContext ManagerEvent BusInterceptor ChainPool ManagerTransaction ManagerSecurity ManagerTimer ServiceEntity ManagerConnection Pool@RequestScopedController@ApplicationScopedService@DependentHelper@StatelessSessionBean@SingletonAppService@MessageDrivenMDBHTTP/RESTJMS QueueTimer Events"Lightweight DIType-safe injectionEvent-driven""Enterprise servicesTransaction managementDistribution support""UI ControllersRequest handlingUser interaction""Business logicTransactional operationsData processing"Manages contextsHandles eventsApplies interceptorsCoordinates transactionsEnforces securityManages timersManages lifecycleManages lifecycleManages lifecycleRequest contextApplication contextEvent notificationsMethod interceptionPool managementSingleton lifecycleMessage handlingTransaction controlTransaction controlSecurity enforcementTimer execution@Inject EJB@Inject EJB@Inject CDI bean@PersistenceContext@PersistenceContext (via producer)Database accessWeb requestsMessage deliveryScheduled execution

Logigramme de Décision

Logigramme de Décision : CDI (@Inject) vs EJB (@EJB)Logigramme de Décision : CDI (@Inject) vs EJB (@EJB)Analyser les besoinsde l'applicationBesoin de servicestransactionnels ACID ?OuiNonTransactions distribuéesou complexes ?OuiNonUtiliser EJB@TransactionAttributeFramework de transactiondisponible ?OuiNonCDI + @Transactionalou framework externeUtiliser EJB poursimplicité transactionnelleContinuer l'analyseApplication distribuéeou interfaces remote ?OuiNonUtiliser EJB@Remote obligatoireContinuer l'analyseSécurité d'entreprisecomplexe requise ?OuiNonAuthentification/autorisationdéclarative suffisante ?OuiNonUtiliser EJB@RolesAllowed, @RunAsCDI + frameworksécurité personnaliséContinuer l'analyseBesoin de servicessystème spécialisés ?OuiNonTimer, MDB,Asynchrone ?OuiNonUtiliser EJBservices natifsContinuer l'analyseContinuer l'analysePerformance critiqueet haute charge ?OuiNonBénéfice du poolinget cache container ?OuiNonUtiliser EJB@Stateless avec poolCDI pour overheadminimalContinuer l'analyseApplication webmoderne/agile ?OuiNonUtiliser CDI@RequestScoped, @NamedComposants légerset utilitaires ?OuiNonUtiliser CDI@Dependent, @ApplicationScopedAnalyser architecturehybrideArchitecture hybriderecommandée ?OuiNonServices métier → EJBPrésentation → CDIIntégration → @Inject EJBSolution pure CDIou pure EJBPoints de décision basés sur :- Besoins fonctionnels- Contraintes techniques- Expérience de l'équipe- Architecture existante
Logigramme de Décision : CDI (@Inject) vs EJB (@EJB)Logigramme de Décision : CDI (@Inject) vs EJB (@EJB)Analyser les besoinsde l'applicationBesoin de servicestransactionnels ACID ?OuiNonTransactions distribuéesou complexes ?OuiNonUtiliser EJB@TransactionAttributeFramework de transactiondisponible ?OuiNonCDI + @Transactionalou framework externeUtiliser EJB poursimplicité transactionnelleContinuer l'analyseApplication distribuéeou interfaces remote ?OuiNonUtiliser EJB@Remote obligatoireContinuer l'analyseSécurité d'entreprisecomplexe requise ?OuiNonAuthentification/autorisationdéclarative suffisante ?OuiNonUtiliser EJB@RolesAllowed, @RunAsCDI + frameworksécurité personnaliséContinuer l'analyseBesoin de servicessystème spécialisés ?OuiNonTimer, MDB,Asynchrone ?OuiNonUtiliser EJBservices natifsContinuer l'analyseContinuer l'analysePerformance critiqueet haute charge ?OuiNonBénéfice du poolinget cache container ?OuiNonUtiliser EJB@Stateless avec poolCDI pour overheadminimalContinuer l'analyseApplication webmoderne/agile ?OuiNonUtiliser CDI@RequestScoped, @NamedComposants légerset utilitaires ?OuiNonUtiliser CDI@Dependent, @ApplicationScopedAnalyser architecturehybrideArchitecture hybriderecommandée ?OuiNonServices métier → EJBPrésentation → CDIIntégration → @Inject EJBSolution pure CDIou pure EJBPoints de décision basés sur :- Besoins fonctionnels- Contraintes techniques- Expérience de l'équipe- Architecture existante

8. Bonnes Pratiques et Recommandations

Patterns d'Utilisation Recommandés dans Wildfly

Pattern Service Facade avec CDI :

// Facade CDI coordonnant des EJBs
@RequestScoped
public class OrderWorkflowFacade {
    @Inject
    private OrderValidationService validationService;

    @Inject  // Injection d'EJB via CDI
    private PaymentProcessorEJB paymentProcessor;

    @Inject
    private InventoryManagerEJB inventoryManager;

    @Inject
    private Event<OrderEvent> orderEvents;

    public OrderResult processOrder(OrderRequest request) {
        // Validation via CDI
        ValidationResult validation = validationService.validate(request);
        if (!validation.isValid()) {
            return OrderResult.invalid(validation.getErrors());
        }

        try {
            // Réservation via EJB transactionnel
            String reservationId = inventoryManager.reserveItems(request.getItems());

            // Paiement via EJB transactionnel
            PaymentResult payment = paymentProcessor.processPayment(request.getPayment());

            if (payment.isSuccessful()) {
                // Événement CDI pour découplage
                orderEvents.fire(new OrderProcessedEvent(request.getOrderId()));
                return OrderResult.success(payment.getTransactionId());
            } else {
                // Libération de réservation en cas d'échec
                inventoryManager.releaseReservation(reservationId);
                return OrderResult.failed(payment.getErrorMessage());
            }

        } catch (Exception e) {
            logger.error("Order processing failed", e);
            return OrderResult.error("Internal processing error");
        }
    }
}

Pattern Producer pour Configuration :

@ApplicationScoped
public class ResourceProducer {

    @Produces @Database
    public DataSource createDataSource() {
        // Configuration personnalisée DataSource
        return DataSourceFactory.create("jdbc:postgresql://db:5432/app");
    }

    @Produces @Cacheable
    public Cache<String, Object> createCache() {
        return CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .build();
    }

    @Produces @ConfigProperty("app.max.connections")
    public int maxConnections(InjectionPoint ip) {
        String property = ip.getAnnotated().getAnnotation(ConfigProperty.class).value();
        return Integer.parseInt(System.getProperty(property, "10"));
    }
}

Erreurs Courantes à Éviter

1. Mélange inapproprié des annotations :

// ❌ ERREUR : Mélange d'annotations incompatibles
@Stateless  // EJB
@RequestScoped  // CDI - Conflit !
public class BadServiceEJB {
    // Les EJBs ont leur propre gestion de lifecycle
}

// ✅ CORRECT : Choix cohérent
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class GoodServiceEJB {
    // EJB pur avec services natifs
}

// ✅ CORRECT : CDI pur
@RequestScoped
public class GoodServiceCDI {
    // CDI pur avec injection flexible
}

2. Injection circulaire non gérée :

// ❌ ERREUR : Dépendance circulaire
@ApplicationScoped
public class ServiceA {
    @Inject ServiceB serviceB;  // ServiceB injecte ServiceA
}

// ✅ CORRECT : Utilisation d'Instance pour injection paresseuse
@ApplicationScoped
public class ServiceA {
    @Inject Instance<ServiceB> serviceBInstance;

    public void someMethod() {
        ServiceB serviceB = serviceBInstance.get();
        // Utilisation paresseuse
    }
}

3. Mauvaise gestion des scopes sérialisables :

// ❌ ERREUR : Bean non-sérialisable avec scope sérialisable
@SessionScoped
public class UserSession {  // Doit implémenter Serializable !
    @Inject
    private NonSerializableService service;  // Problème !
}

// ✅ CORRECT : Bean sérialisable avec injection appropriée
@SessionScoped
public class UserSession implements Serializable {
    @Inject
    private Instance<NonSerializableService> serviceInstance;

    // Méthode helper pour accès paresseux
    private NonSerializableService getService() {
        return serviceInstance.get();
    }
}

Migration d'une Approche vers l'Autre

Migration EJB vers CDI :

// Étape 1 : EJB existant
@Stateless
public class PaymentServiceEJB {
    @EJB
    private AuditServiceEJB auditService;

    public PaymentResult processPayment(Payment payment) {
        // Logique existante
    }
}

// Étape 2 : Migration progressive
@Stateless  // Garde les transactions EJB
public class PaymentServiceEJB {
    @Inject  // Remplace @EJB par @Inject
    private AuditService auditService;  // Interface plutôt qu'implémentation

    public PaymentResult processPayment(Payment payment) {
        // Même logique
    }
}

// Étape 3 : Conversion complète CDI (si applicable)
@ApplicationScoped
public class PaymentService {
    @Inject
    private AuditService auditService;

    @Transactional  // Framework externe pour transactions
    public PaymentResult processPayment(Payment payment) {
        // Même logique métier
    }
}

Conseils pour Applications Legacy

Stratégie de migration graduelle :

  1. Phase 1 : Remplacer @EJB par @Inject pour injection EJB existants
  2. Phase 2 : Introduire beans CDI pour nouvelles fonctionnalités
  3. Phase 3 : Migrer services non-transactionnels vers CDI
  4. Phase 4 : Évaluer migration services transactionnels selon besoins

Coexistence durant migration :

// Service legacy EJB conservé pour transactions
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class LegacyOrderServiceEJB {
    @PersistenceContext
    private EntityManager em;

    public Order saveOrder(Order order) {
        return em.merge(order);  // Transaction critique conservée
    }
}

// Nouveau service CDI pour logique métier
@ApplicationScoped
public class OrderBusinessService {
    @Inject
    private LegacyOrderServiceEJB orderService;  // Réutilise EJB existant

    @Inject
    private ValidationService validationService;  // Nouveau service CDI

    public OrderResult processOrder(OrderRequest request) {
        // Nouvelle logique métier avec CDI
        ValidationResult validation = validationService.validate(request);
        if (validation.isValid()) {
            Order order = orderService.saveOrder(request.toOrder());  // Délègue à EJB
            return OrderResult.success(order.getId());
        }
        return OrderResult.invalid(validation.getErrors());
    }
}

9. Exemples de Code Complets

Exemple d'Application Utilisant CDI (@Inject)

Configuration de l'application :

<!-- beans.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       version="4.0" bean-discovery-mode="annotated">
    <interceptors>
        <class>com.example.LoggingInterceptor</class>
        <class>com.example.PerformanceInterceptor</class>
    </interceptors>
</beans>

Modèle de domaine :

// Entité JPA
@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String customerEmail;

    @Column(nullable = false)
    private BigDecimal totalAmount;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();

    // Constructeurs, getters, setters
    public Order() {
        this.createdAt = new Date();
        this.status = OrderStatus.PENDING;
    }

    // ... autres méthodes
}

@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @Column(nullable = false)
    private String productSku;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false)
    private BigDecimal unitPrice;

    // Constructeurs, getters, setters
}

Services CDI :

// Interface de service
public interface OrderService {
    OrderResult createOrder(OrderRequest request);
    Optional<Order> findOrder(Long orderId);
    List<Order> findOrdersByCustomer(String customerEmail);
}

// Implémentation CDI
@ApplicationScoped
public class OrderServiceImpl implements OrderService {

    @Inject
    private OrderRepository orderRepository;

    @Inject
    private ProductCatalogService productCatalog;

    @Inject
    private ValidationService validationService;

    @Inject
    private Event<OrderCreatedEvent> orderCreatedEvent;

    @Inject
    private Logger logger;

    @Override
    @Monitored  // Intercepteur personnalisé
    public OrderResult createOrder(OrderRequest request) {
        logger.info("Creating order for customer: {}", request.getCustomerEmail());

        // Validation
        ValidationResult validation = validationService.validateOrderRequest(request);
        if (!validation.isValid()) {
            return OrderResult.invalid(validation.getErrors());
        }

        try {
            // Construction de l'ordre
            Order order = buildOrder(request);

            // Sauvegarde
            Order savedOrder = orderRepository.save(order);

            // Événement CDI
            orderCreatedEvent.fire(new OrderCreatedEvent(savedOrder.getId(), 
                                                        savedOrder.getCustomerEmail()));

            logger.info("Order created successfully: {}", savedOrder.getId());
            return OrderResult.success(savedOrder);

        } catch (Exception e) {
            logger.error("Failed to create order for customer: " + request.getCustomerEmail(), e);
            return OrderResult.error("Order creation failed: " + e.getMessage());
        }
    }

    private Order buildOrder(OrderRequest request) {
        Order order = new Order();
        order.setCustomerEmail(request.getCustomerEmail());

        BigDecimal total = BigDecimal.ZERO;
        for (OrderItemRequest itemRequest : request.getItems()) {
            Product product = productCatalog.findBySku(itemRequest.getProductSku())
                .orElseThrow(() -> new IllegalArgumentException("Product not found: " + itemRequest.getProductSku()));

            OrderItem item = new OrderItem();
            item.setOrder(order);
            item.setProductSku(product.getSku());
            item.setQuantity(itemRequest.getQuantity());
            item.setUnitPrice(product.getPrice());

            order.getItems().add(item);
            total = total.add(product.getPrice().multiply(BigDecimal.valueOf(itemRequest.getQuantity())));
        }

        order.setTotalAmount(total);
        return order;
    }

    @Override
    public Optional<Order> findOrder(Long orderId) {
        return orderRepository.findById(orderId);
    }

    @Override
    public List<Order> findOrdersByCustomer(String customerEmail) {
        return orderRepository.findByCustomerEmail(customerEmail);
    }
}

// Repository CDI
@ApplicationScoped
public class OrderRepository {

    @Inject
    private EntityManager entityManager;

    public Order save(Order order) {
        if (order.getId() == null) {
            entityManager.persist(order);
            return order;
        } else {
            return entityManager.merge(order);
        }
    }

    public Optional<Order> findById(Long id) {
        Order order = entityManager.find(Order.class, id);
        return Optional.ofNullable(order);
    }

    public List<Order> findByCustomerEmail(String customerEmail) {
        TypedQuery<Order> query = entityManager.createQuery(
            "SELECT o FROM Order o WHERE o.customerEmail = :email ORDER BY o.createdAt DESC", 
            Order.class);
        query.setParameter("email", customerEmail);
        return query.getResultList();
    }
}

// Producteur pour EntityManager
@ApplicationScoped
public class JPAProducer {

    @PersistenceContext(unitName = "orderPU")
    private EntityManager entityManager;

    @Produces
    public EntityManager createEntityManager() {
        return entityManager;
    }

    @Produces
    public Logger createLogger(InjectionPoint injectionPoint) {
        return LoggerFactory.getLogger(injectionPoint.getBean().getBeanClass());
    }
}

Contrôleur JAX-RS CDI :

@Path("/orders")
@RequestScoped
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class OrderController {

    @Inject
    private OrderService orderService;

    @Inject
    private Logger logger;

    @POST
    @Monitored
    public Response createOrder(@Valid OrderRequest request) {
        logger.info("Received order creation request for: {}", request.getCustomerEmail());

        OrderResult result = orderService.createOrder(request);

        if (result.isSuccess()) {
            return Response.status(Response.Status.CREATED)
                    .entity(result.getOrder())
                    .build();
        } else if (result.isInvalid()) {
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(result.getValidationErrors())
                    .build();
        } else {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(result.getErrorMessage())
                    .build();
        }
    }

    @GET
    @Path("/{orderId}")
    public Response getOrder(@PathParam("orderId") Long orderId) {
        Optional<Order> order = orderService.findOrder(orderId);

        if (order.isPresent()) {
            return Response.ok(order.get()).build();
        } else {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
    }

    @GET
    @Path("/customer/{email}")
    public Response getOrdersByCustomer(@PathParam("email") String customerEmail) {
        List<Order> orders = orderService.findOrdersByCustomer(customerEmail);
        return Response.ok(orders).build();
    }
}

Exemple d'Application Utilisant EJB (@EJB)

Configuration EJB :

<!-- ejb-jar.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="https://jakarta.ee/xml/ns/jakartaee" version="4.0">
    <enterprise-beans>
        <session>
            <ejb-name>OrderManagementEJB</ejb-name>
            <business-remote>com.example.OrderManagementRemote</business-remote>
            <business-local>com.example.OrderManagementLocal</business-local>
        </session>
    </enterprise-beans>

    <assembly-descriptor>
        <security-role>
            <role-name>customer</role-name>
        </security-role>
        <security-role>
            <role-name>admin</role-name>
        </security-role>

        <method-permission>
            <role-name>customer</role-name>
            <method>
                <ejb-name>OrderManagementEJB</ejb-name>
                <method-name>createOrder</method-name>
            </method>
        </method-permission>

        <method-permission>
            <role-name>admin</role-name>
            <method>
                <ejb-name>OrderManagementEJB</ejb-name>
                <method-name>*</method-name>
            </method>
        </method-permission>
    </assembly-descriptor>
</ejb-jar>

Services EJB :

// Interface Remote pour distribution
@Remote
public interface OrderManagementRemote {
    OrderResult createOrder(OrderRequest request);
    Order findOrder(Long orderId);
    List<Order> findOrdersByCustomer(String customerEmail);
}

// Interface Local pour accès interne
@Local
public interface OrderManagementLocal extends OrderManagementRemote {
    void processOrderPayment(Long orderId, PaymentDetails payment);
    void cancelOrder(Long orderId, String reason);
}

// Implémentation EJB Stateless
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@DeclareRoles({"customer", "admin", "system"})
@RolesAllowed({"customer", "admin"})
public class OrderManagementEJB implements OrderManagementRemote, OrderManagementLocal {

    @PersistenceContext(unitName = "orderPU")
    private EntityManager entityManager;

    @EJB
    private PaymentProcessorEJB paymentProcessor;

    @EJB
    private InventoryManagerEJB inventoryManager;

    @EJB
    private AuditServiceEJB auditService;

    @Resource
    private SessionContext sessionContext;

    @Override
    @RolesAllowed({"customer", "admin"})
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public OrderResult createOrder(OrderRequest request) {
        String currentUser = sessionContext.getCallerPrincipal().getName();

        try {
            // Validation des autorisations
            if (!isAuthorizedForCustomer(currentUser, request.getCustomerEmail())) {
                throw new SecurityException("Not authorized to create order for this customer");
            }

            // Validation du stock
            boolean stockAvailable = inventoryManager.checkAvailability(request.getItems());
            if (!stockAvailable) {
                return OrderResult.invalid(Arrays.asList("Insufficient stock for requested items"));
            }

            // Création de l'ordre
            Order order = buildOrder(request);
            entityManager.persist(order);
            entityManager.flush(); // Force l'attribution de l'ID

            // Réservation du stock
            String reservationId = inventoryManager.reserveItems(order.getId(), request.getItems());
            order.setReservationId(reservationId);

            // Audit log
            auditService.logOrderCreation(order.getId(), currentUser);

            return OrderResult.success(order);

        } catch (Exception e) {
            // La transaction sera automatiquement rollback
            sessionContext.setRollbackOnly();
            return OrderResult.error("Order creation failed: " + e.getMessage());
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void processOrderPayment(Long orderId, PaymentDetails payment) {
        Order order = entityManager.find(Order.class, orderId);
        if (order == null) {
            throw new IllegalArgumentException("Order not found: " + orderId);
        }

        try {
            PaymentResult result = paymentProcessor.processPayment(payment);

            if (result.isSuccessful()) {
                order.setStatus(OrderStatus.PAID);
                order.setPaymentTransactionId(result.getTransactionId());

                // Confirm inventory reservation
                inventoryManager.confirmReservation(order.getReservationId());

                auditService.logPaymentProcessed(orderId, result.getTransactionId());
            } else {
                order.setStatus(OrderStatus.PAYMENT_FAILED);

                // Release inventory reservation
                inventoryManager.releaseReservation(order.getReservationId());

                auditService.logPaymentFailed(orderId, result.getErrorMessage());
            }

        } catch (Exception e) {
            order.setStatus(OrderStatus.PAYMENT_ERROR);
            sessionContext.setRollbackOnly();
            throw new EJBException("Payment processing failed", e);
        }
    }

    @Override
    @PermitAll
    public Order findOrder(Long orderId) {
        return entityManager.find(Order.class, orderId);
    }

    @Override
    @RolesAllowed({"admin"})
    public List<Order> findOrdersByCustomer(String customerEmail) {
        TypedQuery<Order> query = entityManager.createQuery(
            "SELECT o FROM Order o WHERE o.customerEmail = :email ORDER BY o.createdAt DESC", 
            Order.class);
        query.setParameter("email", customerEmail);
        return query.getResultList();
    }

    @Override
    @RolesAllowed({"admin", "system"})
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void cancelOrder(Long orderId, String reason) {
        Order order = entityManager.find(Order.class, orderId);
        if (order == null) {
            throw new IllegalArgumentException("Order not found: " + orderId);
        }

        if (order.getStatus() == OrderStatus.CANCELLED) {
            return; // Already cancelled
        }

        // Release any reservations
        if (order.getReservationId() != null) {
            inventoryManager.releaseReservation(order.getReservationId());
        }

        // Process refund if paid
        if (order.getStatus() == OrderStatus.PAID && order.getPaymentTransactionId() != null) {
            paymentProcessor.processRefund(order.getPaymentTransactionId(), order.getTotalAmount());
        }

        order.setStatus(OrderStatus.CANCELLED);
        order.setCancellationReason(reason);
        order.setCancelledAt(new Date());

        auditService.logOrderCancellation(orderId, reason, sessionContext.getCallerPrincipal().getName());
    }

    private boolean isAuthorizedForCustomer(String currentUser, String customerEmail) {
        // Vérification d'autorisation métier
        return sessionContext.isCallerInRole("admin") || currentUser.equals(customerEmail);
    }

    private Order buildOrder(OrderRequest request) {
        // Logique de construction identique à l'exemple CDI
        Order order = new Order();
        order.setCustomerEmail(request.getCustomerEmail());

        BigDecimal total = BigDecimal.ZERO;
        for (OrderItemRequest itemRequest : request.getItems()) {
            // Récupération produit et construction items
            // ... logique identique
        }

        order.setTotalAmount(total);
        return order;
    }
}

// EJB Service pour paiements
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@Local
public class PaymentProcessorEJB {

    @EJB
    private AuditServiceEJB auditService;

    public PaymentResult processPayment(PaymentDetails payment) {
        // Logique de traitement de paiement
        try {
            // Simulation appel service externe
            String transactionId = UUID.randomUUID().toString();

            // Log audit
            auditService.logPaymentAttempt(payment.getAmount(), payment.getPaymentMethod());

            // Simulation succès
            return PaymentResult.success(transactionId);

        } catch (Exception e) {
            return PaymentResult.failed("Payment processing error: " + e.getMessage());
        }
    }

    public RefundResult processRefund(String originalTransactionId, BigDecimal amount) {
        // Logique de remboursement
        try {
            String refundId = "REF-" + UUID.randomUUID().toString();
            auditService.logRefundProcessed(originalTransactionId, amount, refundId);
            return RefundResult.success(refundId);
        } catch (Exception e) {
            return RefundResult.failed("Refund processing error: " + e.getMessage());
        }
    }
}

// EJB Singleton pour audit
@Singleton
@Startup
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
@Lock(LockType.READ)  // Accès concurrent en lecture
public class AuditServiceEJB {

    @PersistenceContext(unitName = "auditPU")
    private EntityManager auditEntityManager;

    @Lock(LockType.WRITE)  // Exclusif pour écriture
    public void logOrderCreation(Long orderId, String userId) {
        AuditEvent event = new AuditEvent();
        event.setEventType("ORDER_CREATED");
        event.setEntityId(orderId.toString());
        event.setUserId(userId);
        event.setTimestamp(new Date());

        auditEntityManager.persist(event);
    }

    @Lock(LockType.WRITE)
    public void logPaymentProcessed(Long orderId, String transactionId) {
        AuditEvent event = new AuditEvent();
        event.setEventType("PAYMENT_PROCESSED");
        event.setEntityId(orderId.toString());
        event.setDetails("Transaction ID: " + transactionId);
        event.setTimestamp(new Date());

        auditEntityManager.persist(event);
    }

    // Autres méthodes d'audit...
}

Exemple d'Application Mixte

Architecture hybride recommandée :

// Couche de présentation CDI
@RequestScoped
@Named("orderController")
public class OrderWebController {

    @Inject  // Injection EJB via CDI
    private OrderManagementEJB orderService;

    @Inject  // Service CDI pour UI
    private MessageService messageService;

    @Inject  // Événements CDI
    private Event<UIEvent> uiEvents;

    // Propriétés pour binding JSF
    private OrderRequest currentOrder = new OrderRequest();
    private List<Order> customerOrders;

    public String submitOrder() {
        try {
            OrderResult result = orderService.createOrder(currentOrder);

            if (result.isSuccess()) {
                messageService.addInfoMessage("Order created successfully!");
                uiEvents.fire(new OrderSubmittedEvent(result.getOrder().getId()));

                // Reset form
                currentOrder = new OrderRequest();
                return "order-confirmation?faces-redirect=true&orderId=" + result.getOrder().getId();

            } else if (result.isInvalid()) {
                for (String error : result.getValidationErrors()) {
                    messageService.addErrorMessage(error);
                }
                return null; // Stay on current page

            } else {
                messageService.addErrorMessage("Order submission failed: " + result.getErrorMessage());
                return null;
            }

        } catch (SecurityException e) {
            messageService.addErrorMessage("You are not authorized to create this order");
            return "access-denied";
        } catch (Exception e) {
            messageService.addErrorMessage("An unexpected error occurred");
            return "error";
        }
    }

    @PostConstruct
    public void init() {
        // Chargement des données initiales
        loadCustomerOrders();
    }

    public void loadCustomerOrders() {
        try {
            String customerEmail = getCurrentUserEmail();
            this.customerOrders = orderService.findOrdersByCustomer(customerEmail);
        } catch (Exception e) {
            messageService.addErrorMessage("Failed to load order history");
            this.customerOrders = Collections.emptyList();
        }
    }

    // Méthode helper
    private String getCurrentUserEmail() {
        // Récupération depuis contexte de sécurité ou session
        return FacesContext.getCurrentInstance()
            .getExternalContext()
            .getRemoteUser();
    }

    // Getters/Setters pour JSF binding
    public OrderRequest getCurrentOrder() { return currentOrder; }
    public void setCurrentOrder(OrderRequest currentOrder) { this.currentOrder = currentOrder; }

    public List<Order> getCustomerOrders() { return customerOrders; }
}

// Service CDI pour gestion des messages UI
@RequestScoped
public class MessageService {

    private List<FacesMessage> messages = new ArrayList<>();

    public void addInfoMessage(String message) {
        addMessage(FacesMessage.SEVERITY_INFO, message);
    }

    public void addErrorMessage(String message) {
        addMessage(FacesMessage.SEVERITY_ERROR, message);
    }

    public void addWarningMessage(String message) {
        addMessage(FacesMessage.SEVERITY_WARN, message);
    }

    private void addMessage(FacesMessage.Severity severity, String message) {
        FacesMessage facesMessage = new FacesMessage(severity, message, null);
        FacesContext.getCurrentInstance().addMessage(null, facesMessage);
        messages.add(facesMessage);
    }

    public List<FacesMessage> getMessages() {
        return Collections.unmodifiableList(messages);
    }

    public boolean hasMessages() {
        return !messages.isEmpty();
    }
}

// Observateur d'événements CDI
@ApplicationScoped
public class OrderEventHandler {

    @Inject
    private Logger logger;

    @Inject  // Peut injecter EJB pour actions système
    private NotificationServiceEJB notificationService;

    public void onOrderSubmitted(@Observes OrderSubmittedEvent event) {
        logger.info("Order submitted via UI: {}", event.getOrderId());

        // Actions UI spécifiques
        updateDashboardCounters();
        refreshOrderHistory();
    }

    public void onOrderCreated(@Observes OrderCreatedEvent event) {
        logger.info("Order created: {} for customer: {}", event.getOrderId(), event.getCustomerEmail());

        // Délégation vers service EJB pour actions métier
        try {
            notificationService.sendOrderConfirmation(event.getOrderId(), event.getCustomerEmail());
        } catch (Exception e) {
            logger.error("Failed to send order confirmation", e);
        }
    }

    private void updateDashboardCounters() {
        // Logique de mise à jour UI
    }

    private void refreshOrderHistory() {
        // Rafraîchissement cache UI
    }
}

// Service EJB pour notifications système
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class NotificationServiceEJB {

    @EJB
    private AuditServiceEJB auditService;

    @Resource(mappedName = "java:/JmsXA/DefaultJMSConnectionFactory")
    private ConnectionFactory connectionFactory;

    @Resource(mappedName = "java:/jms/queue/notifications")
    private Queue notificationQueue;

    public void sendOrderConfirmation(Long orderId, String customerEmail) {
        try (Connection connection = connectionFactory.createConnection();
             Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
             MessageProducer producer = session.createProducer(notificationQueue)) {

            ObjectMessage message = session.createObjectMessage();
            message.setObject(new NotificationMessage("ORDER_CONFIRMATION", orderId, customerEmail));
            message.setStringProperty("notificationType", "ORDER_CONFIRMATION");
            message.setLongProperty("orderId", orderId);

            producer.send(message);

            auditService.logNotificationSent("ORDER_CONFIRMATION", orderId, customerEmail);

        } catch (Exception e) {
            throw new EJBException("Failed to send order confirmation", e);
        }
    }
}

Cette architecture hybride illustre les bonnes pratiques :

  • EJBs pour la logique métier transactionnelle et les services système
  • CDI pour la couche présentation, la gestion d'état UI et la coordination
  • Intégration fluide via @Inject pour injecter EJBs dans beans CDI
  • Séparation des responsabilités selon les forces de chaque technologie

10. Mémo Synthétique Final

Tableau Récapitulatif des Critères de Choix

Critère CDI (@Inject) EJB (@EJB) Recommandation
Type d'application Applications web, POJOs, composants légers Applications d'entreprise, services transactionnels CDI pour web/UI, EJB pour logique métier critique
Gestion des transactions Limitée, dépend du container ou frameworks tiers Complète et déclarative (@TransactionAttribute) EJB pour besoins transactionnels complexes
Sécurité Basique via intercepteurs personnalisés Déclarative complète (@RolesAllowed, @RunAs) EJB pour sécurité d'entreprise robuste
Performance Overhead minimal, injection directe Overhead des proxies et pooling CDI pour haute performance, EJB acceptable
Cycle de vie Scopes flexibles (@RequestScoped, @SessionScoped, etc.) Modèles fixes (@Stateless, @Stateful, @Singleton) CDI pour flexibilité, EJB pour contrôle strict
Distribution/Remote Non supporté nativement Support natif des interfaces remote EJB obligatoire pour applications distribuées
Testing Excellent support avec mocks et alternatives Plus complexe, nécessite container de test CDI pour facilité de test
Configuration Minimale (beans.xml optionnel en CDI 1.1+) Plus complexe (ejb-jar.xml, annotations) CDI pour simplicité de configuration
Interopérabilité Peut injecter des EJBs et autres beans Limité aux EJBs principalement CDI pour architecture mixte
Évènements Système d'événements intégré (@Event, @Observes) Limité aux MDB pour JMS CDI pour communication découplée

Mémo Décisionnel

✅ CHOISIR CDI quand :

  • Développement d'applications web modernes (JSF, JAX-RS)
  • Besoin de flexibilité dans la gestion du cycle de vie
  • Architecture événementielle et communication découplée
  • Tests unitaires fréquents et mocking intensif
  • Intégration avec frameworks tiers (Spring, etc.)
  • Performance critique avec overhead minimal
  • Développement agile et prototypage rapide

✅ CHOISIR EJB quand :

  • Gestion transactionnelle ACID complexe requise
  • Applications distribuées avec interfaces remote
  • Sécurité d'entreprise robuste nécessaire
  • Services système (timers, messaging, asynchrone)
  • Applications legacy nécessitant compatibilité
  • Haute disponibilité avec clustering
  • Conformité stricte aux standards Java EE

🔄 ARCHITECTURE HYBRIDE recommandée :

  • Couche EJB : Services métier, accès données, gestion transactionnelle
  • Couche CDI : Présentation, coordination workflow, gestion d'état UI
  • Intégration : @Inject pour injecter EJBs dans beans CDI
  • Communication : Événements CDI pour découplage inter-couches

Points Clés à Retenir

  1. Complémentarité : CDI et EJB sont complémentaires, pas concurrents
  2. Intégration native : @Inject peut injecter des EJBs transparement
  3. Migration progressive : Possible de migrer graduellement d'EJB vers CDI
  4. Wildfly optimisé : Support natif excellent pour les deux approches
  5. Choix architectural : Basé sur les besoins fonctionnels, pas les préférences techniques

Cette documentation fournit une base complète pour la prise de décision architecturale entre CDI (@Inject) et EJB (@EJB) dans l'environnement JBoss Wildfly, en s'appuyant sur les spécifications officielles et les bonnes pratiques industrielles éprouvées.