Traitements asynchrones - EJB Messages

Chapitres traités   

Dans cette étude, nous nous intéresserons à la communication entre les applications clientes avec un système asynchrone de type JMS. JMS est un service proposé par Java EE qui intègre une messagerie et qui permet de stocker des messages par une application cliente. Ces messages sont ensuites délivrés à une autre application cliente qui les attend éventuellement (gestion événementielle). Toutefois, si cette dernière n'est pas encore active, les messages sont concervés par la messagerie.

Les messages peuvent aussi être dédiés au serveur lui-même. Nous verrons alors qu'il existe un bean spécialisé dans la réception de message issu du service JMS, il s'agit du bean Message Driven Bean.

Nous en profiterons également pour voir comment envoyer ou recevoir du courrier électronique. Et pour finir, nous verrons comment mettre en oeuvre le service de Timer intégré dans Java EE.

Choix du chapitre Introduction

Au sein d'une application d'entreprise de grande ampleur, il peut être intéressant de faire communiquer entre elles les différentes sous-applications clientes et serveurs. Par communication, il faut comprendre un envoi de données directement interprétables et utilisables par les autres applications.

Ce sont les beans messages qui permettent de traiter les messages venant d'autres applications. Avant de connaître ce type de bean, dans un premier temps, nous présenterons l'API JMS, qui permet la connexion avec un système de messagerie inter-applications.

Le concept de bean message (MDB - Message Driven Bean) a été introduit avec les EJB 2.0 afin de traiter les messages venant d'un fournisseur JMS (Java Message Service). En réalité, avec EJB 3.0, les MDB supportent maintenant n'importe quel système de messagerie et sont donc indépendants de JMS et simplifie grandement le développement de ces composants.

Toutefois, tous les serveurs d'applications compatibles EJB 3.0 doivent supporter JMS. Beaucoup d'éditeurs fournissent leur propre implémentation JMS. dans tous les cas, un fournisseur JMS est inévitable pour utiliser les MDB.

Choix du chapitre Java Message Service - concepts

JMS, ou Java Message Service, est une API d'échanges de messages pour permettre un dialogue entre applications via un fournisseur (Provider) de messages. L'application cliente envoie un message dans une liste d'attente (plutôt qu'à une autre application directement, ce qui permet de faire un bon découplage logiciel), sans se soucier de la disponibilité de cette application (chaque système possède son propre cycle de vie). Le client a, de la part du fournisseur de messages, une garantie de qualité de service (certitude de remise au destinataire, delai de remise, etc.).

JMS est l'API utilisée pour l'accès à un système de messagerie d'entreprise. Ce système permet donc l'échange de messages entre différentes applications distantes. Les serveurs de messageries sont souvent méconnus (contrairement aux serveurs de base de données), bien qu'ils soient nombreux. On retrouve :

  1. MQSeries d'IBM,
  2. JBoss Messaging,
  3. One Message Queue de Sun Microsystem, ...

Les applications utilisant JMS sont indépendantes du type de serveur auquel elles se connectent du moment qu'elles utilisent l'API JMS.
.

Les applications utilisent généralement JMS dans les architectures de type B2B (Business to Business). En effet, cette API permet d'interconnecter n'importe quel système utilisant le principe de messagerie où l'envoi et la réception de message sont asynchrones. Cela signifie que les applications communiquant via JMS peuvent ne pas s'exécuter en même temps, sur le même principe que le système de courrier électronique (email). Lorsque l'expéditeur envoie une requête (via email), il ne reçoit pas directement la réponse. Il se peut qu'il ne reçoive jamais de réponse, ou seulement un accusé de réception.

Architecture JMS

L'architecture JMS est composée de différents éléments :

  1. Un fournisseur : (Provider) : c'est l'élément qui a la charge de la livraison des messages entre les différents intervenants. Il s'occupe de traiter les envois et de faire en sorte qu'ils soient bien reçus. Il s'agit d'un service qui implémente l'API JMS pour échanger les messages entre deux clients.
  2. Un client : c'est une application ou un composant d'application intervenant lors des échanges. Il envoie ou reçoit les messages. Il s'agit d'une classe Java qui utilise JMS pour émettre et/ou recevoir des messages. Un client envoie un message vers une file d'attente, et le client destinataire reste à l'écoute d'une file d'attente pour recevoir le message. Le transfert du message et sa persistance sont assurés par le fournisseur.
  3. Un message : c'est, comme son nom l'indique, l'élément qui va transiter via une communication entre les clients. Un fournisseur sert toujours d'intermédiaire ; nous ne les envoyons donc pas directement d'un client à un autre. Un message est un ensemble de données échangées de manière asynchrone entre les composants. Il existe plusieurs types de messages (texte, objet, binaire, etc.).
  4. Les destinations : ce sont des objets configurés au niveau du fournisseur qui sont à disposition des clients et qui seront utilisés par ces derniers pour l'envoi et la réception des messages. Pour schématiser, nous pouvons dire qu'il s'agit de boîtes à lettres dans lesquelles sont placées les messages en attendant qu'un client vienne les réclamer. Ce sont des ressources à rechercher dans l'annuaire JNDI du fournisseur.

Un point important dans cette architecture est qu'elle permet une communication faiblement couplée entre les clients : un client ne se préoccupe pas de l'identité de son ou de ses correspondants ni de leur éventuel état. De plus ce système peut travailler en environnement hétérogène (application C++, Java ...).

Modèle de messagerie

JMS offre deux modèles de messagerie point à point et publication/abonnement ou (publication/souscription) :

  1. Le mode point à point utilise les files d'attente (javax.jms.Queue) pour communiquer. Ce mode (un émetteur, un récepteur) s'apparente à l'envoi d'un e-mail.
  2. Le mode publication/abonnement utilise des sujets (javax.jsm.Topic) pour échanger des messages. Ce mode (un émetteur, multiples récepteurs) correspond, par exemple, à une souscription auprès d'un serveur de news. Par défaut, seuls les récepteurs connectés au sujet (Topic) sont alertés de l'arrivée du message. Pour que les messages soient concervés pour les récepteurs déconnectés, ils doivent être déclarés comme durable.

Nous pouvons considerer ces deux modes de la façon suivante : le modèle point à point représente une relation "One To One" entre un message et un destinataire alors que le modèle publication/souscription est représentée par une relation "One To Many".

