MokaByte 61 - Marzo 2002 
Java Message Service
La gestione dei messaggi in Java
II parte: la pratica
di
Stefano Rossini
Nello scorso numero di Mokabyte [JMS1] si parlato in generale dei sistemi MOM introducendo JMS e le relativeAPI. Questa seconda puntata dedicata interamente alla pratica e viene presentato un esempio completo di applicazione JMS

JMS in pratica
L'esempio propone un'applicazione JMS che utilizza il modello Publish/Subscribe e la modalità di ricezione asincrona. Scopo dell'applicazione è permettere lo scambio tra piu' produttori e consumatori che condividono la medesima destinazione realizzando di fatto la classica chat.



Figura 1

A tale scopo ogni client JMS si dichiara sia publisher che subscriber presso il medesimo topic diventando in grado sia di ricevere che di spedire messaggi.
Si spiega ora nel dettaglio il listato completo dell'esempio riportato alla fine dell'articolo.
La prima operazione che effettua il costruttore è creare un JNDI Context per potere reperire gli Administered Object (AO) mediante le opportune lookup (linea 66 e 69) .
Nel caso in cui si esegua il programma senza parametri da riga di comando, si ricerca un ConnectionFactory ed un Topic rispettivamente di nome MyConnectionFactory e MyTopic (linea 40). Naturalmente i nomi degli AO sono valorizzabili mediante i parametri passati da riga di comando (riga 43 e 46).
Gli AO sono risorse vendor-dependent, cioè la loro implementazione varia da provider a provider, ed è per questo motivo che non sono gestite a livello di codice. Il programmatore Java fa riferimento ai nomi logici JNDI associati alle risorse astraendosi così dai dettagli implementativi dipendenti dallo specifico server JMS.
Andando nel concreto vediamo come fare funzionare l'applicazione con diversi JMS Server prendendo come esempio la Reference Implementation di SUN, l'application server Weblogic di BEA, Fiorano MQ e Tibco Rendezvous.
Questa panoramica di Server JMS non ha la pretesa di mostrare in modo dettagliato ed esaustivo l'utilizzo dei prodotti, bensì dare un'idea delle gestioni proprietarie degli AO.
Supponendo di eseguire il programma senza parametri , quello che bisogna fare è creare sul Server gli AO opportuni, cioè le risorse "fisiche" da associare ai nomi logici MyConnectionFactory e MyTopic.

 

Gestione degli Administered Objects
Nel caso della RI [SUNRI] la creazione degli AO si traduce nell'utilizzo del tool j2eeadmin (<J2EE_HOME>/bin)eseguibile da console, specificando i flag addJmsFactory per creare i ConnectionFactory e addJmsDestination per creare le destination [EEDOC].
Riconducendoci al nostro esempio i due comandi da utilizzare, una volta avviato il J2EE (<J2EE_HOME>/bin/j2ee), sono :

j2eeadmin -addJmsFactory MyTopicConnectionFactory topic
j2eeadmin -addJmsDestination MyTopic topic

Nel caso di Weblogic versione 6 la costruzione degli AO può essere effettuata mediante la console Web.
Dopo avere avviato Weblogic, mediante browser ci si collega alla macchina server specificando l'url "http://<SERVER>:7001/console" ed autenticandosi con nome utente system e come password la stessa usata per l'installazione (di default è weblogic).


