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.
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.
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 :
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.
L'architecture JMS est composée de différents éléments :
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 ...).
JMS offre deux modèles de messagerie point à point et publication/abonnement ou (publication/souscription) :
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 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.
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.
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.
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.
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 |
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 |
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();
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).
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);
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 :
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. |
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, 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. |
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 :
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);
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);
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);
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);
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);
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.
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();
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 :
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) { } } }
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.
Application cliente qui envoi les photos ............................................. Application cliente qui reçoit les photos
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 :
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.
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 :
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 :
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.
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.
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.
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 : |
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 |
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 :
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 } |
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 :
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.
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 {
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.
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) où 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.
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%'");
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 :
@MessageDriven(mappedName="JmsPointVisionneuse", activationConfig={ @ActivationConfigProperty(propertyName="acknowledgeMode", propertyValue="Auto-acknowledge") }) public class MessagerieArchivageBean extends JFrame implements MessageListener {
@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.
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 (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.
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
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 :
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 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 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 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();
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();
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 :
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.
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.
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 :
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 :
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.
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.
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 |
* |
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" |
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" |
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.
import javax.ejb.*; @Stateless public class Statistiques { @Schedule(dayOfMonth= , hour= , minute= ) public void statistiquesDesSoldes() { ... } @Schedules({ @Schedule(hour= ), @Schedule(hour= , dayOfWeek= ) }) public void générerRapport() { ... } @Schedule(minute= , hour= , persistent=false) public void rafraîchirCache() { ... } }
Voici un autre exemple :
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= ) public void traiterHebdomadaires() { Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO, ); } @Schedule(minute= , hour= ) public void traiterMinutes() { Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO, +info.format( new Date())); } @Schedule(second= , minute= , hour= ) public void traiterTrenteSecondes() { Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO, +info.format(new Date())); } }
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.
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 :
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 :
.
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, ); ScheduleExpression scheduleExp = new ScheduleExpression().second( ).minute( ).hour( ); timerService.createCalendarTimer(scheduleExp); } @Timeout public void executerTraitement(Timer timer) { Logger.getLogger(TraitementsPériodiques.class.getName()).log(Level.INFO, +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.
import javax.annotation.Resource; import javax.ejb.*; import javax.persistence.*; @Stateless public class GestionPersonnel { @Resource TimerService timerService; @PersistenceContext EntityManager ; public void créationPersonnel(Personnel p) { .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().
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.
@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 } }
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, ); 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, +timer.getInfo()+") "+information.format(new Date())); } }