Chaque mode utilise une interface différente pour envoyer des messages : javax.jms.QueueSender dans le mode point à point, et javax.jms.TopicPublisher pour le mode publication/abonnement. Tous deux héritent de la super interface javax.jms.MessageProducer.

Le mode point à point

Le mode point à point repose sur le concept de files d'attente (Queue). Cela signifie que chaque message est envoyé par un producteur dans une file d'attente, et est reçu par un seul consommateur. Une fois le message consommé, il disparaît de la file d'attente. Dans ce principe, les messages sont envoyés et empilés au fur et à mesure. Lorsque l'application cliente consommatrice est libre, elle reçoit ainsi l'ensemble des messages empilés.

Tant qu'un message n'est pas consommé, ou qu'il n'a pas expiré, il reste stocké au sein du fournisseur. Dès que le client devient actif, il peut alors consulter le message qui lui était destiné, et ceci sans aucun problème. Ceci peut se faire à tout moment. C'est vraiment le même principe que la messagerie.

Le mode publication/abonnement

Dans ce modèle, un producteur peut envoyer un message à plusieurs consommateurs par le biais d'un sujet (topic). Chaque consommateur doit cependant être préalablement inscrit à ce sujet sinon il ne reçoit rien. Dans ce mode, l'émetteur du message ne connait pas les destinataires qui se sont abonnés.

Contrairement au mode point à point, dans un mode publication/abonnement un message envoyé va être donc reçu par plusieurs clients. Le message ne disparait du Topic que lorsque tous les abonnés l'ont lu et acquitté.

Il existe deux types de souscription : temporaire et durable. Dans le premier cas, les consommateurs reçoivent les messages tant qu'ils sont connectés au sujet. Dans le cas d'une souscription durable, on oblige le fournisseur à enregistrer les messages lors d'une déconnexion, et à les envoyer lors de la nouvelle connexion du consommateur.

Mise en place de tous les composants JMS afin d'établir la communication par messages asynchrones

Il existe un certain nombre de composants qui s'occupe de la gestion globale de JMS, et de permettre ainsi une communication asynchrone entre applications clientes.

ConnectionFactory et Destination

Pour travailler avec JMS, la première étape consiste d'abord à se connecter au fournisseur JMS. Pour cela, nous devons récupérer un objet ConnectionFactory via JNDI qui rend ainsi la connexion possible avec le fournisseur. Cet objet peut être assimilé à une DataSource (en JDBC). En effet, de la même façon qu'un DataSource fournit une connexion JDBC, une ConnectionFactory fournit une connexion JMS au service de routage de message. L'autre élément à récupérer est la destination. Les destinations (Destination) sont des objets qui véhiculent les messages. JMS comporte deux types de destination, comme nous venons de le découvrir, les Queue et les Topic.

Voici l'écriture à proposer côté application cliente (plate-forme indépendante Java SE) :

Context ctx = new InitialContext();
ConnectionFactory fabrique = (ConnectionFactory)ctx.lookup("ConnectionFactory");
Destination destination = (Destination)ctx.lookup("queue/MaFile");

Voici une autre écriture où nous passons par un bean session qui nous permer d'utiliser l'injection. Il suffit alors de spécifier l'annotation @Resource :

@Resource(mappedName="ConnectionFactory")
private ConnectionFactory fabrique;
@Resource(mappedName="queue/MaFile")
private Destination destination;

Pour obtenir une ConnectionFactory, une Queue, ou un Topic, il faut les rechercher par leur nom dans l'annuaire JNDI ou utiliser l'injection. Cela suppose donc que ces ressources soient préalablement mis en oeuvre et qu'elles soient recensées au travers du service d'annuaire JNDI.

Création du contexte JNDI

Les propriétés pour la création du contexte JNDI sont dépendantes du fournisseur utilisé, et à ce titre, nous devons utiliser la même démarche que pour la communication par les beans session. Il est donc nécessaire d'initialiser ce contexte avec tous les bons paramètres requis. Le plus facile, à mon avis (puisque portable), est de placer ces différents paramètres, dans un fichier de configuration dont le nom est bien précis (jndi.properties) et qui doit être placé dans le répertoire racine du projet.

Voici les paramètres correspondant au serveur d'application Glassfish :

jndi.properties (Glassfish)
# Accès au serveur d'application Glassfish
java.naming.factory.initial=com.sun.enterprise.naming.SerialInitContextFactory
java.naming.factory.url.pkgs=com.sun.enterprise.naming
java.naming.factory.state=com.sun.corba.ee.impl.presentation.rmi.JNDIStateFactoryImpl
org.omg.CORBA.ORBInitialHost=portable
org.omg.CORBA.ORBInitialPort=3700

Dans le cas où vous devez utiliser le serveur d'application JBoss, voici le fichier de configuration que vous devez mettre en place :

jndi.properties (JBoss)
# Accès au serveur d'application JBoss
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces
java.naming.provider.url=portable:1099

Archives à déployer sur chaque poste cliente

Pour que la communication puisse s'établir correctement avec le serveur d'application, notamment dans le service JMS, vous devez impérativement récupérer les archives suivantes pour toutes les applications qui seront à déployer sur chaque poste client. Voici un exemple d'archives pour le serveur d'application Glassfish :

Archives supplémentaires à déployer avec l'application cliente pour le serveur d'application Glassfish
# Archives à installer
appserv-rt.jar
javaee.jar
appserv-deployment-client.jar
appserv-ext.jar

# Archives supplémentaires pour JMS
imqjmsra.jar
appserv-admin.jar
appserv-ws.jar

Connection et Session

L'objet ConnectionFactory permet de créer une connexion avec le fournisseur JMS. Une fois la connexion créée, elle est ensuite utilisée pour créer une session. La session sert à regrouper toutes les opérations d'envoi et de réception des messages. Dans la majorité des cas, une session unique est suffisante. La création de plusieurs sessions est utile seulement dans le cas d'applications multi-tâches qui produisent et reçoivent des messages en même temps. Effectivement, l'objet Session est mono-tâche, c'est-à-dire que ses méthodes n'autorisent pas l'accès concurrent. Généralement, le thread qui crée l'objet Session utilise le producteur et le consommateur de cette session.