Figura 2 (clicca per l'ingrandimento)

A questo punto bisogna configurare un JMS Server (Fig.3.A) , una sua destinazione (Fig.3.B) ed un ConnectionFactory(Fig.3.C). Per i dettagli delle operazioni vedere [JWLS].


Figura 3 (clicca per l'ingrandimento)

Nel caso di Fiorano Server MQ5.21,una volta avviato il server si avvia l'Admin console (passwd è la password di default) e si crea il Topic Connection Factory (figura 4) ed il Topic (figura 5); per dettagli vedere [FIORA].


Figura 4 (clicca per l'ingrandimento)

 


Figura 5 (clicca per l'ingrandimento)

Dopo avere visto come ogni vendor ha un modo proprietario di creare ed amministrare i propri AO, si mostra come procedere alla loro localizzazione.

 

Lookup degli Administered Objects
Ogni prodotto usa un proprio specifico protocollo per reperire ed utilizzare gli AO.
Mediante le API JNDI è possibile utilizzare diverse tipologie di protocolli a parità di codice Java.
Da programma basta invocare il metodo lookup sull'oggetto jndiContext di classe javax.naming.Context specificando il nome logico della risorsa da ricercare ed effettuare l'opportuno downcast.

TopicConnectionFactory connectionFactory =
(TopicConnectionFactory)jndiContext.lookup("MyDestiantionFactory");
Topic dest = (Topic) jndiContext.lookup("MyDest");

Per reperire opportunamente gli AO bisogna specificare l'URL del Service Provider (il JMS server) e la classe che fornisce l'implementazione del protocollo.
Questi informazioni sono definite come proprietà di sistema, e piu' precisamente per quanto riguarda il protocollo bisogna configurare la proprietà di sistema java.naming.factory.initial (javax.naming.Context.INITIAL_CONTEXT_FACTORY), mentre per il provider URL la proprietà java.naming.provider.url (javax.naming.Context.PROVIDER_URL).
Di seguito si riportano i valori delle proprietà per poter comunicare con i JMS Server precedentemente presentati.

Al posto di <SERVER> bisogna specificare il nome o l'indirizzo IP del JMS Server (ad esempio localhost o 127.0.0.1 nel caso in cui il JMS Server sia installato sulla macchina locale).
La valorizzazoine di queste due proprietà può avvenire da codice, da riga di comando oppure da file di properties. [CEJB]
Da codice è possibile referenziare le proprietà mediante le proprietà pubbliche e statiche della classe javax.naming.Context : Context.INITIAL_CONTEXT_FACTORY e Context.PROVIDER_URL.
Ad esempio nel caso di Weblogic il seguente codice permette di utilizzare il protocollo proprietario BEA denominato T3, la cui implementazione è fornita dalla classe di nome weblogic.jndi.WLInitialContextFactory interrogando il server di nome manowar in ascolto sulla porta TCP 7001 :

java.util.Properties properties = new java.util.Properties();
properties.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
properties.put(Context.PROVIDER_URL, "t3://manowar:7001");
Context jndiContext = new InitialContext(properties);

Piuttosto che valorizzare il Context factory ed il Provider URL da codice si può effettuare la configurazione mediante il flag -D da riga di comando in fase di lancio dell'interprete java :

-Djava.naming.factory.initial=weblogic.jndi.WLInitialContextFactory
-Djava.naming.provider.url=t3://<SERVER>:7001

In questo caso nel codice si istanzia il Context mediante il costruttore di default della classe InitialContext come avviene nell'esempio alla linea 143.

Context jndiContext = new InitialContext();

Oltre a valorizzare le proprietà JNDI bisogna indicare nel classpath i jar contenenti le classi che forniscono sia l'implementazione del ContextFactory che del protocollo di comunicazione.
Nel caso di Weblogic nel classpath bisogna indicare il weblogic.jar che contiene sia le classi JMS che quelle del driver di comunicazione che implementa il protocollo T3 :

%JAVA_HOME%\bin\java -cp .;classes;
%BEA_HOME%\wlserver6.1\lib\weblogic.jar
-Djava.naming.factory.initial=weblogic.jndi.WLInitialContextFactory
-Djava.naming.provider.url=t3://<SERVER>:7001
it.mokabyte.jms.chat.JmsChatter

Tale commando va specificato su un'unica riga e al posto di Server va specificato o il nome o l'indirizzo IP del Server JMS.
Nel caso di TIBCO le classi JMS che del driver di comunicazione sono contenute in 3 diversi jar che devono essere referenziati nel classpath :

%JAVA_HOME%\bin\java
-cp .;..\classes;%J2EE_HOME%\lib\j2ee.jar;
tibjmsnaming.jar;tibjmsadmin.jar;tibjms.jar

-Djava.naming.factory.initial=
      com.tibco.tibjms.naming.TibjmsInitialContextFactory
-Djava.naming.provider.url=
      tibjmsnaming://<SERVER>:7222
it.mokabyte.jms.chat.JmsChatter

Nel caso di FIORANO le classi sono contenute in un unico file in formato zip :

set CLASSPATH
%JAVA_HOME%\bin\java -cp .;..\classes;
..\libjms\fiorano\fmprtl.zip
-Djava.naming.factory.initial=
    fiorano.jms.runtime.naming.FioranoInitialContextFactory
-Djava.naming.provider.url=http://<SERVER>:1856
it.mokabyte.jms.chat.JmsChatter

Nel caso Sun RI 1.3.01 è possibile utilizzare le proprietà CORBA :

set CLASSPATH=.;..\classes;
%J2EE_HOME%\lib\j2ee.jar
%JAVA_HOME%\bin\java
-cp %CLASSPATH%
-Dorg.omg.CORBA.ORBInitialHost=<SERVER>
-Dorg.omg.CORBA.ORBInitialPort=1050
it.mokabyte.jms.pubsub.JmsChatter

La chat JMS
Costruiti e localizzati gli AO, passiamo ora ad analizzare il codice dell'applicazione indipendente dalla tipologia di JMS server.
Alla linea 92 si procede a costruire l'interfaccia grafica (metodo buildGUI - linea 155) dell'applicazione che è costituita da due bottoni, una text field ed una text area.
La text field ha il compito di permettere l'inserimento del messaggio da trasmettere, il bottone bSend gestisce l'invio del messaggio mentre la text area visualizza il testo dei messaggi ricevuti.
Alla linea 95 viene creata una connessione con il JMS Server invocando sull'oggetto topicConnectionFactory il metodo createTopicConnection().
Dall'oggetto topicConnection viene creata una sessione mediante il metodo createTopicSession che ha come parametri in ingresso un boolean, per specificare se la sessione deve essere gestita in un contesto transazionale, e un intero per indicare la modalità di conferma ricezione del messaggio :

TopicSession topicSession =
topicConnection.createTopicSession(false,Session.AUTO_ACKNOWLEDGE);

Se si specifica true come primo parametro la conferma ricezione (acknowledge) dei messaggi è automatica e a carico del JMS Server.
Se invece è false ci sono tre diverse possibilità di gestione della conferma della ricezione del messaggio mediante il valore del secondo parametro :

  • AUTO_ACKNOWLEDGE : la sessione effettua l'acknowledge automatico della ricezione del messaggio senza però prevenire la possibilità di evitare la ricezione di messaggi duplicati. Questa opzione è tra le tre la piu' "leggera" in termini di overhead di protocollo, però è utilizzabile solo laddove la ricezione di messaggi duplicati non comporti problematiche applicative.
  • DUPS_OK_ACKNOWLEDGE : la sessione effettua l'acknowledge automatico della ricezione del messaggio una volta effettivamente consumato dal ricevitore (alla fine del metodo receive nel modo sincrono, alla fine del metodo onMessage nel modo asincrono).
  • CLIENT_ACKNOWLEDGE : il client è responsabile della conferma della ricezione dei messaggi mediante l'uso del metodo acknowledge() sugli oggetti Message.


Successivamente si hanno le istruzioni che dichiarano l'applicazione interessata a ricevere i messaggi dal topic (sottoscrizione).
Per prima cosa si costruisce l'oggetto listener di classe TextListener (linea 113) per poi invocare sull'oggetto TopicSession il metodo createSubscriber,passando come parametro la destinazione d'interesse, al fine di ottenere il TopicSubscriber :

topicSubscriber = topicSession.createSubscriber(topic);

Nel nostro caso siamo interessati a essere sia publisher che subscriber del topic e questo implica che tutti i messaggi che invieremo verranno anche recapitati a noi stessi.
Per evitare questo effetto di "eco" (nel caso di una chat alquanto indesiderato), basta utilizzare il metodo nella versione overload che prevede oltre al topic, una String ed un boolean :

public TopicSubscriber createSubscriber(Topic topic,java.lang.String messageSelector, boolean noLocal) throws JMSException

Specificando come terzo parametro (noLocal) il valore false, si inibisce il recapito dei messaggi pubblicati dal client medesimo.

topicSubscriber = topicSession.createSubscriber(topic,null,true);

Nel caso in cui si volesse creare un filtro per indicare le condizioni che devono essere rispettate affinché il pacchetto venga recapitato, basta specificare come secondo parametro il Message Selector da utilizzare.
Prendendo in considerazione un esempio sportivo, si potrebbe creare una chat tra i soli tifosi della medesima squadra specificando come MessageSelector una proprietà (ad esempio di nome TEAM) avente come valore il nome della propria squadra preferita :

topicSubscriber =
topicSession.createSubscriber(topic,"TEAM='Pittsburgh Steelers'",true);

Una volta creato il topicSubsriber, mediante il metodo setMessageListener si registra il topic listener di classe MyTextListener (linea 114).
La classe TextListener implementa l'interfaccia javax.jms.MessageListener (linea 227) ridefinendo il metodo onMessage (linea 243) che si limita a stampare nella TextArea il messaggio ricevuto nel caso sia di classe TextMessage (linea 248,249 e 253).
Manca infine da spiegare come avviene la trasmissione dei messaggi. Premendo il bottone bSend, viene invocato il metodo actionPerformed (linea 177) della classe anonima (linea 176) adibita alla gestione dell'evento. Tale metodo valorizza l'oggetto TextMessage mediante il metodo setText (linea 179) e provvede al suo invio (linea 180) mediante il metodo publish invocato sull'oggetto topicPublisher.
Se si volesse introdurre un Message Selector, prima dell'invio del messaggio bisognerebbe utilizzare il metodo setStringProperty per specificare il valore del filtro.
Riferendosi nuovamente all'esempio sportivo, questo significherebbe utilizzare il metodo setProperty prima di inviare il messaggio specificando il nome ed il realtivo valore del property field:

message.setStringProperty("TEAM","'Pittsbugh Steelers'");

 

Codice dell'esempio
Il codice dell'esempio è costituito da due classi : il client JMS (JmsChatter) e la classe listener adibita alla ricezione asincrona dei messaggi (MyTextListener) di seguito riportate :

La classe JmsChatter

1 package it.mokabyte.jms.pubsub;
2
3 import javax.jms.*;
4 import javax.naming.*;
5 import java.io.*;
6 import java.awt.*;
7 import java.awt.event.*;
8 import javax.swing.*;
9
10 /**
11 * Class : JmsChatter
12 * Descrizione : Applicazione JMS Publisher/Subscriber
13 * @version 1.0 01/02/2002
14 * @author Stefano Rossini
15 */
16 public class JmsChatter {
17
18 /* Componenti interfaccia grafica */
19 private JFrame frame = null;
20 private JButton bClear = null;
21 private JButton bSend = null;
22 private JTextField tfTx = null;
23 private JTextArea taRx = null;
24
25 /** Il nome dell'utente */
26 private String userName = null;
27 /** La connesione JMS */
28 private TopicConnection topicConnection = null;
29 /** Il Message sender */
30 private TopicPublisher topicPublisher = null;
31 /** Il messaggio */
32 private TextMessage message = null;
33
34 /**
35 * @param args parametri da riga di comando
36 */
37 public static void main(String[] args) {
38
39 if(args.length == 0) {
40 new JmsChatter("MyTopicConnectionFactory","MyTopic");
41 }
42 else if ( (args.length == 1)) {
43 new JmsChatter(args[0],"MyTopic");
44 }
45 else if ( (args.length == 2)) {
46 new JmsChatter(args[0],args[1]);
47 }
48 else {
49 System.out.println("Usage:[<topic-connection-factory>
                       <topic-name>]");
50 }
51 }
52
53 /**
54 * Costruttore
55 * @param topicConnectionFactoryName Connection Factory name
56 * @param topicName Destination name
57 */
58 public JmsChatter(String topicConnectionFactoryName, String topicName)
59 {
60 try {
61 Context jndiContext = this.getContext();
62 System.out.println("JNDI Context["+jndiContext+"]...");
63
64 // Look up connection ConnectionFactory e detination Topic
65 System.out.println("Looking up connection factory ["+
topicConnectionFactoryName+"]...");
66 TopicConnectionFactory topicConnectionFactory =          (TopicConnectionFactory)
          jndiContext.lookup(topicConnectionFactoryName);
67
68 System.out.println("Looking up topic ["+topicName+"]...");
69 Topic topic = (Topic) jndiContext.lookup(topicName);
70
71 System.out.println("topicConnectionFactory [" +
topicConnectionFactory + "] - topic [" + topic + "]");
72 this.go(topicConnectionFactory,topic);
73
74 } catch (NamingException ne) {
75 System.out.println(" # NamingException : " + ne.toString());
76 ne.printStackTrace();
77 System.exit(1);
78 }
79 }
80
81 /**
82 * Configurazione dell'applicazione (publisher e subscriber del Topic)
83 * @param topicConnectionFactory l'oggetto Connection Factory
84 * @param topic l'oggetto destinazione Topic
85 */
86 private void go(TopicConnectionFactory topicConnectionFactory,
                   Topic topic){
87
88
89 try {
90 this.readUserName(); //legge il nome dell'utente da stdin
91
92 this.buildGUI(); //costruisce la GUI
93
94 // JMS
95 this.topicConnection =
topicConnectionFactory.createTopicConnection();
96 System.out.println("topicConnection ["+topicConnection+"]");
97
98 TopicSession topicSession =
topicConnection.createTopicSession(false,Session.AUTO_ACKNOWLEDGE);
99 System.out.println("topicSession ["+topicSession+"]");
100
101 // Si crea l'oggetto message
102 this.message = topicSession.createTextMessage();
103
104 // Parte di publisher
105 this.topicPublisher = topicSession.createPublisher(topic);
106 System.out.println("topicPublisher ["+topicPublisher+"]");
107
108 // Parte di Subscriber
109 TopicSubscriber topicSubscriber =
topicSession.createSubscriber(topic,null,true);
110 System.out.println("topicSubscriber ["+topicSubscriber+"]");
111
112 System.out.println("Creazione e
                       installazione del listener ...");
113 MyTextListener topicListener = new MyTextListener(taRx);
114 topicSubscriber.setMessageListener(topicListener);
115 topicConnection.start();
116 System.out.println("Inizializzazione JMS completata ...");
117
118 } catch (JMSException e) {
119 System.out.println(" # JMSException : " + e.toString());
120 e.printStackTrace();
121 System.exit(0);
122 }
123 }
124
125 /**
126 * Legge da tastiera il nome dell'utente
127 */
128 private void readUserName() {
129 try{
130 BufferedReader msgStream = new BufferedReader (new
InputStreamReader(System.in));
131 System.out.print("Inserisci il tuo nome : ");
132 this.userName = msgStream.readLine();
133 } catch(IOException ioe) {
134 this.userName = "";
135 }
136 }
137
138 /**
139 * Crea e ritorna il JNDI Context
140 * @return il JNDI Context creato
141 */
142 private Context getContext() throws NamingException {
143 Context ctx = new InitialContext();
144 // Print Context envinroment properties
145 java.util.Hashtable ht = ctx.getEnvironment();
146 java.util.Enumeration enum = ht.keys();
147 while(enum.hasMoreElements()) {
148 String str = (String)enum.nextElement();
149 System.out.println("\tProp["+str + "] -
                       value[" + ht.get(str)+"]");
150 }
151 return ctx;
152 }
153
154 /** Costruisce l'interfaccia grafica */
155 public void buildGUI()
156 {
157 // Costruzione del Frame
158 frame = new JFrame("JMS Chat : utente[" + userName + "]");
159 frame.getContentPane().setLayout(new BorderLayout());
160
161 // inizializzazione degli oggetti della GUI
162 tfTx = new JTextField();
163 taRx = new JTextArea();
164 taRx.setEditable(false);
165 bClear = new JButton("CLEAR");
166 bSend = new JButton("SEND");
167
168 // disposizione dei componenti grafici
169 JPanel p = new JPanel();
170 p.setLayout(new BorderLayout());
171 p.add(bClear, BorderLayout.WEST);
172 p.add(tfTx, BorderLayout.CENTER);
173 p.add(bSend, BorderLayout.EAST);
174
175 // il bottone send invia il messaggio contenuto nel TextField
176 bSend.addActionListener(new java.awt.event.ActionListener() {
177 public void actionPerformed(ActionEvent e) {
178 try{
179 message.setText("["+userName+"] " + tfTx.getText());
180 topicPublisher.publish(message);
181 System.out.println("Message["+tfTx.getText()+"] sent ...");
182 } catch(JMSException jmse) {
183 tfTx.setText(jmse.getMessage());
184 jmse.printStackTrace();
185 }
186 }
187 });
188
189 // il bottone clear ripulisce il TextField
190 bClear.addActionListener(new java.awt.event.ActionListener() {
191 public void actionPerformed(ActionEvent e) {
192 tfTx.setText("");
193 }
194 });
195
196 // l'evento di chiusura della finestra genera la chiusura
197 // della connessione JMS
198 frame.addWindowListener(new WindowAdapter() {
199 public void windowClosing(WindowEvent e) {
200 System.out.println("windowClosing : JMS clean-up code...");
201 if (topicConnection != null) {
202 try {
203 topicConnection.close();
204 } catch (JMSException jmse) {}
205 }
206 System.exit(0);
207 }
208 });
209
210 frame.getContentPane().add(p, BorderLayout.NORTH);
211 JScrollPane sc = new JScrollPane(taRx);
212 frame.getContentPane().add(sc, BorderLayout.CENTER);
213
214 Dimension screenSize =
Toolkit.getDefaultToolkit().getScreenSize();
215 frame.setSize(screenSize.width / 2,screenSize.height / 2);
216 Dimension frameSize = frame.getSize();
217 frame.setLocation((screenSize.width -
                  frameSize.width) / 2,
                  (screenSize.height - frameSize.height) / 2);
218
219 frame.setVisible(true);
220 }
221}
222

La classe MyTextListener

223  /**
224  * Class : MyTextListener
225  * Descrizione : la classe listener della destinazione topic
226  */
227  class MyTextListener implements MessageListener {
228
229  private javax.swing.JTextArea textArea = null;
230
231  /**
232  * Costruttore
233  * @param ta TextArea visualizza contenuto
       dei messaggi ricevuti
234  */
235  public MyTextListener(javax.swing.JTextArea ta) {
236     this.textArea = ta;
237  }
238
239  /**
240   * Effettua il cast del messaggio e ne visualizza il contenuto
241   * @param message il messaggio ricevuto
242   */
243  public void onMessage(Message message) {
244  
245     TextMessage msg = null;
246
247     try {
248      if (message instanceof TextMessage) {
249        msg = (TextMessage) message;
250        // stampo a video
251        System.out.println(new java.util.Date().toString() +
           "Ricevuto messaggio : "+ msg.getText());
252        // stampo nella TextArea
253        this.textArea.append(new java.util.Date().toString()+
                   ":" + msg.getText() +
                   System.getProperty("line.separator"));
254       } else {
255         System.out.println("TextListener.onMessage : Message of             wrong type: " + message.getClass().getName());
256       }
257     } catch (JMSException e) {
258         System.out.println(" # TextListener.onMessage :             JMSException in onMessage(): " + e.getMessage());
259         e.printStackTrace();
260     } catch (Throwable te) {
261         System.out.println(" # TextListener.onMessage :             Exception in onMessage():" + te.getMessage());
262         te.printStackTrace();
263    }
264  }
265 }

Conclusioni
Con questo esempio si è voluto mostrare l'utilizzo delle API JMS mettendo in pratica i concetti esposti nel precedente articolo.

 

Bibliografia

[JMS1] S.Rossini - "JMS -La gestione dei messaggi", Mokabyte N.60, Febbraio 2002
[CEJB] S.Rossini - "Client EJB ", Mokabyte N53, Giugno 2001
[JMST] JMS tutorial - http://java.sun.com/products/jms/tutorial/index.html
[EEDOC] J2SDKEE Documentation
[JMSS] JMS Specification (versione 1.0.2b) - http://java.sun.com/products/jms/docs.html
[BWLS] P. Gomez, P. Zadrozny - "Java 2 Enterprise Editino with BEA Weblogic Server", Wrox 2001
[JWLS] Weblogic JMS DOC : http://e-docs.bea.com/wls/docs60/jms/intro.html
[CPJMS] P. Campanelli - "Java Message Service", Computer Programmino N. 105
[SUNRI] http://java.sun.com/j2ee/download.html#sdk
[TIBCO] http://www.tibco.com/products/jms/enterprise_for_jms.html
[FIORA] http://www.fiorano.com/best_cgi/frame.cgi?target=developer/documentation_home.htm

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it