Java EE / Jakarta EE
Enterprise Beans 4 : session beans stateless, stateful et singleton
Intermédiaire10 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Les session beans sont les composants de la couche métier de Jakarta EE : le conteneur les instancie, les met en pool, enveloppe chaque appel dans une transaction et un contrôle de sécurité, et peut les exécuter de façon asynchrone ou planifiée. Ce guide présente les beans stateless, stateful et singleton, leurs vues et leurs cycles de vie, les intercepteurs, les timers et le contrôle de la concurrence, puis trace la frontière entre ce qui exige encore un enterprise bean et ce qu'un simple bean CDI fait tout aussi bien.
Ce que vous allez apprendre
- Choisir entre session bean stateless, stateful et singleton selon le cas d'usage
- Exposer un bean via une vue sans interface, locale ou distante, et savoir ce que coûte chacune
- Utiliser les services du conteneur : transactions, sécurité déclarative, intercepteurs, timers et méthodes asynchrones
- Contrôler la concurrence d'un singleton avec @Lock et comprendre le pooling et la passivation
- Savoir quand un bean CDI annoté @Transactional remplace un enterprise bean
Enterprise JavaBeans est la partie de la plateforme qui a la pire réputation, et la meilleure excuse : ce dont on se souvient, c'est la version 2.x, avec ses home interfaces, ses RemoteException obligatoires et ses centaines de lignes de XML par composant. La version 3.0 (2006) a remplacé tout cela par des classes ordinaires et une poignée d'annotations, et depuis, la spécification (aujourd'hui Jakarta Enterprise Beans 4.0) offre un moyen compact de dire « exécute cette classe dans le conteneur et donne-lui transactions, sécurité, pooling et planification ». Comprendre ce que cela vous apporte, et ce que CDI fournit désormais tout seul, vous permet d'utiliser les enterprise beans là où ils sont rentables, et nulle part ailleurs.
Ce que le conteneur fait pour un session bean
Un session bean est une classe annotée @Stateless, @Stateful ou @Singleton. Vous ne l'instanciez jamais : le conteneur s'en charge, et chaque appel d'un client passe par un proxy que le conteneur place devant l'instance. C'est sur ce proxy que se greffent les services de la plateforme :
- Transactions : par défaut, chaque méthode métier s'exécute dans une transaction JTA (
REQUIRED), démarrée s'il n'en existe pas et validée au retour de la méthode la plus externe ; une exception runtime l'annule. Le guide sur les transactions détaille les attributs. - Sécurité :
@RolesAllowed,@PermitAllet@DenyAll, posées sur la classe ou la méthode, sont vérifiées avant l'appel ;SessionContext.getCallerPrincipal()vous indique qui appelle. - Concurrence : le conteneur garantit un accès mono-thread aux instances stateless et stateful, si bien que le code du bean n'a besoin d'aucun verrou.
- Injection : les champs
@Inject,@EJB,@PersistenceContextet@Resourcesont renseignés avant l'exécution de@PostConstruct. - Interception : les callbacks de cycle de vie et les intercepteurs
@AroundInvokes'exécutent autour des méthodes. - Accès distant, planification et asynchronisme : les vues distantes, les timers
@Scheduleet les méthodes@Asynchronousne sont disponibles que sur les enterprise beans.
Le prix à payer, c'est le proxy : appeler une méthode sur this le contourne, et chaque appel géré entraîne un certain surcoût. Ni l'un ni l'autre n'est significatif dans du code normal, mais les deux expliquent toute une famille de bugs du type « mon annotation est ignorée ».
Session beans stateless
C'est le cheval de trait. Un bean stateless ne conserve aucun état conversationnel entre deux appels : le conteneur maintient donc un pool d'instances et en prête une à chaque invocation. Deux appels consécutifs du même client peuvent tomber sur deux instances différentes ; les champs d'instance sont donc réservés aux ressources injectées et à la configuration immuable.
package shop.service;
import java.util.List;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
public class OrderService {
@PersistenceContext(unitName = "shop")
EntityManager em; // container-managed, transaction-scoped
public List<Order> find(String customer, OrderStatus status) {
return em.createQuery("""
select o from Order o
where o.customer.name = :customer
and (:status is null or o.status = :status)
order by o.placedAt desc""", Order.class)
.setParameter("customer", customer)
.setParameter("status", status)
.getResultList();
}
@RolesAllowed("sales")
public Order place(Long customerId, List<OrderLine> lines) {
Customer customer = em.getReference(Customer.class, customerId);
Order order = new Order(customer, lines);
em.persist(order); // committed when this method returns normally
return order;
}
}
Pas d'interface, pas de descripteur, pas de transaction explicite. find et place s'exécutent chacune dans une transaction démarrée par le conteneur, place n'est accessible qu'aux utilisateurs du rôle sales, et l'EntityManager est lié à cette transaction. C'est la vue sans interface (no-interface view) introduite par EJB 3.1 : les clients injectent directement la classe.
@Inject OrderService orderService; // CDI injection works for session beans
@EJB OrderService orderService; // the older enterprise-bean-specific injection; still valid
La taille des pools relève de la configuration de l'éditeur (strict-max-pool chez WildFly, maxPoolSize chez Open Liberty, etc.). Quand le pool est épuisé, les appelants attendent, ce qui fait du pool un limiteur naturel devant une base de données : dimensionnez-le en tenant compte de la taille du pool de connexions.
Session beans stateful
Un bean stateful conserve un état pour un seul client sur plusieurs appels : un panier, un assistant en plusieurs étapes, un workflow qui accumule des données avant de valider. Le conteneur crée une instance neuve lorsque le client obtient sa première référence, et la détruit à la fin d'une méthode annotée @Remove ou à l'expiration du bean (@StatefulTimeout).
import java.util.concurrent.TimeUnit;
import jakarta.ejb.Remove;
import jakarta.ejb.Stateful;
import jakarta.ejb.StatefulTimeout;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.PersistenceContextType;
@Stateful
@StatefulTimeout(value = 30, unit = TimeUnit.MINUTES)
public class Checkout {
@PersistenceContext(unitName = "shop", type = PersistenceContextType.EXTENDED)
EntityManager em; // extended: entities stay managed across calls
private Order draft;
public void start(Customer customer) { draft = new Order(customer); }
public void add(Product p, int qty) { draft.addLine(p, qty); }
@Remove
public Order confirm() {
em.persist(draft); // the extended context flushes here, inside the transaction
return draft;
}
@Remove
public void cancel() { draft = null; }
}
Deux choses distinguent un bean stateful d'un bean CDI @SessionScoped. D'abord, le contexte de persistance étendu : les entités chargées lors d'un appel restent managées lors du suivant, sans merge, jusqu'au commit de la transaction qui clôt la conversation ; c'est le seul endroit de la plateforme où c'est possible. Ensuite, la passivation : un bean stateful inactif peut être sérialisé sur disque pour libérer de la mémoire, ses champs doivent donc être sérialisables ou transient, exactement comme pour un bean Faces view-scoped. En pratique, les beans stateful sont rares : l'essentiel de l'état conversationnel vit dans la couche web, et le contexte de persistance étendu reste leur principale justification.
Session beans singleton
Un singleton est instancié une seule fois par application et partagé par tous les clients, ce qui en fait l'endroit idéal pour les caches, le travail de démarrage et la coordination. Puisqu'il est partagé, le conteneur doit gérer les accès concurrents, et la spécification propose deux modes.
import jakarta.annotation.PostConstruct;
import jakarta.ejb.*;
@Singleton
@Startup // create at deployment, not at first use
@DependsOn("ConfigurationLoader") // order start-up relative to another singleton
@Lock(LockType.READ) // default for all methods: concurrent readers
public class ExchangeRates {
private Map<String, BigDecimal> rates = Map.of();
@PostConstruct
void load() { rates = fetchFromProvider(); }
public BigDecimal rate(String currency) { return rates.get(currency); } // READ lock
@Lock(LockType.WRITE) // exclusive: readers wait until refresh finishes
@Schedule(hour = "*", minute = "0", persistent = false)
public void refresh() { rates = fetchFromProvider(); }
@AccessTimeout(value = 5, unit = TimeUnit.SECONDS) // fail rather than block forever
@Lock(LockType.WRITE)
public void override(String currency, BigDecimal rate) { /* ... */ }
}
Avec la concurrence gérée par le conteneur (le mode par défaut), les méthodes @Lock(READ) s'exécutent en parallèle et les méthodes @Lock(WRITE) en exclusivité : c'est le pattern du verrou lecture/écriture sans avoir à écrire de verrou. Le verrou par défaut d'un singleton est WRITE, qui sérialise tous les appels ; déclarez donc READ au niveau de la classe et marquez les méthodes qui modifient l'état. @ConcurrencyManagement(BEAN) désactive la gestion par le conteneur et vous laisse utiliser vous-même synchronized ou java.util.concurrent.
Les singletons @Startup sont la réponse de la plateforme à « exécute ceci au démarrage de l'application » ; combinés à @DependsOn, ils permettent une initialisation ordonnée. CDI 4.0 propose une alternative qui ne requiert pas d'enterprise bean : observer @Initialized(ApplicationScoped.class).
Vues : sans interface, locale et distante
Un bean expose une ou plusieurs vues, et c'est la vue qui détermine l'aspect du proxy pour les clients :
| Vue | Déclarée par | Sémantique d'appel | À utiliser quand |
|---|---|---|---|
| Sans interface | Rien (méthodes publiques de la classe) | Dans la JVM, par référence | Presque toujours |
| Locale | Interface @Local | Dans la JVM, par référence | Vous voulez une interface pour les tests ou plusieurs implémentations |
| Distante | Interface @Remote | Entre JVM, par valeur (sérialisé) | Un autre processus doit appeler le bean |
Dans le code, les appels distants ont exactement la même allure, mais leur comportement n'a rien à voir : chaque argument est copié, chaque résultat est recopié en retour, un aller-retour réseau a lieu, et les échecs remontent sous forme d'EJBException enveloppant la cause. Un client distant situé hors du serveur recherche le bean par son nom JNDI portable, à l'aide des bibliothèques clientes fournies par l'éditeur :
var props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory");
props.put(Context.PROVIDER_URL, "remote+http://app-server:8080");
var ctx = new InitialContext(props);
var orders = (OrderServiceRemote) ctx.lookup("ejb:/shop/OrderService!shop.service.OrderServiceRemote");
L'habitude héritée de l'ère J2EE, qui consiste à découper une application en une couche web appelant une couche métier distante, est presque toujours une erreur aujourd'hui : si deux processus doivent communiquer, une frontière REST ou de messaging est plus simple à faire évoluer, à sécuriser et à superviser. Enterprise Beans 4.0 a rendu optionnelle l'interopérabilité CORBA/IIOP et supprimé purement et simplement les entity beans EJB 2.x et les endpoints JAX-RPC ; les protocoles distants restants sont donc propres à chaque éditeur.
Callbacks de cycle de vie et intercepteurs
Tout session bean peut annoter une méthode avec @PostConstruct (après l'injection, avant le premier appel métier) et @PreDestroy (avant que l'instance ne soit détruite) ; les beans stateful y ajoutent @PrePassivate et @PostActivate. C'est là que vous ouvrez et fermez les ressources dont le bean est propriétaire.
Les intercepteurs enveloppent les méthodes métier ; ils sont, côté enterprise beans, l'équivalent des aspects. Une classe d'intercepteur déclare une méthode @AroundInvoke qui reçoit un InvocationContext et doit appeler proceed() :
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@Timed @Interceptor @Priority(Interceptor.Priority.APPLICATION)
public class TimingInterceptor {
@AroundInvoke
Object time(InvocationContext ctx) throws Exception {
long start = System.nanoTime();
try {
return ctx.proceed();
} finally {
log.info("{}.{} took {} µs", ctx.getTarget().getClass().getSimpleName(),
ctx.getMethod().getName(), (System.nanoTime() - start) / 1000);
}
}
}
Ici, @Timed est un interceptor binding CDI (une annotation que vous définissez avec @InterceptorBinding) : tout bean ou toute méthode portant l'annotation @Timed se voit appliquer l'intercepteur. L'ancienne annotation @Interceptors(TimingInterceptor.class) de la spécification Enterprise Beans fonctionne toujours, mais couple le bean à la classe d'intercepteur. Les intercepteurs s'exécutent à l'intérieur de l'interception transactionnelle et de sécurité propre au conteneur, dans l'ordre de @Priority, et s'appliquent aux beans CDI comme aux session beans : le modèle d'interception est commun.
Timers et méthodes asynchrones
@Schedule déclare un timer calendaire avec des champs proches de cron (second, minute, hour, dayOfWeek, dayOfMonth, month, year, plus timezone). Les timers sont persistants par défaut : le conteneur les stocke et rejoue les exécutions manquées après un redémarrage, ce qui est exactement ce qu'on veut pour un traitement nocturne et exactement ce qu'on ne veut pas pour un rafraîchissement de cache ; d'où le persistent = false ci-dessus. Pour les timers créés à l'exécution, injectez TimerService et appelez createCalendarTimer ou createIntervalTimer ; le callback est une méthode annotée @Timeout. Dans un cluster, les timers persistants ne se déclenchent que sur un seul nœud, les timers non persistants sur chaque nœud.
@Asynchronous demande au conteneur d'exécuter une méthode sur un thread distinct et de rendre la main immédiatement. Une méthode void fonctionne en fire-and-forget ; une méthode qui renvoie un Future<T> permet à l'appelant de récupérer le résultat ou d'annuler :
@Asynchronous
public Future<Invoice> generateInvoice(Long orderId) {
Invoice invoice = render(orderId); // runs in its own transaction, on a container thread
return new AsyncResult<>(invoice);
}
La méthode asynchrone s'exécute dans un nouveau contexte transactionnel (celui de l'appelant n'est pas propagé), tandis que son contexte de sécurité est propagé. Le ManagedExecutorService de Jakarta Concurrency couvre un terrain similaire pour le code hors bean ; @Asynchronous reste l'option la plus simple quand le traitement vit déjà dans un bean, et les serveurs de l'époque CDI 4.0 prennent aussi en charge l'@Asynchronous de Jakarta Concurrency 3.0 sur les beans CDI, qui renvoie un CompletableFuture.
Enterprise beans ou beans CDI ?
CDI est arrivé avec Java EE 6 et a absorbé l'essentiel de ce qui exigeait autrefois un enterprise bean. Voici comment trancher aujourd'hui :
| Besoin | Bean CDI | Session bean |
|---|---|---|
| Injection, scopes, événements, intercepteurs, décorateurs | Oui | Oui (via CDI) |
| Transactions | @Transactional (JTA 1.2+) | @TransactionAttribute, REQUIRED par défaut sur chaque méthode |
| Contrôles de rôles déclaratifs | Par programmation via SecurityContext.isCallerInRole ; @RolesAllowed sur les beans CDI uniquement comme extension de l'éditeur | @RolesAllowed partout |
| Pooling et limitation du nombre d'instances | Non | Pool stateless |
| Interface distante | Non | @Remote |
| Consommation de messages | Non (sauf extensions d'éditeur) | Message-driven beans |
| Timers calendaires | Par programmation uniquement (ManagedScheduledExecutorService) ; Jakarta Concurrency 3.1 dans EE 11 ajoute les méthodes asynchrones planifiées | @Schedule, TimerService |
| Contexte de persistance étendu | Non | Bean stateful |
| Ordre de démarrage | Observateur @Initialized | @Startup + @DependsOn |
| Disponibilité | Tous les profils, Core compris | Web Profile (Lite) ou Platform |
Une règle raisonnable : écrivez vos services en beans CDI @ApplicationScoped avec @Transactional, sauf si vous avez besoin d'une fonctionnalité de la colonne de droite, auquel cas l'annotation supplémentaire ne coûte rien. Les bases de code de l'époque Java EE 6 et 7 penchent dans l'autre sens (@Stateless partout) et rien ne presse de les convertir : les deux modèles interopèrent sans friction.
Erreurs fréquentes
- Appeler
this.otherMethod()en s'attendant à ce queREQUIRES_NEWou@RolesAlloweds'applique. Le proxy est contourné. Injectez le bean dans lui-même (@Inject OrderService self;) et appelezself.otherMethod(). - Stocker de l'état dans un bean stateless. Un champ renseigné dans une méthode et lu dans une autre fonctionnera en test avec une seule instance, puis échouera en charge lorsque le pool fournira une autre instance.
- Des singletons avec le verrou
WRITEpar défaut. Tous les appels sont sérialisés ; le débit s'effondre dès qu'il y a de la concurrence. Placez@Lock(READ)sur la classe. - Des timers persistants pour un travail éphémère. Après un redémarrage, le conteneur rejoue chaque déclenchement manqué. Utilisez
persistent = falsepour les rafraîchissements et les heartbeats. - Des vues distantes au sein d'un même déploiement. Copier les arguments pour un appel dans la même JVM n'est que du surcoût. Utilisez la vue sans interface ou la vue locale.
- Attraper l'exception runtime dans le bean et rendre la main normalement. La transaction est déjà marquée pour rollback au moment où une
EJBExceptionatteint l'appelant ; si vous l'attrapez vous-même, vérifiezsessionContext.getRollbackOnly()ou levez plutôt une@ApplicationException. - Des champs non sérialisables dans les beans stateful. La passivation lève une exception ; marquez-les
transientet reconstruisez-les dans@PostActivate.
À retenir
- Un session bean est une classe gérée par le conteneur, placée derrière un proxy ; ce proxy fournit les transactions, les contrôles de sécurité, l'accès mono-thread, l'injection et les intercepteurs.
- Les beans stateless sont mis en pool et ne conservent aucun état client ; les beans stateful conservent un état par client et peuvent posséder un contexte de persistance étendu ; les singletons sont partagés et ont besoin de
@Lockpour autoriser les lectures concurrentes. - Privilégiez la vue sans interface ; réservez
@Remoteaux appels entre processus, et préférez REST ou le messaging pour cette frontière. - Les timers (
@Schedule,TimerService) et les méthodes@Asynchronoussont des fonctionnalités du conteneur inaccessibles aux objets ordinaires ; rendez les timers non persistants sauf si les exécutions manquées doivent être rejouées. - Les intercepteurs, les callbacks de cycle de vie et l'injection sont communs avec CDI ; un bean CDI avec
@Transactionalremplace la plupart des beans stateless. - Enterprise Beans 4.0 a supprimé les entity beans EJB 2.x et JAX-RPC ; la spécification est stable et a peu de chances d'évoluer encore.
Sources et lectures complémentaires
Questions fréquentes
- Les EJB sont-ils encore utilisés, ou CDI les a-t-il remplacés ?
- Les deux sont utilisés. CDI gère l'injection, les scopes, les événements et, avec @Transactional, les transactions : une grande partie de la logique métier nouvelle s'écrit donc en beans CDI. Les enterprise beans restent le moyen standard d'obtenir, sur un serveur Jakarta EE, des interfaces distantes, des consommateurs message-driven, des timers, des méthodes @Asynchronous, des contrôles de rôles déclaratifs et une logique de démarrage dans un singleton. La spécification est stable plutôt qu'en évolution : la version 4.0 n'a pas bougé depuis Jakarta EE 9.
- Pourquoi mettre en pool un bean stateless s'il n'a pas d'état ?
- Parce que sa création n'est pas gratuite : le conteneur doit réaliser l'injection, résoudre les ressources et appeler @PostConstruct. Un pool d'instances déjà initialisées permet de servir une requête immédiatement et garantit qu'un seul thread utilise une instance à la fois, si bien que votre code n'a jamais besoin de synchronisation. La taille du pool plafonne la concurrence, ce qui en fait aussi un limiteur de débit bien pratique.
- Quelle différence entre une interface locale et une interface distante ?
- Une vue locale correspond à un appel dans la même JVM : les arguments sont passés par référence et le surcoût se limite à un appel de proxy. Une vue distante (@Remote) est conçue pour être appelée depuis une autre JVM via un protocole propre à l'éditeur : arguments et résultats sont sérialisés et passés par valeur, les exceptions sont enveloppées dans une EJBException, et l'appel est plus lent de plusieurs ordres de grandeur. N'utilisez le distant que s'il existe réellement un second processus.
- Puis-je appeler une méthode d'un session bean depuis une autre méthode du même bean et obtenir une nouvelle transaction ?
- Pas en l'appelant directement via this : le proxy du conteneur est court-circuité, donc aucun attribut transactionnel, contrôle de sécurité ni intercepteur ne s'applique. Injectez le bean dans lui-même via son interface métier (ou SessionContext.getBusinessObject) et passez par cette référence.