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 :
@Injectpeut injecter des EJBs,@EJBreste 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.xmlen modeannotated - 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
annotatedrecommandé vsallpour 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 :
@Injectpour injecter EJBs dans beans CDI
7. Diagrammes PlantUML Illustratifs
Diagramme de Séquence : Injection CDI
Diagramme de Séquence : Injection EJB
Diagramme de Classes : Architecture CDI
Diagramme de Classes : Architecture EJB
Diagramme de Composants : Comparaison CDI vs EJB
Logigramme de Décision
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 :
- Phase 1 : Remplacer
@EJBpar@Injectpour injection EJB existants - Phase 2 : Introduire beans CDI pour nouvelles fonctionnalités
- Phase 3 : Migrer services non-transactionnels vers CDI
- 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
@Injectpour 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 :
@Injectpour injecter EJBs dans beans CDI - Communication : Événements CDI pour découplage inter-couches
Points Clés à Retenir
- Complémentarité : CDI et EJB sont complémentaires, pas concurrents
- Intégration native :
@Injectpeut injecter des EJBs transparement - Migration progressive : Possible de migrer graduellement d'EJB vers CDI
- Wildfly optimisé : Support natif excellent pour les deux approches
- 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.