Connection connexion = fabrique.createConnection();
Session session = connexion.createSession(true, 0); // createSession(transaction, accuséRéception);

La méthode createSession() prend deux paramètres. Une session, je le rappelle, est un contexte transactionnel utilisé pour grouper un ensemble d'envois ou de réceptions de messages dans une même unité de travail. Comme avec les bases de données, une session transactionnelle n'est validée qu'après un appel implicite ou explicite d'un ordre commit. Si donc, vous désirez pouvoir travailler avec plusieurs messages pour une même session, vous devez autoriser le mode transactionnel dans le premier argument de la fonction. Le deuxième est utile pour savoir si vous désirez qu'un accusé réception soit renvoyé afin de préciser que message est bien arrivé à sa destination. Dans l'affirmative, vous devez utilisez la constante Session.AUTO_ACKNOWLEDGE.

Toutefois, la spécification indique que la valeur de ces arguments est ignorée au sein d'un conteneur EJB. En effet, celui-ci gère les transactions et les accusés réceptions en fonction des paramètres de déploiement.

Les bonnes pratiques de développement incitent à fermer les connexions une fois le travail terminé.

Connection connexion = fabrique.createConnection();
...
connexion.close();

MessageProducer et MessageConsumer

La dernière étape nous sert à préciser le sens du transfert du message, est-ce pour envoyer ou est-ce pour recevoir ? Deux objets correspondent à ces deux situations, respectivement MessageProducer et MessageConsumer :

MessageProducer envoi = session.createProducer(destination);
MessageConsumer
réception = session.createConsumer(destination);
...
envoi.send(message);

Chacune des méthodes de l'objet session prend en paramètre la destination sur laquelle l'objet est connecté. Une fois que la nature de l'échange est créée, vous n'avez plus qu'à utiliser la méthode correspondante, notamment la méthode send() pour envoyer le message (dans le cas de la réception, il faut mettre en place une gestion d'événement que nous allons bientôt découvrir).

Ajustement possible des composants JMS suivant le mode de communication choisi

ConnectionFactory, Destination, MessageProducer et MessageConsumer sont en réalité des interfaces génériques, que nous pouvons utiliser directement sans aucun problème. Il est toutefois possible, dès le départ, de proposer des interfaces plus spécifiques, qui héritent d'ailleurs de ces interfaces, correspondant respectivement au mode point à point ou au mode publication/abonnement. Voici, en conséquence, les différentes interfaces que vous pouvez choisir :

Générique point à point publication/abonnement
ConnectionFactory QueueConnectionFactory TopicConnectionFactory
Connection QueueConnection TopicConnection
Destination Queue Topic
Session QueueSession TopicSession
MessageProducer QueueSender TopicPublisher
MessageConsumer QueueReceiver TopicSuscriber

A titre d'exemple, voici l'enchaînement des classes que nous pouvons prendre dans le cas spécifique d'un mode point à point :

Context ctx = new InitialContext();
QueueConnectionFactory fabrique = (QueueConnectionFactory)ctx.lookup("QueueConnectionFactory");
Queue destination = (Queue)ctx.lookup("queue/MaFile");
QueueConnection connexion = fabrique.createConnection();
QueueSession session = connexion.createSession(true, 0);
QueueSender envoi = session.createProducer(destination);
ou
QueueReceiver réception = session.createConsumer(destination);

Les messages

Venons en maintenant au point crucial, c'est-à-dire les messages. Bien évidemment, pour dialoguer, les clients JMS s'échangent des messages, c'est-à-dire qu'un client expédie un message vers une file d'attente, et qu'un client destinataire exécutera un traitement à la réception de ce message. Dans JMS, un message est un objet Java qui doit implémenter l'interface javax.jms.Message. Il est composé de trois parties :

  1. L'en-tête (header) : qui se compose des informations de destination, d'expiration, de priorité, date d'envoi, etc.
  2. Les propriétés (properties) : qui représentent les caractéristiques fonctionnelles du message.
  3. Le corps du message (body) : qui contient les données à transporter.
L'en-tête du message

L'en-tête du message contient un certain nombre de champs prédéfinis permettant de l'identifier. Nous pouvons voir cette section comme les métadonnées du message : qui a créé le message, date de création, durée de vie, accusé réception demandé ou non, etc. Chacune de ces métadonnées possède des accesseurs getXxx() et setXxx() (définis dans l'interface javax.jms.Message) qui permettent d'en modifier le contenu, mais la plupart sont affectées automatiquement par le fournisseur.

Nom Description
JMSMessageID identifiant unique de message
JMSCorremationID Utilisé pour associer de façon applicative deux messages par leur identifiant.
JMSDeliveryMode Il existe deux modes d'envoi : persistant ( le message est délivré une et une seule fois au destinataire, c'est-à-dire que même au cas de panne du fournisseur, le message sera délivré) et non persistant (le message peut ne pas être délivré en cas de panne puisqu'il n'est pas rendu persistant).
JMSDestination File d'attente destinataire du message.
JMSExpiration Date d'expiration du message.
JMSPriority Priorité du message. Cet attribut indique la priorité de façon croissante à partir de 0 (les messages de niveau 9 ont plus de priorité que les messages de niveau 0).
JMSRedelivered Booléen qui signifie que le message a été redélivré au destinataire.
JMSReplyTo File d'attente de réponse du message.
JMSTimestamp L'heure d'envoi du message est affecté automatiquement par le fournisseur.

Les propriétés

Cette section du message est optionnelle et agit comme une extension des champs d'en-tête. Les propriétés d'un message JMS sont des couples (nom, valeur), où la valeur est un type de base du langage Java (entiers, chaînes de caractères, booléens, etc.). L'interface javax.jms.Message définit des accesseurs pour manipuler ces valeurs. Ces données sont généralement positionnées par le client avant l'envoi d'un message et, comme nous le verrons par la suite, peuvent être utilisées pour filtrer les messages.

Le corps du message

Le corps du message, bien qu'optionnel, est la zone qui contient les données. Ces données sont formatées selon le type du message qui est défini par les interfaces suivantes (qui héritent toutes de javax.jms.Message) :

Interface Description
javax.jms.BytesMessage Pour les messages sous forme de flux d'octets.
javax.jms.TextMessage Echange de données de type texte.
javax.jms.ObjectMessage Messages composés d'objets Java sérialisés.
javax.jms.MapMessage Echange de données sous la forme clé/valeur. La clé doit être une String et la valeur de type primitif.
javax.jms.StreamMessage Echange de données en provenance d'un flux.

