Java EE / Jakarta EE
Message-driven beans et Jakarta Messaging (JMS 3.1) dans le conteneur
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 message-driven beans sont le moyen qu'offre le conteneur pour consommer des messages Jakarta Messaging (JMS) : vous écrivez une seule méthode onMessage et le serveur gère connexions, threads, transactions et redélivraison. Ce guide explique comment un MDB est activé et configuré, comment envoyer depuis un session bean avec l'API simplifiée de JMS 2.0+, comment les transactions gérées par le conteneur interagissent avec l'acquittement et la redélivraison, et comment éviter les poison messages, la perte d'ordre et les traitements en double.
Ce que vous allez apprendre
- Écrire et configurer un message-driven bean avec des propriétés d'activation pour une file ou un topic
- Envoyer des messages depuis du code conteneur grâce à un JMSContext injecté et à des définitions de ressources
- Expliquer comment les transactions gérées par le conteneur pilotent l'acquittement, le rollback et la redélivraison
- Utiliser les sélecteurs, les abonnements durables et partagés, et contrôler la concurrence des MDB
- Concevoir des consommateurs qui tolèrent les doublons et les poison messages
L'essentiel de ce qu'il faut savoir sur la messagerie (pourquoi les files découplent les systèmes, ce que garantit un broker, en quoi un topic diffère d'une file) ne dépend pas de Jakarta EE et est traité dans le sujet messagerie. Ce que la plateforme apporte, c'est la possibilité de consommer et de produire des messages sans gérer vous-même connexions, sessions, threads ni acquittements : le message-driven bean côté consommateur et un JMSContext injecté côté producteur, tous deux branchés sur le gestionnaire de transactions du conteneur. Cette intégration est puissante, mais c'est aussi la source de la plupart des surprises ; ce guide se concentre donc sur le comportement du conteneur plutôt que sur la surface de l'API.
Côté consommateur : les message-driven beans
Un message-driven bean (MDB) est une classe annotée @MessageDriven qui implémente jakarta.jms.MessageListener. Le conteneur se connecte au broker à votre place, crée un pool d'instances et appelle onMessage sur l'une d'elles pour chaque message reçu.
package shop.messaging;
import jakarta.ejb.ActivationConfigProperty;
import jakarta.ejb.MessageDriven;
import jakarta.inject.Inject;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;
import jakarta.jms.TextMessage;
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "java:/jms/queue/orders"),
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "jakarta.jms.Queue"),
@ActivationConfigProperty(propertyName = "messageSelector", propertyValue = "region = 'EU'")
})
public class OrderPlacedListener implements MessageListener {
@Inject
OrderProcessor processor; // the business logic lives in a testable bean
@Override
public void onMessage(Message message) {
try {
String payload = message.getBody(String.class); // JMS 2.0+: typed body access
String orderId = message.getStringProperty("orderId");
processor.process(orderId, payload);
} catch (JMSException e) {
throw new IllegalStateException("cannot read message", e); // roll back and redeliver
}
}
}
onMessage ne renvoie rien et ne déclare aucune exception checked ; la seule façon de signaler un échec est de lever une exception runtime ou de marquer la transaction pour rollback. La configuration d'activation indique au conteneur quoi consommer. Les propriétés définies par la spécification Messaging sont les suivantes :
| Propriété | Signification |
|---|---|
destinationLookup | Nom JNDI de la file ou du topic |
destinationType | jakarta.jms.Queue ou jakarta.jms.Topic |
connectionFactoryLookup | Nom JNDI de la connection factory ; sinon, celle par défaut du serveur |
messageSelector | Filtre de style SQL-92 sur les en-têtes et propriétés, évalué par le broker |
acknowledgeMode | Auto-acknowledge ou Dups-ok-acknowledge ; pertinent uniquement avec des transactions gérées par le bean |
subscriptionDurability | Durable ou NonDurable pour les topics |
subscriptionName, clientId | Identifient un abonnement durable |
Tout le reste (taille du pool, délai de redélivraison, taille de lot) relève des propriétés propriétaires du resource adapter qui relie le serveur au broker. Ce n'est pas un détail : formellement, un MDB est un endpoint de l'architecture Jakarta Connectors, et la même annotation fonctionne avec des adapters pour des systèmes non JMS (mail, fichiers, Kafka), à condition d'implémenter l'interface listener de l'adapter au lieu de MessageListener. Quand un serveur dispose de plusieurs adapters, celui qu'utilise un MDB est choisi par les réglages par défaut du serveur ou nommé dans le descripteur de déploiement propriétaire (par exemple jboss-ejb3.xml sur WildFly).
Il vaut mieux définir les destinations dans le serveur, mais pour les tests et les déploiements autonomes vous pouvez les déclarer dans le code :
@JMSDestinationDefinition(
name = "java:/jms/queue/orders",
interfaceName = "jakarta.jms.Queue",
destinationName = "orders")
@JMSConnectionFactoryDefinition(
name = "java:/jms/OrdersCF",
transactional = true)
@ApplicationScoped
public class MessagingResources { }
Côté producteur : JMSContext
JMS 1.1 imposait une Connection, une Session, un MessageProducer et un try/finally pour tout refermer. JMS 2.0 (Java EE 7) a introduit l'API simplifiée : JMSContext encapsule connexion et session, JMSProducer est un builder fluent, et dans un conteneur vous injectez le contexte sans jamais le fermer.
import jakarta.annotation.Resource;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.jms.*;
@Stateless
public class OrderPublisher {
@Inject
@JMSConnectionFactory("java:/jms/OrdersCF") // optional; default factory if omitted
JMSContext context; // container-managed, scoped to the transaction
@Resource(lookup = "java:/jms/queue/orders")
Queue orders;
public void orderPlaced(Order order) {
context.createProducer()
.setProperty("orderId", order.getId().toString())
.setProperty("region", order.getRegion())
.setDeliveryMode(DeliveryMode.PERSISTENT)
.setTimeToLive(24 * 60 * 60 * 1000L)
.send(orders, toJson(order)); // send(Destination, String) creates a TextMessage
}
}
Le JMSContext injecté a une portée transactionnelle : quand la transaction JTA du session bean est validée, le message est délivré ; quand elle est annulée, il est abandonné. Si la même méthode écrit aussi en base via JPA, les deux opérations sont atomiques, à condition que la connection factory soit compatible XA et que le gestionnaire de transactions coordonne les deux ressources, ce qui fait l'objet du guide sur les transactions. Hors transaction, le contexte se comporte comme une session en auto-acknowledge et les messages partent immédiatement.
Pour le corps des messages, préférez un TextMessage contenant du JSON ou un BytesMessage avec un encodage documenté. ObjectMessage et la sérialisation Java lient producteur et consommateur aux mêmes versions de classes et ouvrent la porte aux attaques par désérialisation ; brokers et serveurs le bloquent de plus en plus par défaut. MapMessage convient pour des payloads petits et plats.
Comment les transactions pilotent l'acquittement
C'est le point qui piège les développeurs ayant appris JMS hors d'un conteneur. Dans un client autonome, vous choisissez un mode d'acquittement (auto, client ou dups-ok) ou une session transactionnelle, et vous appelez vous-même acknowledge() ou commit(). Dans un MDB avec des transactions gérées par le conteneur, le cas par défaut, rien de tout cela ne s'applique :
- Le conteneur reçoit le message et démarre une transaction JTA.
- Il enrôle la session JMS dans cette transaction et appelle
onMessage. - Tout travail JPA, tout autre envoi JMS ou appel JDBC effectué dans la méthode est enrôlé lui aussi.
- Si
onMessagese termine normalement, le conteneur valide : les modifications en base sont écrites, les messages sortants sont libérés et le message entrant est acquitté, le tout ensemble. - Si
onMessagelève une exception runtime ou appellemessageDrivenContext.setRollbackOnly(), tout est annulé et le broker redélivre le message, avecJMSRedeliveredà true etJMSXDeliveryCountincrémenté.
L'attribut transactionnel du MDB peut être REQUIRED (le défaut) ou NOT_SUPPORTED ; les autres n'ont aucun sens puisqu'il n'existe pas de transaction appelante à rejoindre. Avec @TransactionManagement(BEAN), le message est acquitté selon acknowledgeMode au retour de onMessage, quoi qu'ait fait votre UserTransaction : un MDB en gestion par le bean peut donc valider son travail en base et perdre malgré tout l'acquittement, ou acquitter un message dont le traitement a échoué. Restez sur les transactions gérées par le conteneur à moins d'avoir une raison bien précise.
Files, topics et abonnements
Une file délivre chaque message à un seul consommateur ; un topic délivre chaque message à tous les abonnés. Deux aspects de la consommation de topics sont propres au conteneur :
- Un abonnement durable survit au retrait du déploiement du MDB ou à l'arrêt du serveur : le broker conserve les messages jusqu'au retour de l'abonné. Il est identifié par
clientIdplussubscriptionName, et un abonnement durable de type JMS 1.1 ne peut avoir qu'un seul consommateur actif. Cela se heurte au pool de MDB, et c'est pourquoi JMS 2.0 a ajouté les abonnements partagés à l'API (createSharedDurableConsumer) ; qu'un pool de MDB en utilise un dépend d'une propriété d'activation propriétaire (l'adapter Artemis de WildFly l'appelleshareSubscriptions), et dans ce cas plusieurs instances, même réparties sur plusieurs nœuds d'un cluster, se partagent un même abonnement durable. - Un abonnement non durable à un topic perd les messages publiés pendant que le MDB ne tourne pas. C'est acceptable pour des diffusions d'invalidation de cache, pas pour quoi que ce soit de critique pour le métier.
Les sélecteurs de messages permettent à un consommateur de ne recevoir qu'une partie des messages d'une destination, selon leurs en-têtes et propriétés (region = 'EU' AND priority > 4). Ils sont évalués par le broker, si bien que les messages non retenus ne sont jamais envoyés au consommateur ; mais sur une file, ils ne sont pas non plus consommés par ce consommateur, et si aucun consommateur ne les sélectionne ils y restent indéfiniment. Utilisez librement les sélecteurs sur les topics, et sur les files seulement quand chaque message est assuré de trouver un consommateur correspondant.
Concurrence, pooling et ordre
Le conteneur exécute plusieurs instances de MDB en parallèle (le pool MDB par défaut de WildFly et le maxConcurrency d'Open Liberty sont des réglages typiques) et chaque instance reçoit ses messages indépendamment. C'est grâce à cette concurrence que les MDB passent à l'échelle, et elle a deux conséquences :
- L'ordre n'est pas préservé entre instances. Deux messages concernant la même commande peuvent être traités en parallèle et se terminer dans le mauvais ordre. Réduisez le pool à un pour un ordre strict, utilisez les message groups du broker (
JMSXGroupIDsur ActiveMQ Artemis) pour attacher une clé à un seul consommateur, ou rendez le traitement indépendant de l'ordre en transportant une version ou un horodatage dans le message. - Un
onMessagelent monopolise une place du pool. Un traitement long dans un MDB affame les autres messages ; si le traitement est lent, acquittez vite en confiant le travail à une file dédiée ou à un managed executor, dans les limites transactionnelles que vous pouvez accepter.
Du point de vue du conteneur, les instances sont sans état : comme les stateless session beans, elles sont mises en pool, et leurs champs ne doivent contenir que des ressources injectées.
Concevoir un consommateur robuste
Puisque la redélivraison existe, tout consommateur doit être idempotent : traiter deux fois le même message doit avoir le même effet que le traiter une fois. Techniques pratiques, de la moins coûteuse à la plus solide :
- Rendre l'opération naturellement idempotente : positionner un statut plutôt qu'incrémenter un compteur.
- Stocker le
JMSMessageID(ou une clé métier transportée en propriété) dans la même transaction base de données que l'effet, avec une contrainte d'unicité ; un doublon fait alors échouer l'insertion et vous validez sans refaire le travail. - Pour les effets de bord externes qui ne peuvent pas être annulés (envoi d'e-mail, appel à une API de paiement), enregistrer d'abord l'intention en base et exécuter l'effet de bord depuis un consommateur séparé qui vérifie cet enregistrement.
Validez le message avant de toucher la moindre ressource, et envoyez les messages qui ne pourront jamais être traités (schéma invalide, type inconnu) vers une file d'erreur en indiquant la raison dans une propriété, puis terminez normalement pour que le broker cesse de les redélivrer. Journalisez le JMSMessageID et l'identifiant de corrélation sur chaque ligne : quand un message est bloqué en redélivraison, c'est grâce à cet identifiant que vous le retrouvez dans la console du broker. Enfin, gardez le MDB léger (parsing plus un appel à un service bean) pour pouvoir tester la logique avec un simple test JUnit et le MDB une seule fois avec un broker embarqué.
Ce que Jakarta Messaging 3.1 change, et ce qu'il ne change pas
Messaging 3.0 correspondait au changement d'espace de noms (javax.jms vers jakarta.jms) ; la 3.1, intégrée à Jakarta EE 10, a relevé la version minimale de Java et apporté de petites clarifications, sans aucune nouvelle API. La génération suivante discutée depuis longtemps (listeners natifs CDI sur des beans ordinaires, API réactive) n'a pas vu le jour : le MDB reste donc le seul moyen standard et portable de consommer des messages dans un serveur Jakarta EE. Les éditeurs comblent le manque : WildFly et Open Liberty exposent les fonctionnalités d'Artemis et de leur propre moteur via des propriétés d'activation, et MicroProfile Reactive Messaging propose une alternative pilotée par annotations sur les runtimes qui la prennent en charge. Quand vous lisez du code utilisant javax.jms, @MessageDriven de javax.ejb et destinationLookup, c'est le même modèle ; seul le préfixe a changé.
Erreurs courantes
- Lever une exception sur un échec permanent. Le message tourne en redélivraison jusqu'à la limite du broker. Routez-le vers une file d'erreur et terminez normalement.
- Traitements non idempotents. Une redélivraison après un crash entre le commit et l'acquittement, rare mais bien réelle, traite le message deux fois. Dédupliquez sur l'identifiant du message ou une clé métier.
- Transactions gérées par le bean dans un MDB. L'acquittement et votre transaction sont découplés ; vous pouvez perdre des messages ou les traiter deux fois. Utilisez les transactions gérées par le conteneur.
- Sélecteurs sur des files sans consommateur fourre-tout. Les messages non sélectionnés s'accumulent en silence. Surveillez la profondeur de la file, ou utilisez un topic.
- Supposer un traitement ordonné. Le pool traite en parallèle. Concevez en conséquence ou contraignez le pool.
ObjectMessageavec des classes du domaine. Couplage de versions et risque de désérialisation. Utilisez du JSON dans unTextMessage.- Fermer un
JMSContextinjecté. Il appartient au conteneur ; le fermer lève une exception ou casse les appels suivants. Ne créez votre propre contexte à partir d'uneConnectionFactoryque dans du code qui s'exécute hors de la gestion du conteneur.
Points clés
- Un message-driven bean transforme une file ou un topic en appel de méthode : le conteneur gère la connexion, un pool d'instances et la transaction autour de chaque
onMessage. - Les propriétés d'activation (
destinationLookup,destinationType,messageSelector, réglages d'abonnement) sont standard ; taille du pool, limites de redélivraison et gestion des dead letters relèvent de la configuration du broker et du serveur. - Avec les transactions gérées par le conteneur, l'acquittement fait partie du commit : un retour normal acquitte, une exception annule et déclenche la redélivraison.
- Injectez
JMSContextpour envoyer ; il rejoint la transaction JTA englobante, de sorte que les écritures en base et les messages sortants sont validés ensemble. - Les abonnements durables conservent les messages d'un topic pendant l'absence du consommateur, les abonnements partagés permettent à un pool ou à un cluster de les consommer, et les sélecteurs filtrent au niveau du broker.
- Les consommateurs doivent être idempotents, valider tôt et dévier les messages impossibles à traiter au lieu de lever des exceptions indéfiniment.
- Messaging 3.1 n'a rien changé sur le plan fonctionnel ; le modèle MDB est le même qu'en Java EE 7, avec des packages
jakarta.*.
Sources et lectures complémentaires
Questions fréquentes
- Que devient un message quand onMessage lève une exception ?
- Avec les transactions gérées par le conteneur (le défaut), la transaction est annulée et le message n'est pas acquitté : le broker le redélivre donc. Si la cause est permanente (payload mal formé, enregistrement manquant), le même échec se répète jusqu'à ce que la limite de redélivraison du broker envoie le message dans une dead-letter queue. Validez tôt et routez vous-même les messages impossibles à traiter vers une destination d'erreur plutôt que de lever des exceptions en boucle.
- Peut-on appeler un message-driven bean directement, comme un session bean ?
- Non. Un MDB n'a pas de vue client ; seul le conteneur l'appelle, en réponse à un message. Si vous avez besoin de la même logique ailleurs, placez-la dans un session bean ou un bean CDI et faites-y déléguer le MDB, ce qui rend au passage cette logique testable unitairement sans broker.
- Les messages sont-ils consommés dans l'ordre ?
- Une file délivre dans l'ordre à un consommateur unique, mais le conteneur fait tourner un pool d'instances de MDB en parallèle : des messages arrivés dans l'ordre sont traités simultanément et peuvent se terminer dans le désordre. Si l'ordre compte, réduisez la taille du pool à un, utilisez les message groups (une fonctionnalité du broker) ou concevez le traitement pour que l'ordre n'ait pas d'importance.
- Jakarta EE inclut-il un broker de messages ?
- La plateforme exige un fournisseur JMS sans imposer lequel. GlassFish et Payara embarquent OpenMQ, WildFly embarque ActiveMQ Artemis, Open Liberty dispose d'un moteur de messagerie intégré, et tous les serveurs peuvent se connecter à un broker externe comme ActiveMQ Artemis, IBM MQ ou RabbitMQ (via un client JMS ou un resource adapter).