Envoi de messages - JavaMail - Service Timer

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.createProducer(destination);
54          MapMessage message = session.createMapMessage();
55          message.setLong("date", System.currentTimeMillis());
56          message.setString("commande", nomMéthode);
57          message.setString("nom", nomFichier);
58          envoi.send(message);
59          connexion.close();
60       } 
61       catch (JMSException ex) {
62          Logger.getLogger("global").log(Level.SEVERE, null, ex);
63       }
64       return ctx.proceed();
65    }
66 }

Les quatre méthodes que nous venons d'évoquer s'occupent de la logique métier et rend les services demandés par le client. Vous remarquez la présence de la méthode supplémentaire messagerie() qui est une méthode dénommée, je le rappelle, callback interceptor. Comme son nom l'indique, cette méthode particulière est activée par le conteneur d'EJB lui-même, et dans le cas qui nous préoccupe, elle est lancée à chaque fois qu'une méthode est activée par le client (interception des appels de méthode). Pour avoir un tel comportement, il faut signer la méthode désirée avec l'annotation @ArroundInvoke.

Le but de cette méthode messagerie() et d'envoyer un message au service JMS. Ce message est composé de la date et de l'heure actuelle, du nom de la méthode sollicité par le client et éventuellement du nom du fichier photo désiré. Nous proposons donc une trace des événements provoqués par les différentes actions demandées par le client.


Les messages envoyés sont ensuite traités par le bean spécialisé pour la réception des messages, le bean MDB MessagerieArchivageBean.

Attention, je le rappelle : 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 @MessageDriven. Pour réagir à l'arrivée d'un message, il doit implémenter la méthode onMessage(Message) définie dans l'interface 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 pas à 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

photos.MessagerieArchivageBean.java
 1 package photos;
 2 
 3 import java.awt.Graphics;
 4 import java.awt.image.*;
 5 import java.io.*;
 6 import java.text.MessageFormat;
 7 import java.util.Date;
 8 import java.util.logging.*;
 9 import javax.annotation.*;
10 import javax.ejb.*;
11 import javax.imageio.*;
12 import javax.jms.*;
13 import javax.swing.*;
14 
15 @MessageDriven(mappedName="JmsPointVisionneuse")
16 public class MessagerieArchivageBean extends JFrame implements MessageListener {
17    private String répertoire = "J:/Photos/";
18    private Panneau panneau = new Panneau();
19    
20    @PostConstruct
21    private void démarrer() {
22       System.out.println("Démarrage Bean Message");
23    }
24    
25    @PreDestroy
26    private void fin() {
27       System.out.println("Bean Message terminé");
28    }
29 
30    public MessagerieArchivageBean() {
31       setSize(500, 400);
32       setTitle("Visionneuse");
33       add(panneau);    
34       setVisible(true);      
35    }
36    
37    public void onMessage(Message message) {
38       try {
39          MapMessage msg = (MapMessage) message;
40          String motif = "{0, date, full} - {0, time, medium} : {1} : {2}";
41          Date date = new Date(msg.getLong("date"));
42          String commande = msg.getString("commande");         
43          String nom = msg.getString("nom");
44          String affiche = MessageFormat.format(motif, date, commande, nom);
45          System.out.println(affiche);
46          setTitle(affiche);
47          if (commande.equals("stocker")) afficherPhoto(nom);
48       } 
49       catch (JMSException ex) {
50           Logger.getLogger("global").log(Level.SEVERE, null, ex);
51       }
52    }   
53    
54    private void afficherPhoto(String fichier) {
55         try {
56             panneau.change(ImageIO.read(new File(répertoire + fichier)));
57         } 
58         catch (IOException ex) {
59             Logger.getLogger("global").log(Level.SEVERE, null, ex);
60         }
61    }
62    
63    class Panneau extends JComponent {
64       private BufferedImage image;
65       private double ratio;
66      
67       public void change(BufferedImage image) {
68          if (image!=null) {
69             this.image = image;
70             ratio = (double)image.getWidth()/image.getHeight();  
71             repaint();
72          }
73       }
74 
75       @Override
76       protected void paintComponent(Graphics surface) {
77         if (image!=null)
78            surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);   
79       }         
80    }
81 }

Pour avoir un bean MDB :

  1. Ligne 15 : Nous devons donc en premier lieu utiliser l'annotation @MessageDriven et spécifier le paramètre mappedName dont la valeur correspond à la destination du service JMS.
  2. Ligne 16 : Une fois que vous avez réalisé cette opération, vous pouvez choisir le nom de votre classe. Celle-ci peut même hériter d'une autre classe, comme c'est le cas ici. Ainsi, en héritant de JFrame, j'ai la possibilité d'afficher une fenêtre automatiquement à la création du bean, ce qui me permettra de visualiser les photos qui viennent juste d'être archivées.
  3. Lignes 16 et 37 : Il faut que le MDB implémente l'interface MessageListener et redéfinisse donc la méthode associée onMessage() qui sera donc automatiquement sollicité à chaque arrivée d'un nouveau message.
  4. Lignes 37 à 52 : Cette méthode onMessage() réalise le traitement associé au message qui, dans le cas présent, met en forme l'affichage des informations dans la barre de titre de la fenêtre et affiche éventuellement, dans la zone principale, la photo qui vient juste d'être archivée, mais uniquement si le client sollicite la méthode stocker().
  5. Remarquez qu'avec ce MDB, vous n'avez plus à vous préoccuper des objets annexes comme Connection, Session, MessageConsumer. Tout se fait automatiquement en tâche de fond.
  6. Lignes 20 à 28 : Comme tous les autres beans, le MDB possède des méthodes callback interceptor qui sont appelées respectivement à la suite de la construction de l'objet et juste avant sa destruction. Ces méthodes possèdent alors les annotations suivantes : @PostConstruct et @PreDestroy.

En fonctionnement, vous remarquerez qu'éventuellement plusieurs fenêtres apparaissent, ce qui démontre bien que le MDB s'exécutent à l'intérieur du conteneur EJB et assure donc le multithreading.