javax.jms.BytesMessage

Le premier type de message, BytesMessage, sur lequel nous pouvons travailler, permet d'échanger les tableaux d'octets ainsi que les types primitifs. Voici les méthodes que nous pouvons prendre :

  1. Les méthodes de lecture : readBoolean(), readByte(), readBytes(byte[]), readChar(), readDouble(), readFloat(), readInt(), readLong(), readShort() et readUTF().
  2. Les méthodes d'écriture : writeBoolean(), writeByte(), writeBytes(byte[]), writeChar(), writeDouble(), writeFloat(), writeInt(), writeLong(), writeObject(), writeShort() et writeUTF().

Voici un exemple qui permet d'envoyer plusieurs messages qui comportent des valeurs primitives :

Session session = connexion.createSession(true, 0);
MessageProducer envoi = session.createProducer(destination);
BytesMessage message = session.createBytesMessage();
message.writeInt(15);
message.writeDouble(-6.78);
message.writeBoolean(true);
envoi.send(message);

javax.jms.TextMessage

Ce type de message, TextMessage, est certainement le plus simple puisqu'il ne comporte que deux méthodes essentielles : getText() et setText() :

Session session = connexion.createSession(true, 0);
MessageProducer envoi = session.createProducer(destination);
TextMessage message = session.createTextMessage();
message.setText("Bienvenue");
message.setText(" à tout le monde");
envoi.send(message);

javax.jms.ObjectMessage

Avec Java, nous pouvons envoyer un objet sérialisable à l'aide de ObjectMessage. Là aussi, c'est très simple puique cette interface dispose également de deux méthodes getObject() et setObject() :

class Personne implements Serializable { ... }
...
Personne moi = new Personne();
Personne toi = new Personne();
...
Session
session = connexion.createSession(true, 0);
MessageProducer envoi = session.createProducer(destination);
ObjectMessage message = session.createObjectMessage();
message.setText(moi);
message.setText(toi);
envoi.send(message);

javax.jms.MapMessage

Ce type de message, MapMessage, permet d'envoyer et de recevoir des informations suivant le système clé/valeur. Ainsi, nous retrouvons les mêmes méthodes que pour le type BytesMessage, mais à chaque fois, nous devons préciser la clé sous forme de chaîne de caractères. Par ailleurs, les méthodes sont plutôt des accesseurs getXxx() et setXxx() :

Session session = connexion.createSession(true, 0);
MessageProducer envoi = session.createProducer(destination);
MapMessage message = session.createMapMessage();
message.setInt("nombre", 15);
message.setDouble("débit", -6.78);
message.setBoolean("acquitement", true);
envoi.send(message);

javax.jms.StreamMessage

Ce type de message, StreamMessage, est vu cette fois-ci comme un flux. Il ressemble beaucoup d'ailleurs au type DataInputStream ou DataOutputStream. A ce titre nous retrouvons exactement les mêmes méthodes que BytesMessages, mais avec en plus quelques méthodes supplémentaires, savoir : readObject(), readString(), writeObject() et writeString().

Session session = connexion.createSession(true, 0);
MessageProducer envoi = session.createProducer(destination);
StreamMessage message = session.createStreamMessage();
message.writeInt(15);
message.writeDouble(-6.78);
message.writeBoolean(true);
envoi.send(message);

Comment envoyer un message

Pour envoyer un message, nous avons déjà tout recensé. Nous connaissons tout. Nous avons juste à revoir l'ensemble des éléments à mettre en oeuvre.

  1. Tout d'abord, la fabrique de connexion (ConnectionFactory) et la destination (Destination) doivent être connues par le client JMS.
  2. Une fois la référence de la ConnectionFactory obtenue, on se connecte au provider (fournisseur) JMS via l'objet Connection.
  3. A partir de cette connexion, nous devons obtenir une session (Session).
  4. A partir de cette session, nous devons créer un MessageProducer qui va permettre d'envoyer des messages auprès d'une destination.
  5. La session permet également de créer le message suivant le type choisi.

Context ctx = new InitialContext();
ConnectionFactory fabrique = (ConnectionFactory)ctx.lookup
("ConnectionFactory");
Destination destination = (Destination)ctx.lookup
("queue/MaFile");
Connection connexion = fabrique.createConnection
();
Session
session = connexion.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer envoi = session.createProducer(destination);
TextMessage message = session.createTextMessage();
message.setText("Bienvenue");
message.setText(" à tout le monde");
envoi.send(message);
connexion.close();

Comment recevoir un message

