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
|