Home   |   Archivio   |   Chi siamo   |   Contatti
Stefano Rossini
Java Message Service
La gestione dei messaggi in Java - II parte: la pratica
di
Stefano Rossini
Nello scorso numero di Mokabyte si è parlato in generale dei sistemi MOM introducendo JMS e le relative API. 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 - Architettura dell'applicazione JMS Chat
Figura 1 - Architettura dell'applicazione JMS Chat

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 - Console Web di Weblogic
Figura 2 - Console Web di Weblogic

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 - Configurazione JMS in Weblogic
Figura 3 - Configurazione JMS in Weblogic

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 - Fiorano Topic Connection Factory
Figura 4 - Fiorano Topic Connection Factory
Figura 5 - Fiorano Topic Configuration
Figura 5 - Fiorano Topic Configuration

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.INITIALCONTEXTFACTORY), 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.

Tabella 1 - Configurazione JNDI per diversi JMS Server
Tabella 1 - Configurazione JNDI per diversi JMS Server

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 valorizzazione 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.INITIALCONTEXTFACTORY 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.
  • DUPSOKACKNOWLEDGE: 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

package it.mokabyte.jms.pubsub;

import javax.jms.*;
import javax.naming.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
* Class : JmsChatter
* Descrizione : Applicazione JMS Publisher/Subscriber
* @version 1.0 01/02/2002
* @author Stefano Rossini
*/
public class JmsChatter {

/* Componenti interfaccia grafica */
private JFrame frame = null;
private JButton bClear = null;
private JButton bSend = null;
private JTextField tfTx = null;
private JTextArea taRx = null;

/** Il nome dell'utente */
private String userName = null;
/** La connesione JMS */
private TopicConnection topicConnection = null;
/** Il Message sender */
private TopicPublisher topicPublisher = null;
/** Il messaggio */
private TextMessage message = null;

/**
* @param args parametri da riga di comando
*/
public static void main(String[] args) {

if(args.length == 0) {
new JmsChatter("MyTopicConnectionFactory","MyTopic");
}
else if ( (args.length == 1)) {
new JmsChatter(args[0],"MyTopic");
}
else if ( (args.length == 2)) {
new JmsChatter(args[0],args[1]);
}
else {
System.out.println("Usage:[<topic-connection-factory>
                       <topic-name>]");
}
}

/**
* Costruttore
* @param topicConnectionFactoryName Connection Factory name
* @param topicName Destination name
*/
public JmsChatter(String topicConnectionFactoryName, String topicName)
{
try {
Context jndiContext = this.getContext();
System.out.println("JNDI Context["+jndiContext+"]...");

// Look up connection ConnectionFactory e detination Topic
System.out.println("Looking up connection factory ["+
topicConnectionFactoryName+"]...");
TopicConnectionFactory topicConnectionFactory =         (TopicConnectionFactory)
         jndiContext.lookup(topicConnectionFactoryName);

System.out.println("Looking up topic ["+topicName+"]...");
Topic topic = (Topic) jndiContext.lookup(topicName);

System.out.println("topicConnectionFactory [" +
topicConnectionFactory + "] - topic [" + topic + "]");
this.go(topicConnectionFactory,topic);

} catch (NamingException ne) {
System.out.println(" # NamingException : " + ne.toString());
ne.printStackTrace();
System.exit(1);
}
}

/**
* Configurazione dell'applicazione (publisher e subscriber del Topic)
* @param topicConnectionFactory l'oggetto Connection Factory
* @param topic l'oggetto destinazione Topic
*/
private void go(TopicConnectionFactory topicConnectionFactory,
                   Topic topic){


try {
this.readUserName(); //legge il nome dell'utente da stdin

this.buildGUI(); //costruisce la GUI

// JMS
this.topicConnection =
topicConnectionFactory.createTopicConnection();
System.out.println("topicConnection ["+topicConnection+"]");

TopicSession topicSession =
topicConnection.createTopicSession(false,Session.AUTO_ACKNOWLEDGE);
System.out.println("topicSession ["+topicSession+"]");

// Si crea l'oggetto message
this.message = topicSession.createTextMessage();

// Parte di publisher
this.topicPublisher = topicSession.createPublisher(topic);
System.out.println("topicPublisher ["+topicPublisher+"]");

// Parte di Subscriber
TopicSubscriber topicSubscriber =
topicSession.createSubscriber(topic,null,true);
System.out.println("topicSubscriber ["+topicSubscriber+"]");

System.out.println("Creazione e
                       installazione del listener ...");
MyTextListener topicListener = new MyTextListener(taRx);
topicSubscriber.setMessageListener(topicListener);
topicConnection.start();
System.out.println("Inizializzazione JMS completata ...");

} catch (JMSException e) {
System.out.println(" # JMSException : " + e.toString());
e.printStackTrace();
System.exit(0);
}
}

/**
* Legge da tastiera il nome dell'utente
*/
private void readUserName() {
try{
BufferedReader msgStream = new BufferedReader (new
InputStreamReader(System.in));
System.out.print("Inserisci il tuo nome : ");
this.userName = msgStream.readLine();
} catch(IOException ioe) {
this.userName = "";
}
}

/**
* Crea e ritorna il JNDI Context
* @return il JNDI Context creato
*/
private Context getContext() throws NamingException {
Context ctx = new InitialContext();
// Print Context envinroment properties
java.util.Hashtable ht = ctx.getEnvironment();
java.util.Enumeration enum = ht.keys();
while(enum.hasMoreElements()) {
String str = (String)enum.nextElement();
System.out.println("\tProp["+str + "] -
                      value[" + ht.get(str)+"]");
}
return ctx;
}

/** Costruisce l'interfaccia grafica */
public void buildGUI()
{
// Costruzione del Frame
frame = new JFrame("JMS Chat : utente[" + userName + "]");
frame.getContentPane().setLayout(new BorderLayout());

// inizializzazione degli oggetti della GUI
tfTx = new JTextField();
taRx = new JTextArea();
taRx.setEditable(false);
bClear = new JButton("CLEAR");
bSend = new JButton("SEND");

// disposizione dei componenti grafici
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.add(bClear, BorderLayout.WEST);
p.add(tfTx, BorderLayout.CENTER);
p.add(bSend, BorderLayout.EAST);

// il bottone send invia il messaggio contenuto nel TextField
bSend.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
try{
message.setText("["+userName+"] " + tfTx.getText());
topicPublisher.publish(message);
System.out.println("Message["+tfTx.getText()+"] sent ...");
} catch(JMSException jmse) {
tfTx.setText(jmse.getMessage());
jmse.printStackTrace();
}
}
});

// il bottone clear ripulisce il TextField
bClear.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(ActionEvent e) {
tfTx.setText("");
}
});