Le consommateur du message est le client capable d'être à l'écoute d'une file d'attente (ou d'un sujet), et de traiter les messages à leur réception. En effet, le client doit être constamment à l'écoute (listener) et, à l'arrivée d'un nouveau message, il doit pouvoir le traiter. Pour cela, l'application doit appeler la méthode onMessage() de l'interface javax.jms.MessageListener. Celle-ci est très spécialisée et permet ainsi la réception asynchrone des messages. charge au développeur d'implémenter cette interface pour réaliser le traitement adéquat lors de la réception d'un message. Voici la procédure à suivre :

  1. Tout d'abord, comme l'envoi d'un message, la fabrique de connexion (ConnectionFactory) et la destination (Destination) doivent être connues par le client JMS.
  2. Une fois la référence de la ConnectionFactory obtenue, le consommateur doit se connecter au provider (fournisseur) JMS via l'objet Connection.
  3. A partir de cette connexion, nous devons obtenir une session (Session).
  4. A partir de la session, on crée un MessageConsumer qui va permettre de consommer les messages. Pour ce faire, nous associons un listener MessageListener pour traiter les messages de façon asynchrone. Ainsi, à chaque réception d'un nouveau message, la méthode onMessage() est automatiquement invoquée et peut effectuer le traitement désiré.
  5. Attention : à ce stade, il ne faut surtout pas oublier de démarrer la connexion avec la méthode start() sinon aucun message ne sera reçu.

public class Réception implements MessageListener {
   private Panneau panneau = new Panneau();
    
   public Réception() throws Exception {
      Context ctx = new InitialContext();
      ConnectionFactory fournisseur = (ConnectionFactory) ctx.lookup("ConnectionFactory");
      Destination destination = (Destination)ctx.lookup("queue/maFile");
      Connection connexion = fournisseur.createConnection();
      Session session = connexion.createSession(false, Session.AUTO_ACKNOWLEDGE);
      MessageConsumer réception = session.createConsumer(destination);
      réception.setMessageListener(this);
      connexion.start();

   }
 
   public static void main(String[] args) throws Exception {
      new Réception();
   }
 
   public void onMessage(Message arg) {
      try {
        TextMessage message = (TextMessage) arg;
        System.out.println(message.getText());
   			System.out.println(message.getText());
      } 
      catch (Exception ex) { }

   }
}

 

Choix du chapitre Deux applications clientes en communication avec JMS

Nous allons mettre en oeuvre nos nouvelles connaissances sur une communication asynchrone entre deux applications clientes au travers du service JMS. Pour illustrer ces différents mécanismes d'échange, nous allons créer deux applications clientes fenêtrées.

  1. La première doit récupérer des photos présentes sur le poste local et les afficher ensuite dans la zone principale de la fenêtre. Ainsi, vous avez la possibilité de choisir la photo qui vous plait afin de l'envoyer au service de messagerie asynchrone JMS.
  2. La deuxième, sur un autre poste client, en attente d'éventuels messages venant du même fournisseur de messagerie, affiche la photo envoyée par la première application.

Application cliente qui envoi les photos ............................................. Application cliente qui reçoit les photos


Réglage du serveur d'application Glassfish pour exploiter le service JMS

Nous avons deux réglages spécifiques pour mettre en oeuvre le service JMS, d'une part ce qui correspond à la connexion du fournisseur (ConnectionFactory), d'autre part le type de destination (Destination) : le mode point à point ou le mode publication/abonnement. Comme pour la base de données, vous devez effectuer ces différents réglages dans la rubrique Resources :

Ressources supplémentaires à proposer avec chaque application cliente

Pour que chaque client puisse se connecter au bon serveur d'application et pour que le comportement soit bien compris par ce dernier, nous devons d'une part régler le fichier jndi.properties et ensuite déployer l'ensemble des archives correspondant au serveur utilisé.

Nous voyons ici les archives à prendre en compte pour le serveur d'application Glassfish.

Architecture globale des postes clientes avec le serveur d'applications

Application clientes

Après ces différents réglages, nous pouvons maintenant passer dans le vif du sujet, c'est-à-dire le codage des deux applications clientes, d'une part celle qui envoie les messages photos.EnvoyerPhotos et ensuite celle qui reçoit les messages photos.Visionneuse.

photos.EnvoyerPhotos.java
  1 package photos;
  2 
  3 import javax.swing.*;
  4 import java.awt.*;
  5 import java.awt.event.*;
  6 import java.awt.image.BufferedImage;
  7 import java.io.*;
  8 import javax.imageio.*;
  9 import javax.naming.*;
 10 import javax.jms.*;
 11 
 12 public class EnvoyerPhotos extends JFrame implements ActionListener {
 13    private String répertoire = "J:/Stockage/";
 14    private String[] liste;
 15    private Panneau panneau = new Panneau();
 16    private JComboBox choix;
 17    private JButton envoyer = new JButton("Envoyer la photo");
 18    
 19    private static ConnectionFactory fournisseur;
 20    private static Destination destination;
 21 
 22    public EnvoyerPhotos() {
 23        liste = new File(répertoire).list();
 24        choix = new JComboBox(liste);
 25        panneau.change(récupérer());
 26        choix.addActionListener(this);
 27        envoyer.addActionListener(this);
 28        setSize(500, 400);
 29        setTitle("Stockage de photos");
 30        add(choix, BorderLayout.NORTH);
 31        add(envoyer, BorderLayout.SOUTH);
 32        add(panneau);
 33        setDefaultCloseOperation(EXIT_ON_CLOSE);
 34        setVisible(true);
 35    }
 36     
 37    private BufferedImage récupérer() {
 38       try {
 39          BufferedImage photo = ImageIO.read(new File(répertoire+choix.getSelectedItem()));
 40          return photo;
 41       }
 42       catch (Exception ex) {
 43          setTitle("Problème de localisation des photos");
 44          return null;
 45       }      
 46    }
 47 
 48    public static void main(String[] args) throws Exception {
 49        Context ctx = new InitialContext();
 50 //       ctx.addToEnvironment("SECURITY_PRINCIPAL", "guest");
 51 //       ctx.addToEnvironment("SECURITY_CREDENTIALS", "guest");
 52        fournisseur = (ConnectionFactory) ctx.lookup("JmsFournisseurPhotos");
 53        destination = (Destination)ctx.lookup("JmsPointVisionneuse");
 54        new EnvoyerPhotos();
 55    }
 56 
 57    public void actionPerformed(ActionEvent e) {
 58       if (e.getSource()==choix) {
 59          panneau.change(récupérer());
 60       }
 61       else if (e.getSource()==envoyer) {       
 62          try {
 63              File fichier = new File(répertoire+choix.getSelectedItem());
 64              byte[] octets = new byte[(int)fichier.length()];
 65              FileInputStream photo = new FileInputStream(fichier);
 66              photo.read(octets);
 67              envoyer(octets);
 68           }
 69           catch (IOException ex) {
 70              setTitle("Problème avec le fichier");
 71           }
 72       }
 73    }
 74    
 75    private void envoyer(byte[] octets) {
 76         try {
 77             Connection connexion = fournisseur.createConnection();
 78             Session session = connexion.createSession(false, Session.AUTO_ACKNOWLEDGE);
 79             MessageProducer envoi = session.createProducer(destination);
 80             StreamMessage message = session.createStreamMessage();
 81             message.writeString((String)choix.getSelectedItem());
 82             message.writeInt(octets.length);
 83             message.writeBytes(octets);
 84             envoi.send(message);
 85             connexion.close();
 86         } 
 87         catch (JMSException ex) {
 88             setTitle("Problème avec le serveur");
 89         }
 90    }
 91 }
 92 
 93 class Panneau extends JComponent {
 94    private BufferedImage image;
 95    private double ratio;
 96    
 97    public void change(BufferedImage image) {
 98      if (image!=null) {
 99         this.image = image;
100         ratio = (double)image.getWidth()/image.getHeight();  
101         repaint();
102      }
103    }
104   
105    protected void paintComponent(Graphics surface) {
106       if (image!=null)
107         surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);   
108    }   
109 }

