Chapter 14. JMS

14.1. Introduction

Spring fournit un framework qui permet de s'abstraire de JMS et simplifie l'utilisation de cette API. Il permet également de masquer à l'utilisateur les différences entre les APIs JMS en version 1.0.2 et 1.1.

Spring peut être divisé en deux types de fonctionnalités, production et consommation de messages. En J2EE, la possibilité de consommer des messages de manière asynchrone est réalisée grâce aux EJBs "Message-Driven" (MDB) alors que, dans une application autonome, cette fonctionnalité est permise par la création de MessageListeners ou de ConnectionConsumers.

Le package org.springframework.jms.core fournit les fonctionnalités de base pour utiliser JMS. Il comprend la classe template JMS qui simplifie l'utilisation de JMS en se chargeant de la création et de la libération des ressources de la même manière que la classe JdbcTemplate le réalise pour JDBC. Le point commun dans la conception des classes de template de Spring est de fournir des méthodes facilitant l'exécution des opérations communes...

Le package org.springframework.jms.support fournit la fonctionnalité de translation des exceptions JMSException. Celui-ci convertit la hiérarchie des exceptions checkées dans une hiérarchie correspondante d'exceptions non checkées. Si un fournisseur a des sous exceptions spécifiques de la classe javax.jms.JMSException, celles-ci seront encapsulées dans l'exception non checkée UncategorizedJmsException. Le package org.springframework.jms.support.converter fournit une abstraction MessageConverter pour convertir des objets java en messages JMS. Le package org.springframework.jms.support.destination fournit différentes stratégies pour gérer les destinations JMS en fournissant, par exemple, un service de localisation des destinations stockées dans JNDI.

Pour finir, le package org.springframework.jms.connection fournit une implémentation de l'interface ConnectionFactory utilisable dans les applications autonomes. Il contient également une implémentation de l'interface de Spring, PlatformTransactionManager, pour JMS. Cela permet l'intégration de JMS en tant que ressource transactionnelle dans les mécanismes de gestions des transactions de Spring.

14.2. Unification des domaines

La spécification JMS a deux versions majeures: 1.0.2 et 1.1. La version 1.0.2 définit deux domaines de messages, point à point (queue) et publication/souscription (sujet). L'API en version 1.0.2 reflète ces deux aspects de messages en fournissant des classes similaires pour chaque domaine. De ce fait, une application cliente est spécifique à un domaine lors de l'utilisation de l'API JMS. JMS en version 1.1 introduit une unification des domaines qui minimisent aussi bien les différences fonctionnelles que les différences au niveau de l'API cliente JMS entre les deux domaines. Pour illustrer ceci, si vous utilisez un fournisseur supportant la version 1.1 de JMS, vous pouvez dans une même transaction récupérer un message d'un domaine et en produire un autre dans un autre domaine tout en utilisant la même Session

La version 1.1 de la spécification JMS a été publié en avril 2002 et ajoutée à J2EE en novembre 2003. De ce fait, beaucoup de serveurs d'application communément utilisés ne supportent que la version 1.0.2 de JMS.

14.3. JmsTemplate

Deux implémentations de JmsTemplate sont fournis. La classe JmsTemplate utilise l'API JMS version 1.1 et la sous classe JmsTemplate102 l'utilise en version 1.0.2.

Le code utilisant JmsTemplate doit seulement implémenter des interfaces de rappel qui ont des contrats clairement définis. L'interface MessageCreator crée un message à partir d'une Session fourni par le code appelant de JmsTemplate. Afin de permettre une utilisation plus complexe de l'API JMS, l'interface SessionCallback fournit à l'utilisateur une Session JMS et l'interface ProducerCallback, une Session et un MessageProducer.

L'API JMS fournit deux types de méthodes d'envoi, une qui prend des paramètres de qualité de service tels que le mode de livraison la priorité et la durée de vie (QOS) et une qui n'en prend aucun et se base sur les valeurs par défaut. Puisque la classe JmsTemplate fournit plusieurs méthodes d'envoi, la configuration des paramètres de qualité de service a été permise par le biais des propriétés de beans pour éviter les duplications. De la même manière, la valeur de délai d'expiration (timeout) pour les appels synchrones est positionné avec la propriété setReceiveTimeout.