L'annotation @MessageDriven et son attribut activationConfig

Nous venons de voir l'annotation @MessageDriven. Cette annotation possède également un attribut activationConfig qui permet de configurer convenablement les propriétés du MDB. Comme nous pouvons avoir à réger plusieurs propriétés, nous devons donc utiliser un tableau de @ActivationConfigProperty précisant le nom de la propriété propertyName et sa valeur propertyValue.

Nous pouvons, par exemple stipuler que la destination est vraiment du type point à point :


@MessageDriven(mappedName="JmsPointVisionneuse",
   activationConfig={
      @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue")
})
public class MessagerieArchivageBean extends JFrame implements MessageListener {

Tolérance de la déconnexion à un Topic

Le modèle de messagerie de type "abonnement" oblige les applications clientes à être connectées au sujet (Topic) pour recevoir les messages de celui-ci. Si un problème survient, les clients déconnectés perdent les messages émis durant leur déconnexion. Cependant, le mode Topic offre la possibilité d'utiliser un abonnement durable. L'intérêt est donc de pouvoir recevoir les messages émis depuis la dernière déconnexion.

L'utilisation de ce genre d'abonnement doit être précisée au niveau des propriétés du MDB. La propriété à utiliser est subscriptionDurability. Les valeurs prises par celle-ci sont : Durable ou NonDurable. Par défaut, une souscription est NonDurable :


@MessageDriven(mappedName="JmsSujetVisionneuse",
   activationConfig={
      @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"),
      @ActivationConfigProperty(propertyName="subscriptionDurability", propertyValue="Durable")
})
public class MessagerieArchivageBean extends JFrame implements MessageListener {

Lorsque la destination est de type Queue, le principe d'abonnement durable n'a aucun sens. Par nature, pour ce genre de destination, le facteur "durable" n'a pas d'importance car les messages sont automatiquement stockés et doivent être consommés par un client unique.

Sélecteur de message

Il est possible de préciser certains critères permettant de ne pas recevoir l'ensemble des messages d'une destination. Le sélecteur de message utilise les propriétés du message en tant que critère dans les expressions conditionnelles. Ces conditions utilisent des expressions booléennes afin de déterminer les messages à recevoir.

Nous pouvons par exemple récupérer uniquement les messages qui correspondent à l'utilisation de la méthode stocker() par le client :


@MessageDriven(mappedName="JmsPointVisionneuse",
   activationConfig={
      @ActivationConfigProperty(propertyName="messageSelector", propertyValue="commande='stocker'")
})
public class MessagerieArchivageBean extends JFrame implements MessageListener {

Cette fois-ci, le MDB s'active uniquement que lorsque le client demande à archiver une nouvelle photo

Ces sélecteurs se basent sur les propriétés des messages. Celles-ci se situent dans l'en-tête du message, donc dépendant du contenu, et sont asignées par le créateur du message. Tous les types de message intègrent les méthodes de lecture et d'écriture de propriétés. En effet, ces méthodes sont décrites dans la super-interface javax.jms.Message. Les types de propriétés se basent sur les primitives Java : boolean, int, short, char...

Pour définir les valeurs des propriétés, il faut utiliser les méthodes setXxxProperty(Xxx)Xxx représente les types byte, float, String, Object... Les méthodes getXxxProperty() sont également proposées pour récupérer les valeurs.

Pour revenir à notre exemple, voici comment régler la propriété "commande" qui est utilisée par le MDB :


@AroundInvoke
private Object messagerie(InvocationContext ctx) throws Exception {
    String nomMéthode = ctx.getMethod().getName();
    String nomFichier = "";
    if (ctx.getParameters()!=null) nomFichier = (String) ctx.getParameters()[0];
    try {
         Connection connexion = fournisseur.createConnection();
         Session session = connexion.createSession(false, Session.AUTO_ACKNOWLEDGE);
         MessageProducer envoi = session.createProducer(destination);
         MapMessage message = session.createMapMessage();
         message.setLong("date", System.currentTimeMillis());
         message.setString("commande", nomMéthode);
         message.setString("nom", nomFichier);
         message.setStringProperty("commande", nomMéthode);
         envoi.send(message);
         connexion.close();
    } 
    catch (JMSException ex) {
         Logger.getLogger("global").log(Level.SEVERE, null, ex);
    }
    return ctx.proceed();
}

Le système repose sur les mêmes concepts que la sélection des enregistrements avec SQL. Vous pouvez utiliser les opérateurs NOT, AND, OR, <, > ... D'autres fonctionnalités sont possibles. Prenons le cas où nous souhaitons traiter dans le MDB tous les messages concernant le stockage, la suppression et la lecture des photos :


@MessageDriven(mappedName="JmsPointVisionneuse",
   activationConfig={
      @ActivationConfigProperty(propertyName="messageSelector", propertyValue="commande IN ('stocker', 'supprimer', 'getPhoto')")
})
public class MessagerieArchivageBean extends JFrame implements MessageListener {

Vous pouvez également penser à un système d'alerte, dans le cas où le résultat d'une valeur n'est pas situé entre 10% et -10%, pour prévenir qu'il s'agit d'un cas qui sort de l'ordinaire :

résultat NOT BETWEEN -10 AND 10

Voici juste un dernier exemple qui permet de gérer les messages qui correspondent à un archivage de photo ou à une lecture :


@MessageDriven(mappedName="JmsPointVisionneuse",
   activationConfig={
      @ActivationConfigProperty(propertyName="messageSelector", propertyValue="commande NOT LIKE '%i%'")
})
public class MessagerieArchivageBean extends JFrame implements MessageListener {

Cette fonctionnalité est utile lorsque nous souhaitons trier et répartir les messages situés dans la même destination vers différents MDB, afin de leur appliquer des traitements différents.

Sélection de message dans une application cliente

Si la réception des messages est traitée par une application cliente et non plus dans le serveur au moyen d'un MDB, il est également possible de choisir les messages à traiter. Cela se fait tout simplement lorsque nous créons le MessageConsumer au travers de la méthode Session.createConsumer(). En effet, cette méthode est surdéfinie. Nous pouvons prendre celle qui possède un deuxième argument correspondant au critère de sélections. Le critère de sélection est tout à fait du même ordre que celui que nous avons utilisé dans la rubrique précédente :

MessageConsumer réception = session.createConsumer(destination, "commande NOT LIKE '%i%'");

Accusé de réception

L'inconvénient d'un traitement asynchrone des messages est qu'il n'y a pas de valeur de retour à l'expéditeur. Il est donc difficile pour lui de savoir si le message a bien été transmis (lorsqu'il souhaite le savoir bien entendu). Il existe différentes solutions à ce problème.


La première consiste à utiliser des accusés de réception, mécanisme transparent géré par le fournisseur et le conteneur MDB. Cela permet à l'application cliente de signaler que le message a bien été reçu. Sans cet accusé réception, le message continuera d'être envoyé. Ce mécanisme repose sur les transactions au niveau du MDB. Lorsque celle-ci est gérée par le conteneur, l'accusé est envoyé à la suite du commit ou non si la transaction échoue.

Il existe deux modes d'accusé réception :