Rappelez-vous que nous avons un certain nombre d'objets à mettre en oeuvre et ceci dans un ordre bien précis. Nous devons :

  1. Connaître le fournisseur JMS - ligne 52.
  2. Choix de la destination - ligne 53.
  3. Etablir réellement la connexion avec le fournisseur - ligne 77.
  4. Prévoir une session sans transaction avec accusé réception - ligne 78.
  5. Création d'un objet pour l'envoi de messages dans la destination choisie - ligne 79.
  6. Création d'un message adapté pour envoyer la photo désirée avec son nom de fichier et sa taille - lignes 80 à 83.
  7. Envoi du message structuré - ligne 84.
  8. Clôture de la connexion avec le fournisseur JMS - ligne 85.

Sur les lignes 50 et 51, vous remarquez la présence de commentaires qui expliquent comment agir sur un fournisseur qui propose une authentification. Par défaut, l'identification se fait automatiquement avec les valeurs guest. Voici d'ailleurs les propriétés qui sont placées automatiquement lorsque vous mettez en oeuvre la ConnectionFactory au niveau de votre serveur d'applications.

photos.Visionneuse.java
 1 package photos;
 2 
 3 import java.util.logging.*;
 4 import javax.swing.*;
 5 import java.awt.*;
 6 import java.awt.event.*;
 7 import java.awt.image.*;
 8 import java.io.*;
 9 import javax.imageio.*;
10 import javax.naming.*;
11 import javax.jms.*;
12 
13 public class Visionneuse extends JFrame implements MessageListener {
14    private Panneau panneau = new Panneau();
15    
16    public Visionneuse() throws Exception {
17       Context ctx = new InitialContext();
18       ConnectionFactory fournisseur = (ConnectionFactory) ctx.lookup("JmsFournisseurPhotos");
19       Destination destination = (Destination)ctx.lookup("JmsPointVisionneuse");
20       Connection connexion = fournisseur.createConnection();
21       Session session = connexion.createSession(false, Session.AUTO_ACKNOWLEDGE);
22       MessageConsumer réception = session.createConsumer(destination);
23       réception.setMessageListener(this);
24       connexion.start();
25       setSize(500, 400);
26       setTitle("Visionneuse");
27       add(panneau);
28       setDefaultCloseOperation(EXIT_ON_CLOSE);
29       setVisible(true);
30    }
31 
32    public static void main(String[] args) throws Exception {
33        new Visionneuse();
34    }
35 
36    public void onMessage(Message arg) {
37       try {
38          StreamMessage message = (StreamMessage) arg;
39          setTitle(message.readString());
40          byte[] octets = new byte[message.readInt()];
41          message.readBytes(octets);
42          ByteArrayInputStream fluxImage = new ByteArrayInputStream(octets);
43          BufferedImage photo = ImageIO.read(fluxImage);
44          panneau.change(photo);       
45       } 
46       catch (Exception ex) {
47          Logger.getLogger("global").log(Level.SEVERE, null, ex);
48       }
49    }
50 }

Sur l'application cliente qui reçoit les messages, nous retrouvons un certain nombre d'objets qui sont identiques à l'application précédente. La grosse différence vient de la mise en place d'une gestion d'événements avec un écouteur de message adapté. Voici l'ensemble de la procédure à suivre :

  1. La classe doit implémenter l'interface MessageListener - ligne 13.
  2. Il faut également connaître le fournisseur JMS - ligne 18.
  3. Choisir la destination - ligne 19.
  4. Etablir réellement la connexion avec le fournisseur - ligne 20.
  5. Prévoir une session sans transaction avec accusé réception - ligne 21.
  6. Créer un objet pour la réception de messages par rapport à la destination choisie - ligne 22.
  7. Mettre en place un système d'écoute pour être en permanence en attente d'un nouveau message - ligne 23.
  8. Et surtout activer et démarrer la connexion avec le fournisseur JMS pour effectivement recevoir les messages - ligne 24.

    ...............................................................................................
  9. Traitement des messages lorsqu'ils sont reçus - lignes 36 à 49.
  10. Adaptation du message reçu suivant le type choisi dans le protocole d'échange - ligne 38.
  11. Récupération des différents éléments constituant le message - lignes 39 à 41.

Il est possible de sélectionner les messages à récupérer à partir de certains critères. Cette partie sera expliquée à la fin du chapitre suivant.
Si vous souhaitez vous y rendre directement.

 

Choix du chapitre Message Driven Bean - MDB

Pour l'instant, nous venons d'utiliser les compétences de JMS, qui est un service délivré par Java EE, sans passer par un bean quelconque. Cette messagerie est très perfomante et très intéressante puisqu'elle permet une communication entre applications clientes, même si l'une d'elle n'est pas encore en service. Le message délivré n'est pas perdu pour autant. Dès que l'application concernée rentre en activité, elle reçoit le message qui lui était destiné.

Pour mettre en place ce système, nous avons besoin, comme nous l'avons découvert, de faire des recherches JNDI, d'une part pour se connecter au fournisseur JMS, et d'autre part pour choisir la destination souhaitée. Cela peut prendre un certain temps. Par ailleurs, chaque application cliente doit posséder des archives supplémentaires uniquement pour la partie JMS, qui sont au nombre de trois.

Nous avons toutefois la possibilité, pour le client qui envoie un message, de passer par un bean session. Effectivement, nous avons vu qu'il est possible d'utiliser l'injection au travers de l'annotation @Resource pour mettre en place la connexion au fournisseur et désigner la destination. Dans ce cas de figure, côté application cliente, nous pourrons nous passer des archives supplémentaires, et nous aurons un seul appel au service JNDI pour retrouver le bean session. Par contre, l'inconvénient de cette structure, c'est que nous devons développer l'interface correspondante au bean session afin qu'elle soit déployée avec le client et qu'elle permette ainsi la communication entre le client et le serveur Java EE.

Toutefois, il n'est pas possible de le faire pour l'application qui reçoit le message. En effet, nous sommes obligé de concerver la gestion d'événements propre à la réception de message, et du coup nous avons besoin de tous les éléments nécessaires à la construction de cette réception, comme la ConnectionFactory et la Destination.