Certains fournisseurs JMS permettent la configuration des valeurs par défaut de la qualité de service administrativement via la configuration de la ConnectionFactory. Cela implique que l'appel de la méthode send(Destination destination, Message message) de la classe MessageProducer utilisera des valeurs par défaut différentes de celles de la spécification JMS. De plus, pour garantir une gestion conforme des valeurs de qualité de service, la classe JmsTemplate doit être spécifiquement activé pour pouvoir utiliser ses propres valeurs en spécifiant à vrai (true) la valeur de la propriété isExplicitQosEnabled.

14.3.1. ConnectionFactory

L'utilisation de JmsTemplate nécessite une référence à une ConnectionFactory. L'interface ConnectionFactory fait partie intégrante de la spécification JMS. Elle est utilisée par l'application client comme une fabrique pour créer des connexions avec le fournisseur JMS et encapsuler les différents paramètres de configuration, dont beaucoup sont spécifiques à celui-ci comme ceux de la configuration de SSL.

Pour utiliser JMS dans un EJB, le vendeur fournit des implémentations pour les interfaces JMS et, de ce fait, celles-ci peuvent participer dans une gestion transactionnelle déclarative et réaliser un pooling de connexions et de sessions. Pour utiliser cette implémentation, les conteneurs J2EE imposent typiquement la déclaration de la fabrique de connexion JMS en tant que resource-ref dans les descripteurs de déploiment des EJBs et servlets. Pour utiliser cette fonctionnalité avec JmsTemplate dans un EJB, l'application client devrait vérifier qu'il référence bien une implémentation managée de la ConnectionFactory.

Spring fournit une implémentation de l'interface ConnectionFactory, SingleConnectionFactory, qui retourne toujours la même connexion sur tous les appels à createConnection et ignore ceux sur close.. Cela est très utile pour tester et également dans les environnements autonomes, puisque cette même connexion peut être partagée par différents appels à JmsTemplate.

14.3.2. Gestion des transactions

Spring fournit une classe JmsTransactionManager qui gère les transactions pour une unique ConnectionFactory JMS. Cela permet aux applications JMS d'utiliser les possibilités de Spring en matière de gestion des transactions comme décrit dans le chapitre 7. La classe JmsTransactionManager associe la paire Connection/Session d'une ConnectionFactory à un thread. Cependant, dans un environnement J2EE, les ConnectionFactory peuvent gérer des pools de connexions et de sessions, et de ce fait, les instances associées au thread dépendent du comportement du pool. Dans un environnement autonome, utiliser SingleConnectionFactory entrainera l'utilisation d'une unique connexion JMS et chaque transaction aura sa propre session. La classe JmsTemplate peut également être utilisée avec la classe JtaTransactionManager et une ConnectionFactory JMS supportant le protocole XA pour effectuer des transactions globales.

Réutiliser du code sur des environnements autonomes et non autonomes, peut être source de confusion lors de la création d'une Session à partir d'une Connection. Cela vient du fait que l'API JMS a une fabrique unique pour créer une Session et elle nécessite des paramètres pour les modes transactionnel et d'acquittement. Dans un environnement non autonome, positionner ces valeurs... Quand la classe JmsTemplate est utilisée dans un environnement non autonome, il est possible de spécifier ces valeurs par le biais de l'utilisation des propriétés SessionTransacted et SessionAcknowledgeMode. Quand est utilisé une implémentation de PlatformTransactionManager avec la classe JmsTemplate, le template recevra toujours une session JMS transactionnelle.

14.3.3. Gestion des destinations

Les destinations, comme les ConnectionFactory, sont des objets JMS administrés qui peuvent être stockés et récupérés dans JNDI. Au moment de configurer un contexte d'application dans Spring, il est possible d'utiliser la classe de fabrique JndiObjectFactoryBean pour réaliser de l'injection de dépendance sur les références d'objets avec des destinations JMS. Cependant, cette façon de faire est déconseillée s'il y a un grand nombre de destinations dans l'application ou s'il y a des fonctionnalités avancées spécifiques au fournisseur JMS pour la gestion des destinations. Un exemple de gestion de ce type serait la création de destinations dynamiques ou le support d'espaces de nommage hiérarchique pour les destinations. La classe JmsTemplate délègue la résolution du nom de la destination d'un objet JMS à une implémentation de l'interface DestinationResolver. DynamicDestinationResolver est l'implémentation par défaut utilisée par la classe JmsTemplate et qui permet de résoudre les destinations dynamiques. Une classe JndiDestinationResolver est également fournie pour jouer le rôle de service de localisation des destinations contenues dans JNDI et peut également rebasculer sur le comportement de DynamicDestinationResolver.

