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
destinationLookupNom JNDI de la file ou du topic
destinationTypejakarta.jms.Queue ou jakarta.jms.Topic
connectionFactoryLookupNom JNDI de la connection factory ; sinon, celle par défaut du serveur
messageSelectorFiltre de style SQL-92 sur les en-têtes et propriétés, évalué par le broker
acknowledgeModeAuto-acknowledge ou Dups-ok-acknowledge ; pertinent uniquement avec des transactions gérées par le bean
subscriptionDurabilityDurable ou NonDurable pour les topics
subscriptionName, clientIdIdentifient 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 :

  1. Le conteneur reçoit le message et démarre une transaction JTA.
  2. Il enrôle la session JMS dans cette transaction et appelle onMessage.
  3. Tout travail JPA, tout autre envoi JMS ou appel JDBC effectué dans la méthode est enrôlé lui aussi.
  4. Si onMessage se 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.
  5. Si onMessage lève une exception runtime ou appelle messageDrivenContext.setRollbackOnly(), tout est annulé et le broker redélivre le message, avec JMSRedelivered à true et JMSXDeliveryCount incré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 clientId plus subscriptionName, 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'appelle shareSubscriptions), 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 (JMSXGroupID sur 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 onMessage lent 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 :

  1. Rendre l'opération naturellement idempotente : positionner un statut plutôt qu'incrémenter un compteur.
  2. 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.
  3. 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.
  • ObjectMessage avec des classes du domaine. Couplage de versions et risque de désérialisation. Utilisez du JSON dans un TextMessage.
  • Fermer un JMSContext injecté. Il appartient au conteneur ; le fermer lève une exception ou casse les appels suivants. Ne créez votre propre contexte à partir d'une ConnectionFactory que 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 JMSContext pour 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).
Enterprise Beans 4 : session beans stateless, stateful et singletonCe qu'apportent les session beans EJB (pooling, transactions, sécurité, timers, appels asynchrones), leurs différences et quand préférer un bean CDI.Intermédiaire10 min de lectureJakarta Transactions (JTA 2) : transactions gérées par le conteneur, propagation et XALe fonctionnement des transactions JTA dans un serveur Jakarta EE : isolation, propagation @Transactional, règles de rollback, UserTransaction et commit XA.Intermédiaire11 min de lectureMessagerieL'API JMS (Jakarta Messaging 3.1) : sessions, messages, acquittement et sélecteursEnvoyer et recevoir des messages avec jakarta.jms : JMSContext, types et en-têtes, modes d'acquittement, sélecteurs, abonnements durables et request-reply.Intermédiaire10 min de lectureMessagerieMiddleware orienté messages : files, topics et communication asynchroneRôle d'un broker de messages, découplage producteurs/consommateurs par files et topics, protocoles existants, et quand préférer la messagerie à un appel HTTP.Débutant12 min de lectureMessageriePatterns de messagerie et garanties de livraison : idempotence, outbox, retries, ordreAt-most-once, at-least-once et exactly-once expliqués, et les patterns qui fiabilisent la messagerie : consommateur idempotent, outbox, retries et ordre.Avancé14 min de lectureMessagerieActiveMQ Classic 6 : installation, activemq.xml et configuration des transportsInstaller ActiveMQ Classic 6 sous Linux ou Docker, lire activemq.xml, configurer transports, limites mémoire, politiques, failover et réseaux de brokers.Intermédiaire11 min de lecture