Contactez-nous

Envoi et réception de messages avec `JmsTemplate` et `@JmsListener`

Maîtrisez l'envoi et la réception de messages JMS dans Spring Boot en utilisant JmsTemplate pour produire et l'annotation @JmsListener pour consommer de manière asynchrone.

Introduction : Interagir avec JMS dans Spring

Une fois que vous avez configuré une connexion à un broker JMS (comme ActiveMQ ou Artemis) dans votre application Spring Boot, l'étape suivante consiste à envoyer et recevoir des messages. Spring JMS simplifie grandement ces interactions grâce à deux composants clés : `JmsTemplate` pour l'envoi (production) de messages et l'annotation `@JmsListener` pour la réception (consommation) asynchrone.

Ces outils abstraient une grande partie de la complexité de l'API JMS native (gestion des connexions, sessions, producteurs, consommateurs, transactions), vous permettant de vous concentrer sur la logique applicative liée aux messages. Ils s'intègrent parfaitement au modèle de programmation Spring, facilitant l'injection de dépendances et la configuration déclarative.

Nous allons explorer comment utiliser `JmsTemplate` et `@JmsListener` pour mettre en oeuvre des scénarios courants de communication via JMS dans vos applications Spring Boot.

Envoyer des messages avec `JmsTemplate`

Le `JmsTemplate` est la classe centrale de Spring JMS pour l'envoi de messages. Il gère la création et la libération des ressources JMS (connexions, sessions, producteurs) et offre des méthodes pratiques pour envoyer des messages vers une destination (queue ou topic).

Spring Boot auto-configure un bean `JmsTemplate` prêt à l'emploi dès qu'un broker JMS est détecté sur le classpath (via les starters `spring-boot-starter-activemq` ou `spring-boot-starter-artemis`) et qu'une connexion peut être établie (souvent vers un broker embarqué ou via des propriétés comme `spring.activemq.broker-url` ou `spring.artemis.broker-url`). Vous pouvez simplement l'injecter dans vos services ou composants.

La méthode la plus simple pour envoyer un message est `convertAndSend(String destinationName, Object message)`. Elle prend le nom de la destination (queue ou topic) et l'objet à envoyer. Spring utilise un `MessageConverter` (par défaut, `SimpleMessageConverter`) pour convertir votre objet Java en un `javax.jms.Message` approprié avant l'envoi.

Exemple d'un service qui envoie un message simple vers une queue nommée "mailboxQueue" :

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;

@Service
public class MessageSenderService {

    private final JmsTemplate jmsTemplate;

    @Autowired
    public MessageSenderService(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void sendMessage(String messageContent) {
        System.out.println("Envoi du message : " + messageContent);
        // Envoie le message vers la destination spécifiée.
        // Spring s'occupe de la conversion en TextMessage par défaut.
        jmsTemplate.convertAndSend("mailboxQueue", messageContent);
    }

    // Exemple d'envoi d'un objet (nécessite un convertisseur approprié, ex: Jackson)
    public void sendOrder(Order order) {
        System.out.println("Envoi de la commande : " + order.getId());
        // Si un MappingJackson2MessageConverter est configuré,
        // l'objet Order sera sérialisé en JSON dans le corps du message.
        jmsTemplate.convertAndSend("orderQueue", order);
    }

    // Envoi vers un Topic (nécessite d'activer le support pub/sub domain sur le template)
    public void publishEvent(String event) {
        // Configurez jmsTemplate.setPubSubDomain(true) ou via la propriété 
        // spring.jms.pub-sub-domain=true pour envoyer vers des topics
        System.out.println("Publication de l'événement : " + event);
        jmsTemplate.convertAndSend("eventTopic", event);
    }

     // Envoi avec personnalisation du message (ex: ajout d'en-têtes JMS)
     public void sendMessageWithHeaders(String messageContent, String correlationId) {
         System.out.println("Envoi du message avec CorrelationID : " + messageContent);
         jmsTemplate.convertAndSend("replyQueue", messageContent, message -> {
             message.setJMSCorrelationID(correlationId);
             message.setStringProperty("CustomHeader", "CustomValue");
             return message;
         });
     }
}

Pour l'envoi d'objets complexes, il est courant de configurer un `MappingJackson2MessageConverter` pour sérialiser/désérialiser les objets en JSON. Spring Boot peut souvent l'auto-configurer si Jackson est sur le classpath.

Pour envoyer vers des topics (modèle publish/subscribe) plutôt que des queues (point-à-point), vous devez généralement configurer le `JmsTemplate` pour utiliser le domaine pub/sub via `jmsTemplate.setPubSubDomain(true)` ou en définissant la propriété `spring.jms.pub-sub-domain=true` dans `application.properties`.

Recevoir des messages avec `@JmsListener`

Pour recevoir des messages de manière asynchrone, Spring JMS propose une approche basée sur les annotations, très simple et puissante : `@JmsListener`. Cette annotation peut être placée sur une méthode d'un bean Spring. Spring Boot se charge alors de créer un 'Message Listener Container' en arrière-plan qui écoute la destination spécifiée et invoque votre méthode annotée chaque fois qu'un message arrive.

L'annotation `@JmsListener` prend principalement l'attribut `destination`, qui indique le nom de la queue ou du topic à écouter.

Exemple d'un composant qui écoute la queue "mailboxQueue" :

import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.MessageHeaders;
import javax.jms.Message;

@Component
public class MessageReceiver {