  1. Auto-acknowledge : L'accusé de réception doit être envoyé dès que le message a été transféré au MDB. Il s'agit d'un envoi automatique lors du commit de la transaction.

    
    @MessageDriven(mappedName="JmsPointVisionneuse",
       activationConfig={
          @ActivationConfigProperty(propertyName="acknowledgeMode", propertyValue="Auto-acknowledge")
    })
    public class MessagerieArchivageBean extends JFrame implements MessageListener {  
  2. Dups-ok-acknowledge : L'accusé de réception est repoussé à un instant indéterminé et le conteneur choisi ensuite le moment où il a peu de tâches à traiter pour l'envoyer. Cela permet évidemment d'économiser les ressources. Nous pouvons cependant déconseiller cette dernière valeur car le fournisseur pourrait croire que le message n'a pas été traité et déciderait de le transmettre à nouveau (ce qui pourrait entraîner des disfonctionnements). De plus, le coût d'envoi d'un accusé de réception est négligeable que ce soit en capacité de calcul ou en charge réseau.

    
    @MessageDriven(mappedName="JmsPointVisionneuse",
       activationConfig={
          @ActivationConfigProperty(propertyName="acknowledgeMode", propertyValue="Dups-ok-acknowledge")
    })
    public class MessagerieArchivageBean extends JFrame implements MessageListene

La deuxième solution consiste à spécifier la valeur JMSReplyTo dans les paramètres d'en-tête du message. Cela permet au destinataire d'envoyer une réponse vers la destination paramétrée. Du côté de l'expéditeur, nous définissons la destination de réponse de la manière suivante :

Destination destination = (Destination)ctx.lookup("queue/MaFile");
...
message.setJMSReplyTo
(destination);
message.setText("Bienvenue");
message.setText(" à tout le monde");
...

Le MDB recevant le message peut ensuite récupérer cette propriété et envoyer à son tour un message :

Destination destination = message.getReplyTo();

Cette méthode diffère de la précédente car elle permet un réel retour d'information concernant le traitement du message. Cette solution est typiquement utilisée dans un système d'expédition de commandes. Le message de retour alerte le système lorsque l'objet désiré est préparé et epédié. Cela n'aurait pas pu être implémenté avec la première solution. Cette solution est également intéressante pour remonter des rapports d'erreurs dans le processus métier.

 

Choix du chapitre JavaMail

Les principaux protocoles de messagerie

SMTP (Simple Mail Transport Protocole), protocole qui permet l'envoi d'e-mails vers un serveur.

POP3 (Post Office Protocole), protocole qui permet la réception d'e-mails. Protocole très populaire sur Internet, il définit une boîte aux lettres unique pour chaque utilisateur.

IMAP (Internet Message Acces Protocole), protocole qui permet la réception d'e-mails. Ce protocole est plus complexe car il apporte des fonctionnalités supplémentaires : plusieurs répertoires par utilisateur, partage de répertoires entre plusieurs utilisateurs, maintient des messages sur le serveur, etc.

NNTP (Network News Transport Protocol), protocole utilisé par les forums de discussion (news).

Le type MIME

Le type MIME (Multipurpose Internet Mail Extensions) est un standard permettant d'étendre les possibilités du courrier électronique, comme la possibilité d'insérer des documents (images, sons, texte, etc.) dans un courrier.

Les destinataires

Lorsque nous envoyons un e-mail, l'adresse du destinataire peut être typée :

RecipientType.TO : destinataire direct
RecipientType.CC : copie conforme
RecipientType.BCC : copie cachée

Attention au pare-feu

Si vous avez un firewall (pare-feu) sur votre machine, vérifiez bien qu'il autorise le protocole SMTP sur le port 25. Sinon, les e-mails seront bloqués et ne pourrons pas être envoyés.

 

Nous avons très souvent besoin que le système envoie un e-mail récapitulatif au client, lors d'une transaction. JavaMail est l'API qui nous permet d'utiliser le courrier électronique.

Le courrier électronique repose sur le concept de clients et de serveurs. Les clients mails (tel que Outlook ou Firebird, etc.) s'appuient sur un serveur de messagerie pour obtenir ou envoyer des e-mails. Ces échanges sont normalisés par des protocoles particuliers (SMTP, POP3, etc.).

L'API JavaMail permet de s'abstraire de tout système de mail et d'utiliser la plupart des protocoles de communication de façon transparente. Ce n'est pas un serveur d'e-mails, mais un outil pour interagir avec le serveur de messagerie. Les applications développées avec JavaMail sont en réalité comparables aux différentes messagerie clientes telle que Outlook, Firebird, etc. Cette API propose donc des méthodes pour lire ou envoyer des e-mails, rechercher un message, etc. Les classes et les interfaces de cette API sont regroupées dans le paquetage javax.mail.

Pour ce chapitre, nous n'utiliserons pas toute la panoplie des classes et des interfaces de l'API, mais juste les principales :


La classe Session

A la manière de JMS, JavaMail possède une classe javax.mail.Session qui établit la connexion avec le serveur de messagerie. C'est elle qui encapsule les données liées à la connexion (options de configuration, login, mot de passe, nom du serveur) et à partir de laquelle les actions sont réalisées :

Properties propriétés = new Properties();
propriétés.put("mail.smtp.host", "smtp.orange.fr");
propriétés.put("mail.smtp.auth", "true");
Session session = Session.getInstance(propriétés, null);

Pour créer une session, nous utilisons la méthode getInstance() à laquelle nous passons les paramètres d'initialisation.

La classe Message

La classe javax.mail.Message est une classe abstraite qui encapsule le contenu du courrier électronique. Un message est composé d'un en-tête qui contient l'adresse de l'auteur et du destinataire, le sujet, etc. et d'un corps qui contient les données à envoyer ou à recevoir. JavaMail fournit en standard une classe fille nommée javax.mail.internet.MimeMessage pour les messages possédant le type MIME.

La classe Message possède de nombreuses méthodes pour initialiser les données du message :

Méthode Description
Message(session) Créer un nouveau message.
Message(Folder, int) Créer un message à partir d'un message existant.
void addFromAdress(Adress[]) Ajouter des émetteurs au message.
void addRecipient(RecepientType, Address[]) Ajouter des destinataires à un type d'envoi (direct, en copie ou en copie cachée).
Flags getFlags() Retourne les états du message.
Adress[] getFrom() Retourne les émetteurs.
int getLineCount() Retourne le nombre ligne du message.
Address[] getRecipients(RecepientType) Retourne les destinataires du type fourni en paramètre.
int getSize() Retourne la taille du message.
String getSubject() Retourne le sujet du message.
Address getReplyTo() Renvoie les mails pour la réponse.
Message reply(boolean) Créer un message pour la réponse : le booléen indique si la réponse ne doit être faite qu'à l'emetteur.
void setContent(Object, String) Mettre à jour le contenu du message en précisant son type MIME.
void setFrom(Address) Mettre à jour l'émetteur.
void setRecipients(RecepientsType, Address[]) Mettre à jour les destinataires d'un type.
void setSentDate(Date) Mettre à jour la date d'envoi.
void setText(String) Mettre à jour le contenu du message avec le type MIME « text/plain »
void setReply(Address) Mettre à jour le destinataire de la réponse.
void writeTo(OutputStream) Envoie le message au format RFC 822 dans un flux. Très pratique pour visualiser le message sur la console en passant en paramètre (System.out)

Exemple de céation d'un message :

Message message = new MimeMessage(session);
message.setFrom(new InternetAddress("adresse@émetteur.fr"));
message.setRecipient(Message.RecipientType.TO, new InternetAddress("adresse@destinataire.fr"));
message.setSubject("Confirmation de la commande");
message.setText("La commande n°34256 a été bien envoyée.");
message.setSentDate(new Date());

La classe InternetAddress

La classe javax.mail.internet.InternetAddress est nécessaire pour chaque émetteur et destinataire d'e-mail. Elle hérite de la classe javax.mail.Address et représente une adresse e-mail au format contact@serveurmail.extension. Pour créer une adresse e-mail, il suffit de passer une chaîne de caractères au constructeur :

message.setFrom(new InternetAddress("adresse@émetteur.fr"));
message.setRecipient(Message.RecipientType.TO, new InternetAddress("adresse@destinataire.fr"));

La classe Transport

La classe javax.mail.Transport se charge d'envoyer le message avec le protocole adéquat. Dans notre cas, pour SMTP, il faut obtenir un objet Transport dédié à ce protocole en utilisant la méthode getTransport("smtp") d'un objet Session. Il faut ensuite établir la connexion en passant le nom du serveur de messagerie, le nom de l'utilisateur et son mot de passe. Pour envoyer le message que l'on a créé antérieurement, il faut utiliser la méthode sendMessage() en lui passant la liste des destinataires getAllRecipients(). Enfin, il faut fermer la connexion à l'aide de la méthode close() :

Transport transport = session.getTransport("smtp");
transport.connect("smtp.orange.fr", "utilisateur", "mot-de-passe");
transport.sendMessage(message, message.getAllRecipients());
transport.close();

Les classes Store et Folder

La classe abstraite Store représente un système de stockage de messages ou "messagerie". Pour se connecter à cette "messagerie" et ainsi pouvoir consulter vos messages, vous devez obtenir une instance de la classe Store avec la méthode getStore() de votre session, en lui donnant comme paramètre le protocole utilisé. Ensuite vous n'avez plus qu'a vous connecter avec la méthode connect(), en lui précisant le nom du serveur, le nom d'utilisateur et le mot de passe. La méthode close() permet de libérer la connexion avec le serveur.

Store store = session.getStore("pop3");
store.
connect("smtp.orange.fr", "utilisateur", "mot-de-passe");
...
store.close();

La classe abstraite Folder représente un répertoire dans lequel les messages sont stockés. Pour obtenir un instance de cette classe, il faut utiliser la méthode getFolder() d'un objet de type Store en lui précisant le nom du répertoire. Avec le protocole "POP3" qui ne gère qu'un seul répertoire, le seul possible est "INBOX". Ensuite, nous appelons la méthode open() en précisant le mode d'utilisation : READ_ONLY ou READ_WRITE.

Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_ONLY);

Si vous utilisez le protocole IMAP, vous pouvez alors avoir d'autres répertoires.
.

Pour obtenir les messages contenus dans le répertoire, il faut appeler la méthode getMessages(). Cette méthode renvoie un tableau de Message qui peut être null si aucun message n'est renvoyé. Une fois les opérations terminées, il faut fermer le répertoire en utilisant la méthode close().

Message[] messages = folder.getMessages();
folder.close();

En définitive, voici toute la procédure à suivre pour récupérer l'ensemble des messages stockées dans votre messagerie :

Store store = session.getStore("pop3");
store.
connect("smtp.orange.fr", "utilisateur", "mot-de-passe");
Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_ONLY);
Message[] messages = folder.getMessages();
folder.close();

store.close();

Pièces jointes

Il est possible de joindre avec le mail des ressources sous forme de pièces jointes (attachments). Pour cela, nous devons passer par un objet de type MultiPart. Cet objet contient à son tour des objets BodyPart. La structure d'un objet BodyPart ressemble à celle d'un simple objet Message. Donc chaque objet BodyPart contient des attributs et un contenu.

Ainsi, pour mettre en oeuvre les pièces jointes, vous devez :