Dans certaines situations, il arrive que les messages ne soient pas destinés à un client en particulier, mais que ce soit plutôt le serveur d'applications qui doit s'en occuper. L'exemple le plus parlant et d'envoyer un mail pour un certain nombre d'événements qui se produisent : réception de nouvelles photos, suppression de quelques unes, etc. Nous pourrions développer une application cliente tierce pour résoudre ce problème, mais cela réclame beaucoup d'énergie et de ressource. Heureusement, il existe un composant, un EJB qui est spécialisé pour la réception de messages côté serveur, et qui plus est, est très facile à développer et à mettre en oeuvre. Il s'agit du troisième type de bean qui se nomme Message Driven Bean ou MDB.

Définition du Message Driven Bean ou MDB

Un Message Driven Bean ou MDB est un EJB qui se comporte comme un listener JMS, c'est-à-dire qui reçoit des messages et les traite de manière asynchrone. Les MDB se rapprochent des EJB stateless car ils sont, eux aussi, sans état. Ils s'exécutent à l'intérieur du conteneur EJB qui assure donc le multithreading, la sécurité ou la gestion des transactions.

Les MDB sont à l'écoute (listener) d'une file d'attente et se réveillent à chaque arrivée de messages. En fait, il faut garder à l'esprit que c'est le conteneur qui est le véritable listener JMS et qu'il délègue au MDB le traitement du message, et plus particulièrement à la méthode onMessage() que nous avons déjà utilisée. Comme les autres EJB, le MDB peut accéder à tout type de ressources : EJB, JDBC, JavaMail, etc.

Attention : un MDB ne possède pas d'interface distante ou locale puisqu'il n'est pas utilisé par un client. Il est constitué d'une seule classe Java qui doit être annotée par javax.ejb.MessageDriven. Pour réagir à l'arrivée d'un message, il doit implémenter la méthode onMessage(javax.jms.Message) définie dans l'interface javax.jms.MessageListener. Il est associé à une destination JMS, c'est-à-dire à une Queue pour les communications point à point ou à un Topic pour les communications publication/souscription. Avec un MDB, vous n'avez plus à vous préoccuper du fournisseur, donc pas de ConnectionFactory. La méthode onMessage() est activée à la réception d'un message envoyé par un client JMS.

Mise en oeuvre au travers du projet de stockage à distance de photos numériques

Nous allons mettre en place, à la fois le bean session stateless qui s'occupe d'envoyer les messages, et le bean MDB qui va traiter ces messages. Pour cela, nous allons reprendre le projet qui permet de stocker, sur un seul serveur, des photos qui se trouvent sur différents postes clients. Les différents traitements ont été largement développés dans les études précédentes, je passerais donc plus de temps sur la partie gestion et traitement des messages.

Nous avons :

Côté client Côté serveur

Une application fenêtrée qui permet à la fois de visualiser les photos du disque dur du poste client et les photos qui sont archivées sur le serveur. Vous pouvez stocker de nouvelles photos ou supprimer celles qui sont déjà présentes :

Une fenêtre apparaît automatiquement dès qu'un client se connecte au serveur d'applications. Lorsqu'une nouvelle photo est archivée, elle est automatiquement affichée dans la zone principale de la fenêtre. Dans la barre de titre, vous voyez apparaître l'ensemble des événements qui se produisent en relation avec l'activité du client :

L'affichage de ces événements sont également répercutés tout simplement dans la console de visualisation du serveur :

Architecture du projet

Fichier de propriétés jndi.properties et archives à déployer sur chaque poste client

Cette fois-ci, la communication entre le client et le serveur se fait au travers du bean session stateless ArchivagePhotosRemote. Nous n'avons donc plus besoin des archives supplémentaires relatives à JMS. En effet, c'est le bean session qui s'occupe d'envoyer les messages.

Voici les paramètres correspondant au serveur d'applications Glassfish :

jndi.properties (Glassfish)
# Accès au serveur d'application Glassfish
java.naming.factory.initial=com.sun.enterprise.naming.SerialInitContextFactory
java.naming.factory.url.pkgs=com.sun.enterprise.naming
java.naming.factory.state=com.sun.corba.ee.impl.presentation.rmi.JNDIStateFactoryImpl
org.omg.CORBA.ORBInitialHost=portable
org.omg.CORBA.ORBInitialPort=3700
Archives à déployer avec l'application cliente pour le serveur d'application Glassfish
# Archives à installer
appserv-rt.jar
javaee.jar
appserv-deployment-client.jar
appserv-ext.jar

Codage des sources côté client

Pour dialoguer et archiver les photos sur le serveur d'applications, nous passons par l'interface ArchivagePhotosRemote. Celle-ci est en relation directe et à distance avec le bean session stateless ArchivagePhotosBean. Elle propose les méthodes suivantes :

  1. stocker() : permet de stocker la photo choisie par le client en précisant le nom du fichier et la suite des octets constituant la photo.
  2. liste() : délivre la liste des fichiers photos déjà archivées.
  3. supprimer() : détruit la photo archivée dans le serveur dont le nom du fichier est spécifié en argument.
  4. getPhoto() : restitue la photo archivée dans le serveur dont le nom du fichier est spécifié en argument.

Pour le code de l'application cliente, nous n'avons rien de particulier, si ce n'est de faire appel au service du bean session représenté par l'interface ArchivagePhotosRemote :

photos.ArchivagePhotosRemote.java
package photos;

import java.io.IOException;
import javax.ejb.Remote;
import javax.jms.*;