    // Méthode invoquée pour chaque message reçu sur "mailboxQueue"
    @JmsListener(destination = "mailboxQueue")
    public void receiveMessage(String messageContent) {
        System.out.println("Message reçu de mailboxQueue: <" + messageContent + ">" );
        // Logique de traitement du message...
    }

    // Ecoute sur "orderQueue" et désérialise automatiquement l'objet Order
    // (Nécessite un MessageConverter approprié comme MappingJackson2MessageConverter)
    @JmsListener(destination = "orderQueue", containerFactory = "myFactory") // Utilise une factory spécifique si besoin
    public void receiveOrder(@Payload Order order,
                             @Header(name = "jms_correlationId", required = false) String correlationId,
                             MessageHeaders headers,
                             Message message /* Accès au message JMS brut */) {
        System.out.println("Commande reçue: ID=" + order.getId() + ", Produit=" + order.getProduct());
        System.out.println("Correlation ID: " + correlationId);
        System.out.println("Headers: " + headers);
        // Logique de traitement de la commande...
    }
    
    // Ecoute sur un Topic (nécessite un container factory configuré pour les topics)
    // Plusieurs instances de l'application peuvent recevoir le même message.
    @JmsListener(destination = "eventTopic", subscription = "my-app-subscriber", containerFactory = "topicListenerFactory")
    public void receiveEvent(String event) {
        System.out.println("Evénement reçu sur eventTopic: " + event);
        // Traitement de l'événement...
    }
}

Spring s'occupe de la conversion du `javax.jms.Message` reçu en type de paramètre de votre méthode (ici, `String` ou `Order`), en utilisant le `MessageConverter` configuré. Vous pouvez également accéder aux détails du message brut (`Message`) ou à ses en-têtes via les annotations `@Header` ou en injectant `MessageHeaders`.

Pour écouter des topics, vous devez souvent configurer un `JmsListenerContainerFactory` spécifique qui est activé pour le domaine pub/sub (`factory.setPubSubDomain(true)`). L'attribut `subscription` dans `@JmsListener` permet de créer un abonnement durable (si supporté et configuré) ou d'identifier un consommateur spécifique sur un topic partagé.

La configuration des 'listeners containers' peut être personnalisée via un bean `JmsListenerContainerFactory`. Cela permet de régler des paramètres comme la concurrence (nombre de threads consommateurs), la gestion des transactions, les stratégies d'acquittement des messages (acknowledgement), etc.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import javax.jms.ConnectionFactory;

@Configuration
public class JmsConfig {

    // Factory par défaut pour les queues
    @Bean
    public JmsListenerContainerFactory myFactory(ConnectionFactory connectionFactory,
                                                    DefaultJmsListenerContainerFactoryConfigurer configurer) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        // Personnalisations: ex, transactions
        // factory.setSessionTransacted(true);
        return factory;
    }
    
    // Factory spécifique pour les topics
    @Bean
    public JmsListenerContainerFactory topicListenerFactory(ConnectionFactory connectionFactory,
                                                              DefaultJmsListenerContainerFactoryConfigurer configurer) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        factory.setPubSubDomain(true); // !! Important pour les topics
        // factory.setSubscriptionDurable(true); // Si abonnements durables requis
        // factory.setClientId("my-unique-client-id"); // Nécessaire pour durable
        return factory;
    }
}

Gestion des erreurs et transactions

Par défaut, si une exception non interceptée se produit pendant l'exécution d'une méthode `@JmsListener`, le message ne sera pas acquitté (acknowledged). Selon la configuration du broker et du listener container, cela peut entraîner une nouvelle livraison (redelivery) du message. Il est important de gérer les erreurs de manière appropriée, soit en les capturant dans la méthode, soit en configurant une politique de redelivery et éventuellement une 'Dead Letter Queue' (DLQ) où les messages échouant de manière répétée peuvent être placés pour analyse.

Spring JMS s'intègre également avec le support transactionnel de Spring. Si une méthode `@JmsListener` est annotée avec `@Transactional` (ou si le `JmsListenerContainerFactory` est configuré avec `setSessionTransacted(true)`), la réception du message et le traitement effectué dans la méthode feront partie d'une transaction JMS (ou une transaction globale si JTA est configuré). Si la transaction échoue (rollback), le message ne sera pas acquitté et sera potentiellement redélivré. Si la transaction réussit (commit), le message sera acquitté.

En utilisant `JmsTemplate` et `@JmsListener`, vous disposez d'un moyen puissant et cohérent avec le modèle Spring pour intégrer la messagerie JMS dans vos applications, facilitant la communication asynchrone et la construction de systèmes découplés et résilients.