  1. Instancier un objet de type MimeMessage.
  2. Renseigner les éléments qui composent l'en−tête : émetteur, destinataire, sujet ...
  3. Instancier un objet de type MimeMultiPart.
  4. Instancier un objet de type MimeBodyPart et alimenter le contenu de l'élément.
  5. Ajouter cet objet à l'objet MimeMultiPart grâce à la méthode addBodyPart().
  6. Répéter l'instanciation et l'alimentation pour chaque ressource à ajouter.
  7. Utiliser la méthode setContent() du message en passant en paramètre l'objet MimeMultiPart pour associer le message et les pièces jointes au mail.

Exemple :

Multipart multipart = new MimeMultipart();
// creation de la partie principale du message
BodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setText("Texte du courrier électronique");
multipart.addBodyPart(messageBodyPart);
// creation et ajout de la piece jointe
messageBodyPart = new MimeBodyPart();
DataSource source = new FileDataSource("image.gif");
messageBodyPart.setDataHandler(new DataHandler(source));
messageBodyPart.setFileName("image.gif");
multipart.addBodyPart(messageBodyPart);
// ajout des éléments au mail
message.setContent(multipart);

Les classes DataSource et FileDataSource sont des classes issues du paquetage javax.activations et sont prévues pour une utilisation spécifique de l'API JavaMail. Elles permettent de gérer les différentes données de type MIME associées au courrier électronique.

Mise en oeuvre

Afin d'illustrer ces différents éléments, nous allons nous servir du projet de l'application précédente. Toutefois, le MDB MessagerieArchivageBean va cette fois-ci jouer un rôle différent. En effet, à chaque fois qu'une nouvelle photo est archivée, au lieu de l'afficher dans une fenêtre, nous allons l'envoyer par courrier électronique dans la messagerie de l'administrateur. Voici donc l'architecture correspondante :

Et voici le nouveau code du MDB :

photos.MessagerieArchivageBean.java
package photos;

import java.io.*;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Properties;
import java.util.logging.*;
import javax.activation.*;
import javax.annotation.*;
import javax.ejb.*;
import javax.jms.*;
import javax.mail.internet.*;

@MessageDriven(mappedName="JmsPointVisionneuse")
public class MessagerieArchivageBean implements MessageListener {
   