// l'evento di chiusura della finestra genera la chiusura
// della connessione JMS
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.out.println("windowClosing : JMS clean-up code...");
if (topicConnection != null) {
try {
topicConnection.close();
} catch (JMSException jmse) {}
}
System.exit(0);
}
});

frame.getContentPane().add(p, BorderLayout.NORTH);
JScrollPane sc = new JScrollPane(taRx);
frame.getContentPane().add(sc, BorderLayout.CENTER);

Dimension screenSize =
Toolkit.getDefaultToolkit().getScreenSize();
frame.setSize(screenSize.width / 2,screenSize.height / 2);
Dimension frameSize = frame.getSize();
frame.setLocation((screenSize.width -
                  frameSize.width) / 2,
                  (screenSize.height - frameSize.height) / 2);

frame.setVisible(true);
}
}

La classe MyTextListener

/**
* Class : MyTextListener
* Descrizione : la classe listener della destinazione topic
*/
class MyTextListener implements MessageListener {

    private javax.swing.JTextArea textArea = null;

    /**
    * Costruttore
    * @param ta TextArea visualizza contenuto
          dei messaggi ricevuti
    */
    public MyTextListener(javax.swing.JTextArea ta) {
        this.textArea = ta;
    }

    /**
      * Effettua il cast del messaggio e ne visualizza il contenuto
      * @param message il messaggio ricevuto
      */
    public void onMessage(Message message) {

        TextMessage msg = null;

        try {
            if(message instanceof TextMessage) {
                msg = (TextMessage) message;
                // stampo a video
                System.out.println(new java.util.Date().toString() +
                        "Ricevuto messaggio : " + msg.getText());
                // stampo nella TextArea
                this.textArea.append(new java.util.Date().toString() +
                        ":" + msg.getText() +
                        System.getProperty("line.separator"));
            } else {
                System.out.println("TextListener.onMessage : Message of" +
                        "            wrong type: "
                        + message.getClass().getName());
            }
        } catch(JMSException e) {
            System.out.println(" # TextListener.onMessage :            " +
                        "JMSException in onMessage(): "
                        + e.getMessage());
            e.printStackTrace();
        } catch(Throwable te) {
            System.out.println(" # TextListener.onMessage :            " +
                        "Exception in onMessage():"
                        + te.getMessage());
            te.printStackTrace();
        }
    }
}

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