@Remote
public interface ArchivagePhotosRemote {   
   void stocker(String intitulé, byte[] octets) throws IOException;
   String[] liste();
   void supprimer(String nom);
   byte[] getPhoto(String nom) throws IOException;
}
photos.EnvoyerPhotos.java
  1 package photos;
  2 
  3 import javax.swing.*;
  4 import java.awt.*;
  5 import java.awt.event.*;
  6 import java.awt.image.BufferedImage;
  7 import java.io.*;
  8 import javax.imageio.*;
  9 import javax.naming.*;
 10 import javax.jms.*;
 11 
 12 public class EnvoyerPhotos extends JFrame implements ActionListener {
 13   private String répertoire = "J:/Stockage/";
 14   private static ArchivagePhotosRemote archivage;
 15    
 16   private String[] listeLocal;
 17   private String[] listeServeur;
 18   private Panneau panneauPhoto = new Panneau();
 19   private JComboBox choixLocal;
 20   private JComboBox choixServeur;
 21   private JButton envoyer = new JButton("Stocker");
 22   private JButton supprimer = new JButton("Supprimer");   
 23   private JPanel panneauNord = new JPanel();
 24   private JPanel panneauSud = new JPanel();
 25 
 26   public EnvoyerPhotos() throws IOException {
 27     listeLocal = new File(répertoire).list();
 28     listeServeur = archivage.liste();
 29     choixLocal = new JComboBox(listeLocal);
 30     choixServeur = new JComboBox(listeServeur);
 31     panneauPhoto.change(ImageIO.read(new File(répertoire + choixLocal.getSelectedItem())));
 32     choixLocal.addActionListener(this);
 33     envoyer.addActionListener(this);
 34     choixServeur.addActionListener(this);
 35     supprimer.addActionListener(this);
 36     setSize(500, 500);
 37     setTitle("Stockage de photos");
 38     panneauNord.add(new JLabel("Photos en local : "));
 39     panneauNord.add(choixLocal);
 40     panneauNord.add(envoyer);
 41     add(panneauNord, BorderLayout.NORTH);
 42     panneauSud.add(new JLabel("Sur le serveur : "));
 43     panneauSud.add(choixServeur);
 44     panneauSud.add(supprimer);
 45     add(panneauSud, BorderLayout.SOUTH);
 46     add(panneauPhoto);
 47     setDefaultCloseOperation(EXIT_ON_CLOSE);
 48     setVisible(true);
 49   }
 50 
 51   public static void main(String[] args) throws Exception {
 52     Context ctx = new InitialContext();
 53     archivage = (ArchivagePhotosRemote) ctx.lookup(ArchivagePhotosRemote.class.getName());
 54     new EnvoyerPhotos();
 55   }
 56 
 57   public void actionPerformed(ActionEvent e) {
 58     if (e.getSource()==choixLocal) {
 59       try {
 60         panneauPhoto.change(ImageIO.read(new File(répertoire + choixLocal.getSelectedItem())));
 61       } 
 62       catch (IOException ex) {
 63         setTitle("Problème de localisation des photos");
 64       }
 65     }
 66     else if (e.getSource()==envoyer) {       
 67       try {
 68         File fichier = new File(répertoire+choixLocal.getSelectedItem());
 69         byte[] octets = new byte[(int)fichier.length()];
 70         FileInputStream photo = new FileInputStream(fichier);
 71         photo.read(octets);
 72         archivage.stocker((String)choixLocal.getSelectedItem(), octets);
 73         choixServeur.addItem(choixLocal.getSelectedItem());
 74       }
 75       catch (IOException ex) {
 76         setTitle("Problème avec le fichier");
 77       }
 78     }
 79     else if (e.getSource()==choixServeur) {
 80       try {
 81         ByteArrayInputStream fluxImage = new ByteArrayInputStream(archivage.getPhoto((String) choixServeur.getSelectedItem()));
 82         panneauPhoto.change(ImageIO.read(fluxImage));
 83       } 
 84       catch (IOException ex) {
 85         setTitle("Problème avec le serveur");
 86       }
 87     }
 88     else if (e.getSource()==supprimer) {
 89       archivage.supprimer((String) choixServeur.getSelectedItem());
 90       choixServeur.removeItem(choixServeur.getSelectedItem());
 91     }
 92   }
 93 }
 94 
 95 class Panneau extends JComponent {
 96    private BufferedImage image;
 97    private double ratio;
 98    
 99    public void change(BufferedImage image) {
100      if (image!=null) {
101         this.image = image;
102         ratio = (double)image.getWidth()/image.getHeight();  
103         repaint();
104      }
105    }
106   
107    protected void paintComponent(Graphics surface) {
108       if (image!=null)
109         surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);   
110    }   
111 }

Codage des sources côté serveur

Nous nous intéressons tout d'abord au bean session stateless ArchivagePhotosBean. Cette fois-ci, contrairement à une application cliente, nous pouvons utiliser l'injection pour désigner le fournisseur JMS et la destination au travers de l'annotation @Resource (ligne 13 à 16) en spécifiant la valeur du paramètre mappedName. De cette façon, c'est beaucoup plus facile à mettre en oeuvre !

photos.ArchivagePhotosBean.java
 1 package photos;
 2 
 3 import java.io.*;
 4 import java.util.logging.*;
 5 import javax.annotation.*;
 6 import javax.ejb.*;
 7 import javax.interceptor.*;
 8 import javax.jms.*;
 9 
10 @Stateless
11 public class ArchivagePhotosBean implements ArchivagePhotosRemote {
12       
13    @Resource(mappedName="JmsFournisseurPhotos")
14    private ConnectionFactory fournisseur;
15    @Resource(mappedName="JmsPointVisionneuse")
16    private Queue destination;
17    
18    private final String répertoire = "J:/Photos/";
19     
20    public void stocker(String intitulé, byte[] octets) throws IOException {
21       File fichier = new File(répertoire + intitulé);
22       FileOutputStream stockage = new FileOutputStream(fichier);
23       stockage.write(octets);
24       stockage.close();
25     }
26 
27     public String[] liste() {
28       return new File(répertoire).list();
29     }
30 
31     public void supprimer(String nom) {
32       File fichier = new File(répertoire+nom);
33       fichier.delete();
34     }
35     
36     public byte[] getPhoto(String nom) throws IOException {
37       File fichier = new File(répertoire+nom);
38       byte[] octets = new byte[(int)fichier.length()];
39       FileInputStream photo = new FileInputStream(fichier);
40       photo.read(octets);
41       photo.close();
42       return octets;
43     }
44  
45    @AroundInvoke
46    private Object messagerie(InvocationContext ctx) throws Exception {
47       String nomMéthode = ctx.getMethod().getName();
48       String nomFichier = "";
49       if (ctx.getParameters()!=null) nomFichier = (String) ctx.getParameters()[0];
50       try {
51          Connection connexion = fournisseur.createConnection();
52          Session session = connexion.createSession(false, Session.AUTO_ACKNOWLEDGE);
53          MessageProducer envoi = session.