   @PostConstruct
   private void démarrer() {
      System.out.println("Démarrage Bean Message");
   }
   
   @PreDestroy
   private void fin() {
      System.out.println("Bean Message terminé");
   }
  
   public void onMessage(Message message) {
      try {
         MapMessage msg = (MapMessage) message;
         String motif = "{0, date, full} - {0, time, medium} : {1} : {2}";
         Date date = new Date(msg.getLong("date"));
         String commande = msg.getString("commande");         
         String nom = msg.getString("nom");
         String affiche = MessageFormat.format(motif, date, commande, nom);
         System.out.println(affiche);
         if (commande.equals("stocker")) envoyerCourrier(affiche, nom, date);
      } 
      catch (Exception ex) {
          Logger.getLogger("global").log(Level.SEVERE, null, ex);
      }
   }
   
   private void envoyerCourrier(String affiche, String nom, Date date) throws Exception {
      Properties propriétés = new Properties();
      propriétés.put("mail.smtp.host", "smtp.orange.fr");
//     propriétés.put("mail.smtp.auth", "true");
      javax.mail.Session session = javax.mail.Session.getInstance(propriétés, null);
      javax.mail.Message courrier = new MimeMessage(session);
      courrier.setFrom(new InternetAddress("emmanuel.remy@wanadoo.fr"));
      courrier.setRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress("emmanuel.remy@wanadoo.fr"));
      courrier.setSubject("Stockage de photos");
      courrier.setSentDate(date);
      javax.mail.Multipart multipart = new MimeMultipart();
      javax.mail.BodyPart corpsCourrier = new MimeBodyPart();
      corpsCourrier.setText(affiche);
      multipart.addBodyPart(corpsCourrier);
      corpsCourrier = new MimeBodyPart();
      DataSource photo = new FileDataSource("J:/Photos/"+nom);
      corpsCourrier.setDataHandler(new DataHandler(photo));
      corpsCourrier.setFileName("J:/Photos/"+nom);
      multipart.addBodyPart(corpsCourrier);
      courrier.setContent(multipart);
      javax.mail.Transport transport = session.getTransport("smtp");
      transport.connect("smtp.wanadoo.fr", "emmanuel.remy", "mot-de-passe");
      transport.sendMessage(courrier, courrier.getAllRecipients());
      transport.close();       
   }
}

Nous voyons ici l'intérêt d'utiliser un MDB intermédiaire. Effectivement, la construction d'un mail avec une image comme pièce jointe prend pas mal de temps. Il est préférable que le bean session soit le plus rapidement opérationnel et délèque ce type de tâche à quelqu'un d'autre. Ainsi, chacun s'occupe de son propre travail. D'un côté, le bean session s'occupe de la logique métier et prend juste un tout petit peu de temps pour envoyer le message JMS qui donne les informations nécessaires. De l'autre le MDB réalise le traitement souhaité avec le temps qu'il faut pour envoyer ce mail. Rien ne presse.

 

Choix du chapitre Le service Timer

Certaines applications Java EE ont besoin de planifier des tâches pour être averties à des instants donnés. Par exemple, un service de commerce en ligne veut pouvoir :