Très souvent, les destinations utilisées dans une application JMS ne sont connues uniquement qu'au moment de l'exécution et, de ce fait, ne peuvent pas être créées administrativement quand l'application est déployée. Ceci est dû au fait que la logique applicative est partagée par les composants intéragissant dans le système qui créent des destinations au moment de l'exécution en suivant une converions de nommage bien définie et connue. Même si la création dynamique de destinations ne fait pas partie de la spécification JMS, la plupart des fournisseurs offrent cette fonctionnalité. Les destinations dynamiques sont créées avec avec un nom défini par l'utilisateur, ce qui les différencient des destinations temporaires et ne sont parfois pas mises dans JNDI. L'API utilisée pour les créer varie d'un fournisseur à l'autre puisque les propriétés associées lui sont spécifiques. Cependant un choix simple d'implémentation qui est souvent fait par les fournisseurs, est de négliger les mises en garde de la spécification JMS et d'utiliser la méthode createTopic(String topicName) de l'interface TopicSession ou la méthode createQueue(String queueName) de l'interface QueueSession pour créer une nouvelle destination avec les propriétés par défaut des destinations. Suivant l'implémentation du vendeur, DynamicDestinationResolver peut aussi créer une destination physique ay lieu de seulement la résoudre.

La propriété bouléenne PubSubDomain est utilisée pour configurer la classe JmsTemplate en ayant la connaissance du domaine. Par défaut, la valeur de cette propriété est fausse (false) indiquant ainsi que le domaine point à point sera utilisé. Dans l'implémentation relative à la version 1.0.2, la valeur de cette propriété détermine si les opérations d'envoi de JmsTemplate enverront un message à une queue ou une matière. Ce champ n'a aucun effet sur les opérations d'envoi pour l'implémentation relative à la version 1.1. Cependant, dans les deux implémentations, cette propriété détermine le comportement pour la résolution dynamique des destinations via les implémentations de la méthode createTopic(String topicName) de l'interface DestinationResolver.

Il est possible de configurer également la classe JmsTemplate avec une destination par défaut au moyen de la propriété DefaultDestination. Celle-ci sera utilisée avec les opérations d'envoi et de réception qui ne se réferre par à de destinations spécifiques.

14.4. Utilisation de JmsTemplate

Pour débuter avec JmsTemplate, il faut choisir l'implémentation JmsTemplate102 pour la version 1.0.2 ou JmsTemplate pour la version 1.1. Interroger votre fournisseur JMS pour déterminer quelle version est supportée.

14.4.1. Envoi d'un message

La classe JmsTemplate contient beaucoup de méthodes intéressantes pour envoyer un message. Il y a des méthodes d'envoi qui permettent de spécifier la destination en utilisant un objet javax.jms.Destination et celles qui la spécifient en utilisant une chaîne de caractères pour effecture une recherche JNDI. La méthode d'envoi qui ne prend aucune destination utilise celle par défaut. Voici un exemple qui envoie un message à une queue en utilisant l'implémentation relative à la version 1.0.2 de JMS.

import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.Session;

import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.JmsTemplate102;
import org.springframework.jms.core.MessageCreator;

public class JmsQueueSender {

  private JmsTemplate jt;

  private ConnectionFactory connFactory;

  private Queue queue;

  public void simpleSend() {
    jt = new JmsTemplate102(connFactory, false);
    jt.send(queue, new MessageCreator() {
      public Message createMessage(Session session) throws JMSException {
        return session.createTextMessage("hello queue world");
      }
    });
  }

  public void setConnectionFactory(ConnectionFactory cf) {
      connFactory = cf;
  }

  public void setQueue(Queue q) {
      queue = q;
  }

}
      

Cette exemple utilise l'interface de rappel MessageCreator pour créer un message de type texte à partir d'une session fournie. JmsTemplate est construit avec une référence à une ConnectionFactory et un bouléen spécifiant le domaine de messagerie. Un constructeur sans argument et une méthode setConnectionFactory/Queue est aussi fournie et peut être utilisée égalemet pour construire un instance en utilisant un BeanFactory. La méthode simpleSend modifiée pour envoyer sur une matière plutôt que sur une queue est montrée ci-dessous:

public void simpleSend() {
  jt = new JmsTemplate102(connFactory, true);
  jt.send(topic, new MessageCreator() {
     public Message createMessage(Session session) throws JMSException {
        return session.createTextMessage("hello topic world");
     }
  });
}     
      

Au moment de configurer le version 1.0.2 dans une contexte d'application, il est important de ne pas oublier de positionner la propriété bouléenne PubSubDomain pour indiquer quelle destination utiliser, queues ou matière.

La méthode send(String destinationName, MessageCreator c) permet d'envoyer un message en utilisant le nom d'une destination. Si ces noms existent dans JNDI, la propriété du template DestinationResolver doit être positionnée avec une instance de JndiDestinationResolver.

Si vous avez créé une classe JmsTemplate et lui avez spécifié un destination par défaut, la méthode send(MessageCreator c) envoie un message à celle-ci.

14.4.2. Réception synchrone

Bien que JMS soit typiquemebt associé avec de la messagerie asynchrone, il est possible de consommer les messages de manière synchrone. Les méthodes surchargées receive offre cette fonctionnalité. Durant la reception synchrone, le thread réalisant l'appel se bloque jusqu'à ce qu'un message devienne disponible. Cela peut s'avérer être une opération dangereuse ce thread peut potentiellement être bloqué indéfiniment. La propriété receiveTimeout spécifie la durée au cours de laquelle le receveur doit attendre avant de mettre de continuer.

14.4.3. Utilisation des Message Converters

Afin de faciliter l'envoi d'objets de domaine, la classe JmsTemplate a différentes méthodes qui prennent des objets Java en arguments pour les données du message. Les méthodes surchargées convertAndSend et receiveAndConvert de la classe JmsTemplate délègue le traitement de conversion à une instance de l'interface MessageConverter. Cette interface définit un contrat simple pour convertir les objets Java en messages JMS (et inversement). L'implémentation par défaut, SimpleMessageConverter, supporte les conversions entre String et TextMessage, byte[] et BytesMesssage, et java.util.Map et MapMessage. En utilisant le convertisseur, le code de l'application peut se focaliser sur l'objet métier qui va être envoyé ou reçu via JMS et ne plus se soucier des détails de sa représentation en tant que message JMS.

La "sandbox" (bac à sable) contient actuellement une classe MapMessageConverter qui utilise la réflexion pour convertir un JavaBean en MapMessage (et réciproquement). D'autres choix populaires d'implémentations sont les convertisseurs tels que des packages existants de sérialisation XML comme JAXB, Castor, XMLBeans, ou XStream, pour créer un TextMessage représentant l'objet.

Pour faciliter le positionnement des propriétés, entêtes et corps du message qui ne peuvent pas être encapsulés de manière générique dans une classe de conversion, l'interface MessagePostProcessor donne accès au message après qu'il ait été converti mais qu'il ne soit envoyé. L'exemple ci-dessous montre comment modifier une propriété et une entête de message après qu'il ait été converti à partir d'une java.util.Map.

public void sendWithConversion() {
    Map m = new HashMap();
    m.put("Name", "Mark");
    m.put("Age", new Integer(35));
    jt.convertAndSend("testQueue", m, new MessagePostProcessor() {

         public Message postProcessMessage(Message message)
            throws JMSException {
            message.setIntProperty("AccountID", 1234);
            message.setJMSCorrelationID("123-00001");

            return message;
        }
    });
}
      

Le message résultat sera de la forme

MapMessage={ 
  Header={ 
    ... standard headers ...
    CorrelationID={123-00001} 
  } 
  Properties={ 
    AccountID={Integer:1234}
  } 
  Fields={ 
    Name={String:Mark} 
    Age={Integer:35} 
  } 
}
      

14.4.4. SessionCallback et ProducerCallback

Bien que les opérations d'envoi couvre beaucoup de scénarios d'utilisaition, il y a des cas où de multiples opérations doivent être exécutées sur une session JMS ou un MessageProducer. Les interfaces SessionCallback et ProducerCallback exposent respectivement la session JMS et la paire Session/MessageProducer Les méthodes execute() de JmsTemplate exécutent ces méthodes de rappel.