  1. envoyer tous les ans un e-mail d'anniversaire à ses clients,
  2. afficher les statistiques mensuelles des ventes,
  3. produire des rapports toutes les nuits sur l'état du stock
  4. et rafraîchir un cache toutes les 30 secondes.

Pour ce faire, EJB 2.1 a introduit un service timer car les clients ne pouvaient pas utiliser directement l'API Thread, mais il est beaucoup moins riche que d'autres outils ou certains framework (l'utilitaire cron d'UNIX, Quartz, etc.). Il a fallut attendre EJB 3.1 pour voir apparaître une amélioration considérable de ce service, qui s'est inspiré de cron et d'autres outils reconnus. Désormais, il peut répondre à la plupart des besoins de planification :

  1. Possibilité de créer un timer par déclaration en utilisant l'annotation @Schedule ou le descripteur de déploiement associé.
  2. Enrichissement de l'interface TimerService pour créer un timer par programmation avec les mêmes fonctionnalités que par déclaration.

Le service Timer EJB est un service qui permet aux EJB de s'enregistrer pour être rappelés. Les notifications peuvent être planifiées pour intervenir à une date ou une heure données, après un certain délai ou à intervalles réguliers.

Le conteneur mémorise tous les timers et appelle la méthode d'instance appropriée lorsqu'un timer a expiré. La figure ci-dessous montre les deux étapes de l'utilisation de ce service :

L'EJB doit d'abord créer un timer (automatiquement ou explicitement) et s'enregistrer pour être rappelé, puis le service timer déclenche la méthode d'instance enregistrée de l'EJB.

Les timers sont destinés aux processus métiers longs et ils sont donc persistants par défaut, ce qui signifie qu'ils survivent aux arrêts du serveur : lorsqu'il redémarre, les timers s'exécutent comme s'il ne s'était rien passé. Selon vos besoins, vous pouvez également demander des timers non persistants.

Le service timer peut enregistrer des beans sans état et singletons ainsi que des MDB, mais pas de beans avec état. Ces derniers ne doivent donc pas utiliser l'API de planification.

  1. Les timers peuvent être créés automatiquement par le conteneur au moment du déploiement si le bean comprend des méthodes annotées par @Schedule.
  2. Ils peuvent également être créés explicitement par programme et doivent fournir une méthode de rappel annotée par @Timeout.

Expressions calendaires

Le service Timer utilise une syntaxe calendaire inspirée de celle du programme cron d'UNIX. Cette syntaxe est utilisée pour la création des timers par programme (avec la classe ScheduleExpression) ou pour les créations automatiques (via l'annotation @Schedule ou le descripteur de déploiement associé).

Attribut Description Valeurs possibles Valeur par défaut
second Une ou plusieurs secondes dans une minute. [0, 59] 0
minute Une ou plusieurs minutes dans une heure. [0, 59] 0
hour Une ou plusieurs heures dans une journée. [0, 23] 0
dayOfMonth Un ou plusieurs jours dans un mois.

[1, 31] ou
{"1st", "2nd", "3rd", ... , "31st"} ou
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sad"} ou
"Last" (le dernier jour du mois) ou -x (x jour(s) avant le dernier jour du mois)

*
month Un ou plusieurs mois dans une année. [1, 12] ou
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
*
dayOfWeek Un ou plusieurs jours dans une semaine. [0, 7] ou
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sad"}
"0" et "7" signifient Dimanche
*
year Une année particulière. Une année sur quatre chiffres. *
timezone Une zone horaire particulière. Liste des zones horaires fournies par la base de données zoneinfo (ou tz) *

Chaque attribut d'une expression calendaire (second, minute, hour, etc.) permet d'exprimer les valeurs sous différentes formes. Vous pouvez, par exemple, avoir une liste de jours ou un intervalle entre deux années.

Forme Description Exemple
Une valeur simple Une valeur unique correspondant à une des valeurs possibles de l'attribut year = "2010"
month = "May"
Joker (une étoile) Toutes les valeurs possibles de l'attribut second ="*"
dayOfWeek = "*"
Une liste Représente un ensemble de valeurs possibles pour l'attribut séparées par une virgule year = "2008,2012,2016"
dayOfWeek = "Sat,Sun"
minute = "0-10,30,40"
Une plage Représente une plage de valeurs consécutives possibles pour l'attribut dont les deux bornes sont séparées par un tiret

second = "1-10"
dayOfWeek = "Mon-Fri"

Une Incrémentation Définie une expression de la forme x/y où la valeur est incrémentée de y dans la plage de valeurs possibles en commençant à la valeur x. Elle ne peut être appliquée que sur heure, minute et seconde. Une fois la valeur maximale atteinte, l'incrémentation s'arrête minute = "*/15"
second = "30/10"

Cette syntaxe devrait sembler familière à ceux qui connaissent cron, mais elle est bien plus simple. Comme le montre le tableau ci-dessous, elle permet d'exprimer quasiment n'importe quel type d'expression calendaire.

Exemple Expression
Tous les mercredis à minuit dayOfWeek = "Wed"
Tous les mercredis à minuit second ="0", minute = "0", hour = "0", dayOfMonth = "*", month = "*", dayOfWeek = "Wed", year = "*"
Tous les jours de la semaine à 6:55 minute = "55", hour = "6", dayOfWeek = "Mon-Fri"
Tous les jours de la semaine à 6:55 heure de Paris

minute = "55", hour = "6", dayOfWeek = "Mon-Fri", timezone = "Europe/Paris"

Toutes les minutes minute = "*", hour = "*"
Toutes les cinq minutes minute = "*/5", hour = "*"
Toutes les cinq minutes minute = "0,5,10,15,20,25,30,35,40,45,50,55", hour = "*"
Tous les lundis et vendredis, 30 secondes après midi second ="30", hour = "12", dayOfWeek = "Mon, Fri"
Le dernier lundi de décembre à 15 h 00 hour = "15", dayOfMonth = "Last Mon", month = "Dec"
Trois jour avant le dernier jour de chaque mois à 13 h 00 hour = "13", dayOfMonth = "-3"
Toutes les deux heures à partir de midi le second mardi de chaque mois hour = "12/2", dayOfMonth = "2nd Tue"
Toutes les 14 minutes de 1 h 00 et 2 h 00 minute = "*/14", hour = "1,2"
Toutes les 14 minutes de 1 h 00 et 2 h 00 minute = "0,14,28,42,56", hour = "1,2"
Toutes les 10 secondes à partir de la 30e seconde second = "30/10"
Toutes les 10 secondes à partir de la 30e seconde second = "30,40,50"
Le premier de chaque mois à 6 heure du matin hour = "6", dayOfMonth = "1"
Du lundi au vendredi à 10 heure du soir hour = "22", dayOfWeek = "Mon-Fri"
Tous les vendredis à 22 heure 30 minute = "30", hour = "22", dayOfWeek = "Fri"
Du lundi au vendredi à 10, 14 et 18 heure hour = "10, 14, 18", dayOfWeek = "Mon-Fri"
Toutes les heures de chaque lundi hour = "*", dayOfWeek = "1"
Le dernier vendredi de chaque mois à 23 heure hour = "23", dayOfMonth = "Last Fri", month = "*"
Trois jours avant la fin de chaque mois à 22 heure hour = "22", dayOfMonth = "-3"
Tous les quarts d'heure à partir de midi minute = "*/15", hour = "12/1"

Création automatique d'un timer

Le conteneur peut créer automatiquement les timers au moment du déploiement en utilisant les métadonnées. Il crée un timer pour chaque méthode annotée par @java.ejb.Schedule ou @Schedule (ou leur équivalent XML dans le descripteur de déploiement ejb-jar.xml). Par défaut, chaque annotation @Schedule correspond à un seul timer persistant, mais il est également possible de définir des timers non persistants.

Le code source suivant montre un bean Statistiques qui définit plusieurs méthodes :
  1. statistiquesDesSoldes() : crée un timer qui appelera la méthode le premier jour de chaque mois à 5 h 30 ;
  2. générerRapport() : crée deux timers (avec @Schedules) : l'un pour chaque jour à 02 h 00, l'autre chaque mercredi à 14 h 00 ;
  3. rafraîchirCache() : crée un timer non persistant qui rafraîchit le cache toutes les 10 minutes.
Statistiques.java
import javax.ejb.*;

@Stateless
public class Statistiques  {

   @Schedule(dayOfMonth="1", hour="5", minute="30")
   public void statistiquesDesSoldes() {
   	...   
   }
   
   @Schedules({
      @Schedule(hour="2"),
      @Schedule(hour="14", dayOfWeek="Wed")
   })
   public void générerRapport() {
   	...   
   }
   
   @Schedule(minute="*/10", hour="*", persistent=false)
   public void rafraîchirCache() {
   	...   
   }
}

Voici un autre exemple :

TraitementsPériodiques.java
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.*;
import javax.annotation.*;
import javax.ejb.*;

@Stateless
@LocalBean
public class TraitementsPériodiques {
   private DateFormat info = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);

   @Schedule(dayOfWeek="Mon")
   public void traiterHebdomadaires() {
      Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO,"Execution du traitement hebdomadaire");
   }

   @Schedule(minute="*/1", hour="*")
   public void traiterMinutes() {
      Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO,"Execution du traitement chaque minute "+info.format( new Date()));
   }

   @Schedule(second="*/30", minute="*", hour="*")
   public void traiterTrenteSecondes() {
      Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO,"Execution du traitement toutes les 30 secondes "+info.format(new Date()));
   }
}
Résultat

INFO: Execution du traitement chaque minute 31 janv. 2010 16:50:00
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:50:00
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:50:30
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:51:00
INFO: Execution du traitement chaque minute 31 janv. 2010 16:51:00
INFO: Execution du traitement toutes les 30 secondes 31 janv. 2010 16:51:30

L'annotation @Schedule possède un attribut info qui permet de fournir une description du timer. Ces informations peuvent être retrouvées grâce à la méthode getInfo() de l'instance de type Timer.

Création d'un timer par programme

Pour créer un timer par programme, l'EJB doit accéder à l'interface javax.ejb.TimerService en utilisant soit l'injection de dépendances, soit l'EJBContext (EJBContext.getTimerService()), soit une recherche JNDI. L'API TimerService définit plusieurs méthodes permettant de créer quatre sortes de timer :

  1. createTimer() : crée un timer reposant sur des dates, des intervalles ou des durées. Ces méthodes n'utilisent pas les expressions calendaires.
  2. createSingleActionTimer() : crée un timer simple-action qui expire à un instant donné ou après une certaine durée. Le conteneur supprime le timer après l'appel à la méthode de rappel.
  3. createIntervalTimer() : crée un timer intervalle dont la première expiration intervient à un instant donné et les suivantes, après les intervalles indiqués.
  4. createCalendarTimer() : crée un timer utilisant les expressions calendaires à l'aide de la classe ScheduleExpression.
La classe ScheduleExpression permet de créer des expressions calendaires par programme. Ses méthodes sont liées aux attributs du tableau précédent et permettent de programmer tous les exemples précédents. Voici quelques exemples :

new ScheduleExpression().dayOfMonth("Mon").month("Jan");
new ScheduleExpression().second("10,30,50").minute("*/5").hour("10-14");
new ScheduleExpression().dayOfWeek("1,5").timezone("Europe/Lisbon");

Toutes les méthodes de TimerService (createSingleActionTimer(), createCalendarTimer(), etc.) renvoient un objet Timer contenant des informations sur le timer créé (date de création, persistance ou non, etc.) Timer permet également à l'EJB d'annuler le timer avant son expiration.

Lorsque le timer expire, le conteneur appelle la méthode annotée par @Timeout correspondante du bean en lui passant l'objet Timer. Un bean ne peut pas posséder plus d'une méthode @Timeout.

Voici un exemple qui permet d'avoir un traitement périodique par programme :
.

TraitementsPériodiques.java
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.*;
import javax.annotation.*;
import javax.ejb.*;

@Singleton
@Startup
@LocalBean
public class TraitementsPériodiques {

   @Resource
   TimerService timerService;

   private DateFormat information = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);

   @PostConstruct
   public void creerTimer() {
      Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO,"Creation du Timer");
      ScheduleExpression scheduleExp = new ScheduleExpression().second("*/10").minute("*").hour("*");
      timerService.createCalendarTimer(scheduleExp);
   }

   @Timeout
   public void executerTraitement(Timer timer) {
      Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO, "Execution toutes les 10 secondes "+information.format(new Date()));
   }
}

Attention, les méthodes annotées avec @Timeout ne peuvent pas lever d'exceptions.
.

Dans le code suivant, lorsque GestionPersonnel ajoute un nouveau personnel au système (avec la méthode créationPersonnel(), il crée également un timer calendaire reposant sur la date de naissance de ce personnel : chaque année, le conteneur pourra ainsi déclencher un bean pour créer et envoyer un courrier électronique afin de souhaiter l'anniversaire de la personne concernée.

GestionPersonnel.java
import javax.annotation.Resource;
import javax.ejb.*;
import javax.persistence.*;

@Stateless
public class GestionPersonnel  {

   @Resource
   TimerService timerService;
   
   @PersistenceContext
   EntityManager persistance;
   
   public void créationPersonnel(Personnel p) {
      persistance.persist(p);
      ScheduleExpression anniversaire = new ScheduleExpression().dayOfMonth(p.getAnniversaire().getJourDuMois()).month(p.getAnniversaire().getMois());
      timerService.createCalendarTimer(anniversaire, new TimerConfig(p, true));
   }

   @Timeout
   public void envoiMailAnniversaire(Timer timer) {
      Personnel p = (Personnel) timer.getInfo();
      ...
   }
} 

Pour ce faire, le bean sans état doit d'abord injecter une référence au service timer (avec @Resource). La méthode créationPersonnel() stocke le personnel dans la base de données et utilise le jour et le mois de sa naissance pour créer un objet ScheduleExpression qui sert ensuite à créer un timer calendaire avec TimerConfig

L'appel à new TimerConfig(p, true) configure un timer persistant (spécifié par son paramètre true) qui passe l'objet p représentant le personnel.

Une fois le timer créé, le conteneur invoquera tous les ans la méthode @Timeout(envoiMailAnniverssaire()) en lui passant l'objet Timer. Le timer ayant été sérialisé avec l'objet p, la méthode peut y accéder en appelant getInfo().

L'interface Timer

Un objet représentant l'interface Timer visualise exactement un événement de temps et peut être utilisé, grâce à ces méthodes, pour annuler un timer, obtenir un objet sérialisable, récupérer les infos le concernant, connaître le temps restant et obtenir les références du timer suivant.

javax.ejb.Timer
void cancel()
Demande la suppression du timer et de toutes ses notifications au conteneur :
@Stateless
public class QuelconqueBean implements QuelconqueRemote {
   @Resource javax.ejb.TimerService serviceTimer;
   
   public void notification(int numéro) {
      String item = "Timer n°"+numéro;
      for(Object élément : serviceTimer.getTimers()) {
          javax.ejb.Timer timer = (Timer) élément;
          String schéduleur = (String)timer.getInfo();
          if (schéduleur.equals(item)) timer.cancel();
      }
      serviceTimer.createTimer(new Date(), item);
   }
   
   @Timeout
   private void traitement(javax.ejb.Timer timer) {
      // traitement particulier au bout des trentes jours
   }
}
long getTimeRemaining()
Obtenir le nombre de millisecondes avant la prochaine notification d'expiration du timer.
java.util.Date getNextTimeout()
Obtenir la date/heure programmée de la prochaine notification d'expiration du timer.
ScheduleExpression getSchedule()
Obtenir l'objet qui définit l'expression de planification.
TimerHandle getHandle()
Obtenir une version sérialisable du timer.
java.io.Serializable getInfo()
Obtenir les informations complémentaires fournies lors de la création du timer. Le dernier argument de toutes les méthodes TimerService.createTimer() correspond à un objet info. Il est possible de préciser la valeur que vous voulez avec le type que vous désirez, à la condition que cet objet soit sérialisable. Nous pouvons dès lors récupérer cette information au moyen de la méthode getInfo(). L'objet info est enregistré par le service de Timer et délivré ensuite au bean entreprise lorsque la méthode callback Timeout est invoquée. L'objet info peut être utilisé pour des situations bien différentes, mais il est généralement requis pour identifier un timer.
boolean isPersistent()
Déterminer si le timer est persistant ou non.
boolean isCalendar()
Déterminer si le timer est basé sur un calendrier.

TraitementsPériodiques.java
import java.text.DateFormat;
import java.util.Date;
import java.util.logging.*;
import javax.annotation.*;
import javax.ejb.*;

@Singleton
@Startup
@LocalBean
public class TraitementsPériodiques {

   @Resource
   TimerService timerService;

   private DateFormat information = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);

   @PostConstruct
   public void creerTimer() {
      Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO,"Creation du Timer");
      TimerConfig config = new TimerConfig();
      config.setInfo("données complémentaires");
      timerService.createSingleActionTimer(60000, config);
   }

   @Timeout
   public void executerTraitement(Timer timer) {
      Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO, "Execution après 60s d'attente ("+timer.getInfo()+") "+information.format(new Date()));
   }
}