Una sottoscrizione durable

Altro esempio tratto dalla sezione "Robust JMS Applications" del tutorial Sun su Java EE, parte dedicata a JMS é il Durable Subscriber che trattiamo in questa serie di post.

Il punto di questo esempio é che vogliamo che il subscriber possa disconnettersi da JMS senza perdere messaggi a lui indirizzati. Usiamo perciò una connessione durable, in modo da poter creare un TopicSubscriber durable.

La gestione di basso livello per JMS, con la creazione del contesto iniziale, delle connection factory e del topic é la medesima che abbiamo già visto in precedenza, inglobata nella classe JmsConnectionManager, perciò non ne ripetiamo qui i dettagli.

MultiplePublisher

Il publisher é piuttosto semplice, usa la connessione non durable, su cui crea una sessione AUTO_ACKNOWLEDGE, il producer é un MessageProducer costruito sulla session specificando il topic che abbiamo definito precedentemente via Admin Console di GlassFish.

In pratica ci si limita a mandare un certo numero di messaggi sul topic ogni volta che viene invocato il metodo publishMessages(); ecco qui a seguire il codice della classe:

package durable;

import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;

public class MultiplePublisher {
private Connection connection = null;
private Session session = null;
private MessageProducer producer;
private JmsConnectionManager cm = JmsConnectionManager.getInstance();

private int msgBaseIndex = 0;

public MultiplePublisher() throws JMSException {
connection = cm.connFact.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

producer = session.createProducer(cm.topic);
}

public void publishMessages() {
final int NUMMSGS = 3;
final String MSG_TEXT = "This is message ";

try {
TextMessage message = session.createTextMessage();

for(int i = 1; i <= NUMMSGS; ++i) {
message.setText(MSG_TEXT + (msgBaseIndex + i));
System.out.println("Publishing message: " + message.getText());
producer.send(message);
}
msgBaseIndex += NUMMSGS;

// non-text message as end of messages
producer.send(session.createMessage());
}
catch(JMSException e) {
System.err.println("Exception in publishMessages: " + e.toString());
}
}

public void finish() {
if(connection != null) {
try {
connection.close();
}
catch(JMSException e) {}
}
}
}

DurableSubscriber

Il subscriber di questo esempio é simile al subscriber dell'esempio precedente, e ricicliamo per il suo uso le classi TextListener e Monitor, che mettono a disposizione rispettivamente il metodo onMessage() che gestisce i messaggi come indirizzati dal topic e il meccanismo per sincronizzare DurableSubscriber con il listener.

Il suo costruttore crea una connessione durable, e da questa una sessione di tipo AUTO_ACKNOWLEDGE.

La ricezione di messaggi é gestita da due metodi, startSubscriber() e closeSubscriber().

La startSubscriber() crea un durable subscriber per il topic usato dal publisher, crea un listener, di tipo TextListener - che abbiamo già trattato nel post precedente, e lo associa al subscriber. Quindi facciamo partire la connessione.

La closeSubscriber() aspetta che il listener abbia completato la ricezione dei messaggi, quindi chiude il subscriber e ferma la connessione.

Vediamo il codice della classe:

package durable;

import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Session;
import javax.jms.TopicSubscriber;

public class DurableSubscriber {
private Connection connection = null;
private Session session = null;
private TextListener listener = null;
private TopicSubscriber subscriber = null;

private JmsConnectionManager cm = JmsConnectionManager.getInstance();

public DurableSubscriber() throws JMSException {
connection = cm.drbConnFact.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
}

public void startSubscriber() throws JMSException {
System.out.println("Starting subscriber");

subscriber = session.createDurableSubscriber(cm.topic, "MakeItLast");
listener = new TextListener();
subscriber.setMessageListener(listener);
connection.start();
}

public void closeSubscriber() throws JMSException {
listener.waitTillDone();

System.out.println("Closing subscriber");
subscriber.close();
connection.stop();
}

public void finish() {
if (connection != null) {
System.out.println("Unsubscribing from durable subscription");
try {
session.unsubscribe("MakeItLast");
connection.close();
}
catch(JMSException je) {
System.out.println(je.getMessage());
}
}
}
}

Main

Il main fa partire e poi chiude due volte il subscriber. Se non fosse durable i messaggi mandati dal publisher quando il subscriber é giù andrebbero persi. Eseguendo il codice vediamo che, come ci aspettiamo, i messaggi restano sul topic in attesa che il subscriber venga fatto ripartire.

public static void main(String[] args) {
DurableSubscriber subscriber = null;
MultiplePublisher publisher = null;

try {
subscriber = new DurableSubscriber();
publisher = new MultiplePublisher();

subscriber.startSubscriber();
publisher.publishMessages();
subscriber.closeSubscriber();

publisher.publishMessages();

subscriber.startSubscriber();
subscriber.closeSubscriber();
}
catch(JMSException je) {
System.out.println("Exception: " + je.getMessage());
}
finally {
if(subscriber != null)
subscriber.finish();
if(publisher != null)
publisher.finish();
}

System.exit(0);
}

Javafx plugin per Netbeans 6.7

Finalmente disponibile il pluging per NetBeans 6.7!

Sincronizzarsi su di una coda: Subscriber/2

Ultimo passo nel completare l'esempio che abbiamo cominciato a vedere un paio di post fa, sullo scambio di messaggi tra un publisher e un subscriber per mezzo di un topic, usando una coda per sincronizzarsi.

Ci resta da vedere come implementare il listener che deve effettivamente leggere e gestire i messaggi che arrivano dal publisher per conto del subscriber.

TextListener

Abbiamo visto che il metodo receive() del nostro subscriver delega a questa classe la lettura dei messaggi, e resta in attesa, chiamando il suo metodo waitTillDone(), che l'operazione venga completata. Vediamo nel dettaglio come ciò avviene:

package jmsrobust;

import javax.jms.JMSException;
import javax.jms.MessageListener;
import javax.jms.Message;
import javax.jms.TextMessage;

public class TextListener implements MessageListener {
private Monitor monitor = new Monitor();

public void waitTillDone() {
monitor.waitTillDone();
}

public void onMessage(Message message) {
if(message instanceof TextMessage) {
TextMessage msg = (TextMessage)message;

try {
System.out.println("Processing message: " + msg.getText());
}
catch (JMSException je) {
System.err.println("Exception in onMessage(): " + je.toString());
}
}
else {
// non-text message == end of the message stream
monitor.allDone();
}
}
}

Sembra tutto molto semplice, e in effetti lo é. Il metodo onMessage(), dichiarato nell'interfaccia MessageListener che la nostra classe implementa, processa i messaggi che ci vengono passati da JMS - ricordiamo che questo listener viene associato dal metodo receive() del nostro subscriber al topic usato come canale si comunicazione - finché non si vede un messaggio non testuale che abbiamo stabilito essere la convenzione che ci indica il termine delle trasmissioni. Segnaliamo questo al monitor che usiamo allo scopo di sincronizzare il TextListener con chi lo possiede.

Vediamoci dunque come implementiamo il monitor:

package jmsrobust;

public class Monitor {
private boolean done = false;

public synchronized void waitTillDone() {
while(!done) {
System.out.println("Waiting for notification");
try {
this.wait();
}
catch (InterruptedException ie) {
System.out.println("Wait interrupted");
}
}
System.out.println("Notification received");
}

public synchronized void allDone() {
done = true;
System.out.println("allDone notification");
this.notify();
}
}

In realtà mi sembra un po' sovrabbondante definire i metodi come sincronizzati, dato che non vengono chiamati concorrentemente, ma potrebbe essere utile in una situazione più complessa. Per il momento manteniamo il codice così, in attesa di valutare meglio i pro e i contro della decisione.

Per il resto, il codice dovrebbe essere abbastanza intuitivo. Con waitTillDone() mettiamo il thread che lo chiama in attesa. E questa attesa viene risolta con una chiamata a allDone() che cambia lo stato della variabile di controllo e notifica all'oggetto stesso che ha cambiato stato.

Test

Per testare la fuzionalità usiamo questo metodo:

private static void pubSub() {
System.out.println("Start multi publisher example");
Thread subscriber = new AsynchSubscriber();
Thread publisher = new MultiplePublisher();

try {
subscriber.start();
publisher.start();

subscriber.join();
publisher.join();
}
catch (InterruptedException e) {
e.printStackTrace(System.out);
}

System.out.println("Multi publisher example completed");
}

Sincronizzarsi su di una coda: Subscriber

Continuiamo l'esercizio tratto dal tutorial Sun su Java EE, sezione "Creating Robust JMS Applications", in cui un publisher su topic si sincronizza con il subscriber per mezzo di una coda dedicata.

Nel post precedente abbiamo visto il publisher, ora vediamo il subscriber.

AsynchSubscriber

Anche questa classe é una estensione di Thread, il cui metodo run() é questo:

@Override
public void run() {
if(startup()) {
System.out.println("Subscriber ready to start");

receive();
}

closeConnections();
System.out.println("Subscriber run completed");
}

Poco da dire su startup, se non che qui usiamo anche una Durable Connection - ci serve per poter instanziare un durable subscriber - mentre le sessioni sono entrambe di tipo AUTO_ACKNOWLEDGE.

Molto da dire invece sulla receive(). Semplificando, l'idea é quella di instanziare un TopicSubscriber sul topic definito nel nostro Application Server, metterci sopra un listener che si occuperà di trattare i messaggi ricevuti e far partire la connessione.

Comunichiamo quindi al publisher che siamo pronti, aspettiamo che il listener ci confermi che la ricezione é completata e quindi procediamo al cleanup delle risorse utilizzate.

Vediamo il codice:

private void receive() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
TopicSubscriber subscriber =
sessions[0].createDurableSubscriber(cm.topic, "AckSub");

TextListener listener = new TextListener();
subscriber.setMessageListener(listener);
conns[0].start();

if(synchronize() == false) {
System.out.println("Can't synchronize");
return;
}

listener.waitTillDone();
subscriber.close();
sessions[0].unsubscribe("AckSub");
} catch (JMSException e) {
System.err.println("Exception occurred: " + e.toString());
}
}

La synchronize() é una funzione piuttosto semplice, si limita a mandare un messaggio alla coda che utilizziamo per sincronizzare subscriber e producer.

Vediamo a seguire il codice di tutta la classe:

package jmsrobust;

import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.TopicSubscriber;

public class AsynchSubscriber extends Thread {
enum Type {DURABLE, STANDARD};

private Connection conns[] = new Connection[2];
private Session sessions[] = new Session[2];

private boolean startup() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
int i = Type.DURABLE.ordinal();
conns[i] = cm.drbConnFact.createConnection();
sessions[i] = conns[i].createSession(false, Session.AUTO_ACKNOWLEDGE);

i = Type.STANDARD.ordinal();
conns[i] = cm.connFact.createConnection();
sessions[i] = conns[i].createSession(false, Session.AUTO_ACKNOWLEDGE);

return true;
}
catch (Exception e) {
System.err.println("Connection problem: " + e.toString());
}

return false;
}

private void closeConnections() {
for(Type type : Type.values()) {
int i = type.ordinal();
try {
if(conns[i] != null) {
conns[i].close();
conns[i] = null;
}
}
catch(JMSException ee) {}
}
}

private void receive() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
// start a listener on a subscriber
TopicSubscriber subscriber =
sessions[0].createDurableSubscriber(cm.topic, "AckSub");

TextListener listener = new TextListener();
subscriber.setMessageListener(listener);
conns[0].start();

// Let the publisher know that subscriber is ready.
if(synchronize() == false) {
System.out.println("Can't synchronize");
return;
}

// wait for notification from publisher
listener.waitTillDone();
subscriber.close();
sessions[0].unsubscribe("AckSub");
} catch (JMSException e) {
System.err.println("Exception occurred: " + e.toString());
}
}

/**
* Sends a message to the queue to notify a publisher that
* it is ready to receive messages.
*/
private boolean synchronize() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
MessageProducer producer = sessions[1].createProducer(cm.queue);
TextMessage message = sessions[1].createTextMessage();
message.setText("synchronize");
System.out.println("Subscriber is sending synchronize message");
producer.send(message);
} catch (JMSException e) {
System.err.println("Exception occurred for the subscriber: " + e.toString());
return false;
}

return true;
}

/**
* Runs the thread.
*/
@Override
public void run() {
if(startup()) {
System.out.println("Subscriber ready to start");

receive();
}

closeConnections();
System.out.println("Subscriber run completed");
}
}

Ci resta da vedere il codice della TextListener (con la associata classe Monitor) e il main che ci permette di testare le funzionalità, vediamole nel post successivo.

Sincronizzarsi su di una coda: Publisher

Il secondo esempio presentato nel tutorial Java EE di Sun nel paragrafo "Creating Robust JMS Applications", presenta uno scambio di messaggi tra publisher e subcriber che usano una coda accessoria per sincronizzarsi.

Ridisegno il codice presentato, perché mi é sembrato un po' caotico, sperando di ottenere qualcosa di più leggibile.

MultiplePublisher

La classe MultiplePublisher si prende il compito di postare i messaggi su di un Topic per l'uso del subscriber che vedremo più avanti.

Avendo deciso di lavorare con un topic, invece con una queue, dobbiamo aspettare che il ricevente ci dica quando é pronto a ricevere, per evitare che i nostri messaggi vadano persi.

Il metodo run() della classe, che estende Thread, sarà questo:

@Override
public void run() {
if(startup()) {
System.out.println("Publisher ready to start");

if(synchronize())
sendMessages();
}

closeConnections();
System.out.println("Publisher run completed");
}

Dopo la fase di inizializzazione, definita in startup(), ci si sincronizza con il destinatario, e solo se la sincronizzazione ha successo si mandano i messaggi.

La sincronizzazione avviene appoggiandosi su una coda e mettendosi in attesa che arrivi su di essa un messaggio prestabilito:

public boolean synchronize() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
System.out.println("Publisher is synchronizing on the control queue");
MessageConsumer receiver = sessions[1].createConsumer(cm.queue);
conns[1].start();

Message message = receiver.receive(2000);
if(message instanceof TextMessage) {
TextMessage text = (TextMessage) message;
if(text.getText().equals("synchronize")) {
System.out.println("Publisher has received synchronize message");
return true;
}
}
System.out.println("Publisher has not received synchronize message");
} catch (JMSException e) {
System.err.println("Exception occurred: " + e.toString());
}

return false;
}

In questo caso abbiamo deciso di aspettare per due secondi, se nulla arriva nel termine stabilito, diamo per fallita la connessione. Un esempio più realistico sarebbe più opportuno prevedere un meccanismo più sofisticato.

Vediamo ora il codice completo per questa classe. Notiamo che le due sessioni utilizzate (una per il topic su cui avviene il reale scambio di messaggi, una per la coda di controllo) sono create in modalità AUTO_ACKNOWLEDGE.

package jmsrobust;

import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;

public class MultiplePublisher extends Thread {
private final int NUMMSGS = 3;
private final String MSG_TEXT = "Here is an auto-acknowledge message";

private Connection conns[] = new Connection[2];
private Session sessions[] = new Session[2];

private boolean startup() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
for(int i = 0; i < 2; ++i) {
conns[i] = cm.connFact.createConnection();
sessions[i] = conns[i].createSession(false, Session.AUTO_ACKNOWLEDGE);
}
return true;
}
catch (Exception e) {
System.err.println("Connection problem: " + e.toString());
}

return false;
}

private void closeConnections() {
for(int i = 0; i < 2; ++i) {
try {
if(conns[i] != null) {
conns[i].close();
conns[i] = null;
}
}
catch(JMSException ee) {}
}
}

private void sendMessages() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

MessageProducer publisher = null;
TextMessage message = null;
try {
publisher = sessions[0].createProducer(cm.topic);
message = sessions[0].createTextMessage();

for (int i = 0; i < NUMMSGS; i++) {
message.setText(MSG_TEXT + " " + i);
System.out.println("Publishing message: " + message.getText());
publisher.send(message);
}

System.out.println("Sending end of messages");
publisher.send(sessions[0].createMessage());
} catch (JMSException e) {
System.err.println("Exception occurred: " + e.toString());
}
}

@Override
public void run() {
if(startup()) {
System.out.println("Publisher ready to start");

if(synchronize())
sendMessages();
}

closeConnections();
System.out.println("Publisher run completed");
}

public boolean synchronize() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
System.out.println("Publisher is synchronizing on the control queue");
MessageConsumer receiver = sessions[1].createConsumer(cm.queue);
conns[1].start();

Message message = receiver.receive(2000);
if(message instanceof TextMessage) {
TextMessage text = (TextMessage) message;
if(text.getText().equals("synchronize")) {
System.out.println("Publisher has received synchronize message");
return true;
}
}
System.out.println("Publisher has not received synchronize message");
} catch (JMSException e) {
System.err.println("Exception occurred: " + e.toString());
}

return false;
}
}

I dettagli implementativi della connessione a JMS sono nella classe JmsConnectionManager, che abbiamo visto nel precedente post dedicato all'argomento.

Iterator

Pattern descritto in Design Pattern.

Lo scopo del pattern Iterator é quello di fornire una di accesso sequenziale agli elementi di un oggetto aggregato senza esporre la rappresentazione interna.

Esempio

L'interfaccia Iterator disponibile in Java, java.util.Iterator, é leggermente diversa da quella proposta dalla Gang of Four, in particolare prevede anche un metodo per rimuovere l'elemento corrente dalla collezione sottostante. Usiamo comunque quella nel nostro esempio.

Il problema del nostro mondo fantasy che utilizziamo per il nostro esempio é quello di implementare una classe che descrive un tipo peculiare di personaggi che posseggono mezza dozzina di mostri al loro servizio. Questa implementazione prevede che i mostri siano esattamente sei, vengono creati assieme al personaggio ed esistono solo in quanto appartenenti al personaggio stesso.

Per questo motivo li implementiamo con un array e li rendiamo disponibili all'utente della classe per mezzo di un iteratore.

Questo é il codice relativo alla nostra classe Monster, in una implementazione minimale:

package gof.iterator;

public class Monster {
private final int number;
private boolean active = true;

public Monster(int number) {
this.number = number;
}

public int getNumber() {
return number;
}

public boolean isActive() {
return active;
}

public void setActive(boolean active) {
this.active = active;
}
}

La classe che rappresenta il padrone dei mostri viene in questo modo modo:

package gof.iterator;

import java.util.Iterator;

public class MonsterMaster {
private final String name;
private Monster[] monsters = new Monster[6];

public MonsterMaster(String name) {
this.name = name;

for(int i = 0; i < 6; ++i)
monsters[i] = new Monster(i + 1);
}

public String getName() {
return name;
}

public Iterator getIterator() {
return new MonsterIterator(monsters);
}
}

E questa é l'implementazione per il nostro iteratore, che usa come collezione sottostante un array di mostri, come richiesto:

package gof.iterator;

import java.util.Iterator;

public class MonsterIterator implements Iterator {
private Monster[] monsters;
private int current = 0;

public MonsterIterator(Monster[] monsters) {
this.monsters = monsters;
}

public boolean hasNext() {
if(current == monsters.length)
return false;
return true;
}

public Object next() {
if(current == monsters.length)
return null;

return monsters[current++];
}

public void remove() {
monsters[current].setActive(false);
}
}

Verifichiamo la nostra implementazione con questa funzione:

public static void main(String[] args) {
MonsterMaster master = new MonsterMaster("MoMa");

Iterator it = master.getIterator();

System.out.println("List of monsters for " + master.getName());
while(it.hasNext()) {
Monster m = it.next();

System.out.print(m.getNumber() + " ");
}
System.out.println();
}

Messaggi con ricevuta

Per il momento trattiamo sessioni senza transazioni, in questo caso la gestione delle ricevute é determinata da come la sessione viene creata, il che può avvenire in una di queste tre modalità:
  • AUTO_ACKNOWLEDGE, la ricevuta é gestita automaticamente dalla sessione;
  • CLIENT_ACKNOWLEDGE, il client chiama esplicitamente il metodo acknowledge() del messaggio ricevuto per notificare la ricezione. Nota che la notifica non vale per il solo messaggio su cui si chiama il metodo, ma per tutti i messaggi consumati dal client;
  • DUPS_OK_ACKNOWLEDGE, gestione automatica minimizzando i controlli della sessione per evitare duplicazioni di messaggi, permette una gestione più leggera a costo del rischio di duplicazione di messaggi (il campo JMSRedelivered viene messo a true per segnalare messaggi duplicati).
I messaggi senza acknowledgment prima del termine della sessione vengono tenuti in sospeso e rimandati alla nuova connessione alla coda, e anche al topic, nel caso sia associato a un durable TopicSubscriber.

Lavoriamo sul codice fornito dal tutorial Sun su Java EE nel folder jms/advanced/ackequivexample.

L'esempio che sviluppiamo qui mostra come nel caso di un receiver sincrono la ricevuta di ritorno per un messaggio venga generata al termine della gestione.

Specificando CLIENT_ACKNOWLEDGE come modalità di sessione, daremo la ricevuta di ritorno esplicitamente al termine della gestione.

Applicazione Java

Creamo una nuova applicazione Java SE, che chiamiamo jmsRobust e, come negli esempi precedenti, aggiungiamo i jar che ci permettono di interagire con l'application server:
  • appserv-admin.jar
  • appserv-launch.jar
  • appserv-rt.jar
  • imq.jar
  • imqbroker.jar
  • imqjmsra.jar
  • javaee.jar
  • jms.jar
JmsConnectionManager

Creiamo un unico punto di accesso a JMS, la classe JmsConnectionManager, un singleton che va progettato con un minimo di attenzione, visto l'ambiente multithreading in cui ci troviamo.

Al solito, l'URL utilizzata é quella del localhost, nel caso l'application server sia su un'altra macchina occorre fornire l'indirizzo corretto.

Per il resto il codice dovrebbe essere abbastanza leggibile:

package jmsrobust;

import java.util.Hashtable;
import javax.jms.ConnectionFactory;
import javax.jms.Queue;
import javax.jms.Topic;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JmsConnectionManager {
private static final String AS_URL = "iiop://127.0.0.1:3700";
private static final String AS_CLASS = "com.sun.appserv.naming.S1ASCtxFactory";
private static final String CONNECTION = "jms/tConnectionFactory";
private static final String QUEUE = "jms/tQueue";
private static final String TOPIC = "jms/tTopic";

private static volatile JmsConnectionManager instance;

protected ConnectionFactory connFact;
protected Queue queue;
protected Queue ctrlQueue;
protected Topic topic;

private Context ctx;

private JmsConnectionManager() {
System.out.println(Thread.currentThread().getName() + ": initializing the context");

Hashtable properties = new Hashtable(2);
properties.put(Context.PROVIDER_URL, AS_URL);
properties.put(Context.INITIAL_CONTEXT_FACTORY, AS_CLASS);
try {
ctx = new InitialContext(properties);
System.out.println(Thread.currentThread().getName() + ": context initialized");

System.out.println(Thread.currentThread().getName() + ": lookup step");
connFact = (ConnectionFactory)ctx.lookup(CONNECTION);

queue = (Queue)ctx.lookup(QUEUE);
topic = (Topic)ctx.lookup(TOPIC);
System.out.println(Thread.currentThread().getName() + ": lookup done");
}
catch(NamingException ne) {
ne.printStackTrace();
}
}

public static JmsConnectionManager getInstance() {
if(instance == null) {
System.out.println(Thread.currentThread().getName() + ": init required");
synchronized(JmsConnectionManager.class) {
if(instance == null) {
System.out.println(Thread.currentThread().getName() + ": init really required");
instance = new JmsConnectionManager();
System.out.println(Thread.currentThread().getName() + ": init done");
}
}
}

return instance;
}
}

SynchClient

Dobbiamo ora creare due classi, un sender e un receiver che usino la coda per scambiarsi messaggi. Notiamo però che le due classi sarebbero molto simili, c'é una prima fase di startup, uguale per entrambi, in cui si crea una connessione e con questa una sessione; segue un passo specifico in cui il sender manda i messaggi e il receiver li riceve, e infine c'é il cleanup in cui si chiude la connessione.

Tutto ciò ci spinge ad usare il pattern Template Method. La SynchClient sarà la classe astratta alla base della nostra gerarchia:

package jmsrobust;

import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Session;

public abstract class SynchClient extends Thread {
protected Connection connection;
protected Session session;

private boolean startup() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
connection = cm.connFact.createConnection();
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
return true;
}
catch (Exception e) {
System.err.println("Connection problem: " + e.toString());
}

return false;
}

private void closeConnection() {
if (connection != null) {
try { connection.close();}
catch (JMSException ee) {}
}
}

protected abstract void operation();

@Override
public void run() {
if(startup())
operation();
closeConnection();
}
}

Il template method del pattern é qui run(), che definisce come il codice viene eseguito dalle classi derivate. Due dei tre passi sono definiti in questa classe, startup() e closeConnection(), c'é poi il metodo dichiarato qui come abstract, che andrà definito nelle classi derivate.

La cosa importante da notare dal punto di vista di JMS é che la sessione viene creata in modalità CLIENT_ACKNOWLEDGE, dunque toccherà a chi riceve dare conferma esplicita del messaggio ricevuto.

SynchSender

Il sender implementa operation() con lo scopo di mettere un messaggio sulla coda. Usa la session creata da startup() per creare un MessageProducer che faccia riferimento alla coda che abbiamo istanziato in JmsConnectionManager:

package jmsrobust;

import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.TextMessage;

public class SynchSender extends SynchClient {
final String MSG_TEXT = "Please explicitly acknowledge this";

protected void operation() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
MessageProducer producer = session.createProducer(cm.queue);
TextMessage message = session.createTextMessage();

long secs = System.currentTimeMillis() % 10000;
message.setText(MSG_TEXT + " (" + secs + ")");

System.out.println("Sending message: " + message.getText());
producer.send(message);
} catch (JMSException e) {
System.err.println("Exception occurred: " + e.toString());
}
}
}

SynchReceiver

Il receiver implementa operation() per leggere un messaggio dalla coda, crea quindi un MessageConsumer sulla coda usata anche dal sender, fa partire la connessione e si mette in attesa di ricevere il messaggio.

Ricevuto il messaggio e compiuta la sua elaborazione viene mandata ricevuta alla coda:

package jmsrobust;

import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.TextMessage;

public class SynchReceiver extends SynchClient {

protected void operation() {
JmsConnectionManager cm = JmsConnectionManager.getInstance();

try {
MessageConsumer receiver = session.createConsumer(cm.queue);
connection.start();
TextMessage message = (TextMessage)receiver.receive();

System.out.println("The receiver processes: " + message.getText());
System.out.println("The receiver acknowledge the message");
message.acknowledge();
} catch (JMSException e) {
System.err.println("Exception occurred: " + e.toString());
}
}
}

Main

Per testare il codice scriviamo questo codice:

private static void clientAck() {
System.out.println("Start explicit client acknowledgment example");
Thread sender = new SynchSender();
Thread receiver = new SynchReceiver();

sender.start();
receiver.start();

try {
sender.join();
receiver.join();
}
catch (InterruptedException e) {
e.printStackTrace(System.out);
}

System.out.println("Explicit client acknowledgment example completed");
}

public static void main(String[] args) {
clientAck();

System.exit(0);
}

Cercare tra i messaggi in coda

Questa volta creiamo un client che ci permette di guardare i messaggi in attesa su una coda senza consumarli.

In pratica il client é molto simile a quelli che consumano i messaggi e abbiamo già visto, con la differenza che il browsing é applicabile solo a code, e che usiamo un oggetto QueueBrowser per accedere la nostra coda.

Il client consiste in due file: Browser.java e Main.java.

Browser

I cambiamenti sono principalmente raccolti nella funzione getMessages(), dove si crea un oggetto QueueBrowser a partire dalla sessione corrente per l'oggetto Queue specificato.

Interessante é il modo di accedere ai messaggi, per cui la classe QueueBrowser mette a disposizione il metodo getEnumeration(), che li ritorna, per l'appunto, come una enumeration.

package browser;

import java.util.Enumeration;
import java.util.Hashtable;
import javax.jms.ConnectionFactory;
import javax.jms.Queue;
import javax.jms.Connection;
import javax.jms.Session;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.QueueBrowser;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Browser {
private static final String AS_URL = "iiop://127.0.0.1:3700";
private static final String AS_CLASS = "com.sun.appserv.naming.S1ASCtxFactory";
private static final String CONNECTION = "jms/tConnectionFactory";
private static final String QUEUE = "jms/tQueue";

private static Queue queue;

private Connection connection;
private Session session;

private static Context ctx;
static {
System.out.println("initializing the context");

Hashtable properties = new Hashtable(2);
properties.put(Context.PROVIDER_URL, AS_URL);
properties.put(Context.INITIAL_CONTEXT_FACTORY, AS_CLASS);
try {
ctx = new InitialContext(properties);
}
catch (NamingException ex) {
ex.printStackTrace();
}
}


public Browser() throws JMSException, NamingException {

ConnectionFactory f = (ConnectionFactory)ctx.lookup(CONNECTION);
connection = f.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
}

public void getMessages() throws JMSException, NamingException {
// ensure the queue is available
if(queue == null) {
System.out.println("Looking for JMS queue");
queue = (Queue)ctx.lookup(QUEUE);
}

QueueBrowser browser = session.createBrowser(queue);
Enumeration<Message> msgs = browser.getEnumeration();

if(!msgs.hasMoreElements()) {
System.out.println("No messages in queue");
}
else {
while(msgs.hasMoreElements()) {
Message tempMsg = msgs.nextElement();
System.out.println("Message: " + tempMsg);
System.out.println("-------");
}
}
}

public void close() throws JMSException {
System.out.println("Closing JMS connection");
if(connection != null)
connection.close();
}
}

Main

Il main crea semplicemente un oggetto di tipo Browser, chiama la getMessage(), chiude la connessione e termina l'esecuzione.

public static void main(String[] args) {
try {
Browser browser = new Browser();
browser.getMessages();
browser.close();
}
catch(Exception ex) {
System.err.println("Consuming failed: " + ex.toString());
ex.printStackTrace();
System.exit(1);
}
System.exit(0);
}

Singleton

Pattern descritto in Design Pattern.

Lo scopo del pattern Singleton é quello di assicurare che una classe ha una sola istanza, e gli fornisce un punto di accesso globale.

Esempio

E' un pattern molto usato, in quanto accade molto spesso di dover garantire che esista un solo oggetto di una determinata classe. Un esempio per il nostro mondo fantasy potrebbe essere il genio della lampada, alla Alì Babà. Viene descritto da una classe specifica, dato che ha caratteristiche sue peculiari, e ne esiste uno solo.

E' anche un pattern a cui bisogna dedicare una certa attenzione perché tocca un punto delicato. O meglio, se il nostro singleton si trova ad essere eseguito in un singolo thread le cose sono piuttosto semplici, ma sorgono problemi se lavoriamo, come di solito accade in un ambiente "moderno", in multithreading.

Iniziamo a vedere il caso in cui ci sia un solo thread di esecuzione.

Singleton senza multithreading

Dobbiamo regolamentare la creazione del genio, per questo motivo dichiariamo il costruttore standard privato, impedendo di fatto la creazione di genii all'esterno della classe stessa.

Mettiamo quindi a disposizione un metodo statico, per consuetudine chiamato getInstance(), che crea l'unica instanza della classe, se non é ancora stata creata, e la ritorna al chiamante.

Oltre al meccanismo proprio per la gestione del singleton aggiungiamo alla classe una proprietà per tener traccia del fortunato che viene riconosciuto dal genio come suo capo, e metodi per il get e set del master.

La classe andrebbe estesa per gestire tre desideri e quindi resettare il master a null, ma tralasciamo, per il momento.

Il codice minimale é perciò questo:

package gof.singleton;

public class LampGenie {
private static LampGenie instance = null;

public static LampGenie getInstance() {
if(instance == null)
instance = new LampGenie();
return instance;
}

private Human master = null;

private LampGenie() {}

public Human getMaster() {
return master;
}

boolean setMaster(Human newMaster) {
if(master == null) {
System.out.println("Setting the master to " + newMaster.getName());
master = newMaster;
return true;
}
else {
System.out.println("I already have a master: " + master.getName());
return false;
}
}
}

Singleton in multithreading

Ma che succede alla getInstance() se il codice é eseguito contemporaneamente da più thread? In linea teorica possiamo avere grossi problemi e, come ci insegnano le Leggi di Murphy, possiamo star certi che nel momento meno opportuno qualcosa di sgradevole capiterà anche nella pratica.

Infatti se un thread trova che instance é correntemente pari a null, potrebbe ben essere, che un altro thread abbia appena fatto lo stesso controllo e gli cambi il valore di instance praticamente sotto i piedi, ottenendo così il risultato inaspettato che entrambi i thread finiscono per creare una nuova instance.

Abbiamo infatti due competitori per un unica risorsa il cui accesso, per non evitare questi pasticci, va regolamentato.

Ci sono diversi modi di superare il problema, in funzione del contesto in cui ci troviamo.

In questo caso abbiamo una risorsa, il genio della lampada, che costa poco creare, ed é normalmente usata nel corso della nostra applicazione (che storia sarebbe mai senza genio della lampada?), percui ci viene naturale prendere la decisione di lasciare che ci pensi la Java Virtual Machine a garantire che le cose vadano per il verso giusto. Chiamiamo il costruttore per il singleton al momendo della sua dichiarazione come oggetto statico. Ci penserà la JVM a chiamare il costruttore garantendo l'assenza di conflitti tra thread.

Se ci va bene questo approccio, faremo questi cambiamenti nella classe, che rinominiamo SafeLampGenie:

public class SafeLampGenie {
private static SafeLampGenie instance = new SafeLampGenie();

public static SafeLampGenie getInstance() {
return instance;
}
...

Altrimenti dovremmo riscrivere il metodo getInstance implementando noi la sincronizzazione, ovvero il modo che Java ci mette a disposizione per gestire il problema della concorrenza.

Ne parliamo in un altro post, magari.

Per completare l'esempio manca la definizione della classe Human, che riduciamo ai minimi termini, come segue:

package gof.singleton;

public class Human {
private final String name;

public Human(String name) {
this.name = name;
}

public String getName() {
return name;
}
}


E quindi un funzione main che ci permetta di verificare il funzionamento di quanto abbiamo scritto:

public static void main(String[] args) {
Human tom = new Human("Tom");
Human bob = new Human("Bob");

if(SafeLampGenie.getInstance().getMaster() == null) {
System.out.println("Genie is looking for a master");
}

if(SafeLampGenie.getInstance().setMaster(tom))
System.out.println(tom.getName() + ": I'm the Genie Master!");
if(SafeLampGenie.getInstance().setMaster(bob) == false)
System.out.println(bob.getName() + ": Damn, I'm late!");
}

Consumo asincrono di messaggi

Aggiungiamo un altro client al nostro sistema. Questo consuma messaggi in maniera asincrona. In pratica lo lanciamo, mettendolo in ascolto su una Queue o Topic e lo lasciamo correre indefinitamente.

TextListener

Gran parte del codice é uguale a quello che abbiamo scritto per il client sincrono, il principale cambiamento é che la gestione dei messaggi avviene per mezzo di un MessageListener.

A noi interessa gesitire solo i messaggi di testo, quindi lo chiamiamo TextListener:

package asynchConsumer;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

public class TextListener implements MessageListener {
public void onMessage(Message message) {
System.out.println("Enter TextListener.onMessage()");
TextMessage msg = null;

try {
if(message instanceof TextMessage) {
msg = (TextMessage) message;
System.out.println("Reading message: " + msg.getText());
}
else {
System.err.println("Message is not a TextMessage");
}
}
catch(JMSException e) {
System.err.println("JMSException in onMessage(): " + e.toString());
}
catch(Throwable t) {
System.err.println("Exception in onMessage():" + t.getMessage());
}

System.out.println("Exit TextListener.onMessage()");
}
}

Consumer

La classe Consumer di asynchConsumer si differenzia da quella di synchConsumer praticamente nel solo metodo getMessage(). In questo caso ci si limita ad istanziare il Listener e associarlo al consumer:

public void getMessages() throws JMSException, NamingException {
Destination dest = getDestination();

MessageConsumer consumer = session.createConsumer(dest);
TextListener listener = new TextListener();
consumer.setMessageListener(listener);
connection.start();

System.out.println("Waiting for messages");
}

Main

Il cambiamento rilevante nella classe Main é che aggiungiamo la gestione dell'interrupt. L'idea é che il thread iniziale che esegue la funzione main termina dopo aver creato il consumer. Restano vivi i thread generati da questo, che termineremo esplicitamente su richiesta dell'utente, mediante un control-c.

La gestione dello shutdown é affidata a questa classe:

package asynchConsumer;

import javax.jms.JMSException;

public class Shutdown implements Runnable {
Consumer consumer;

public Shutdown(Consumer consumer) {
this.consumer = consumer;
}

public void run() {
System.out.println("Shutting down...");

if(consumer != null)
try { consumer.close();} catch(JMSException ex) {}
}
}

Tiene al suo interno un riferimento al consumer e, quando arriva il segnale di terminazione lo chiude.

La comunicazione alla Java Virtual Machine che va chiamato il metodo run() della nostra classe Shutdown quando arriva il momento di terminare l'esecuzione, si compie per mezzo di una chiamata a addShutdownHook() a cui passiamo un nuovo thread che ha il compito di eseguire la nostra classe.

Vediamo dunque la funzione main() della nostra applicazione con questa variazione, per il resto praticamente uguale al main per il consumer sincrono:

public static void main(String[] args) {
boolean isTopic = (args.length == 1 && args[0].startsWith("t")) ? true : false;

Consumer consumer = null;
try {
consumer = new Consumer();

// default: wait for queue messages
if(isTopic)
consumer.setDestination(Consumer.Type.TOPIC);
consumer.getMessages();
}
catch(Exception ex) {
System.err.println("Consuming failed: " + ex.toString());
ex.printStackTrace();
System.exit(1);
}

Runtime.getRuntime().addShutdownHook(new Thread(new Shutdown(consumer), "Shutdown"));
System.out.println("Control-C to terminate");
}

Template Method

Pattern descritto in Design Pattern.

Lo scopo del pattern Template Method é quello di definire la struttura di un algoritmo, lasciando che alcuni suoi passi vengano definiti dalle sottoclassi. In questo modo le sottoclassi possono ridefinire alcuni passi dell'algoritmo senza cambiarne la struttura.

Partecipanti
  • AbstractClass, definisce come astratte le operazioni primitive che sono implementate alle classi derivate e che definiscono i singoli passi dell'algoritmo; implementa il template method che definisce la struttura dell'algoritmo e che usa le funzioni che definiscono i singoli passi, come definiti nella classe astratta o nelle classi derivate;
  • ConcreteClass, implementa i singoli passi come richiesto dalla versione dell'algoritmo.
Esempio

Nella nostra applicazione fantasy, i personaggi possono andare in appositi negozi per comprare oggetti di utilità più o meno comune. La struttura dell'acquisto é sempre la stessa per ogni negozio: il negoziante valuta se il cliente é di suo gradimento, poi verifica che il prodotto selezionato sia adeguato per il cliente, ci si accorda sul prezzo e solo come ultimo passo viene dato al cliente l'ambito prodotto.

Il problema é che ogni negozio ha una diversa valutazione del cliente (alcuni vendono solo a maghi, ad esempio), ha un diverso magazzino, e un diverso listino. Insomma, vogliamo avere un algoritmo di vendita che abbia una struttura rigida, che deve essere eseguita come stabilito da tutti i negozi, ma nel contempo vogliamo anche una notevole libertà implementativa in ogni passo dell'algoritmo.

Il pattern Template Method ci aiuta a realizzare proprio questo.

AbstractClass

In cima alla gerarchia di negozi c'é la classe astratta Shop, che mette a disposizione il metodo template che qui si chiama buy() e ritorna l'acquisto fatto (o null, in caso la trattativa non vada a buon termine).

In questo caso non abbiamo una implementazione di default per nessuno dei passi dell'algoritmo definito da buy(), perciò tutti i metodi che definiscono i passi intermedi sono astratti:

package gof.templateMethod;

public abstract class Shop {
protected final String name;

public Shop(String name) {
this.name = name;
}

public String getName() {
return name;
}

public Good buy(Character character, int item, int price) {
if(checkCharacter(character) == false) {
System.out.println(name + ": I won't sell you anything");
return null;
}

if(checkGood(character, item) == false) {
System.out.println(name + ": Sorry I can't sell you that");
return null;
}

if(checkPrice(character, item, price) == false) {
System.out.println(name + ": Price is high, but quality too!");
return null;
}

return makeGood();
}

abstract protected boolean checkCharacter(Character character);

abstract protected boolean checkGood(Character character, int item);

abstract protected boolean checkPrice(Character character, int item, int price);

abstract protected Good makeGood();
}

ConcreteClass

Definiamo un solo tipo negozio in questo esempio. Si tratta dell'armeria. Una regola fondamentale della gilda degli armaioli é che solo gli umani possono comprare qui, e quindi quello che farà la checkCharacter() é assicurarsi che il cliente sia umano.
Le armerie vendono prodotti che hanno un numero di codice, stabilito dal Grande Catalogo Universale, compreso tra 1001 e 2000. Controllo che viene compiuto in checkGood(). Per alcuni prodotti potrebbe essere necessario fare altri controlli aggiuntivi, dato che alcune armi piuttosto difficili da usare potrebbero essere vendute solo a personaggi con particolari caratteristiche, per questo motivo si passa a questo metodo anche il riferimento al cliente.
Anche l'implementazione del listino prezzi é correntemente molto debole, tutte le merci disponibili sono vendute a 5 (o più), ma é prevista la possibilità di avere prezzi diversi per ogni tipo di merce venduta, come ci si dovrebbe aspettare, e anche in funzione del cliente.
Del resto il catalogo é in realtà correntemente molto limitato, dato che la makeGood() in realtà ritorna sempre l'istanza di una semplice spada.

Ecco a seguire la classe che implementa le nostre armerie:

package gof.templateMethod;

public class Armory extends Shop {
public Armory(String name) {
super(name);
}

@Override
protected boolean checkCharacter(Character character) {
if(character instanceof Human)
return true;

return false;
}

@Override
protected boolean checkGood(Character character, int item) {
if(item > 1000 && item < 2000)
return true;
return false;
}

@Override
protected boolean checkPrice(Character character, int item, int price) {
if(price >= 5)
return true;
return false;
}

@Override
protected Good makeGood(int item) {
System.out.println(name + ": here is your Simple Sword!");
return new SimpleSword();
}
}

Classi al contorno

Resta da definire la gerarchia di classi dei clienti, di cui diamo una definizione minimale così:

public interface Character {
abstract public String getName();
}

class Human implements Character {
private final String name;

public Human(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

class Monster implements Character {
private final String name;

public Monster(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

Ed ecco la gerarchia di oggetti commerciabili:

public interface Good {
public int getPrice();
}

public interface Weapon extends Good {
public int getOffensiveBonus();
public int getDefensiveBonus();
}

public class SimpleSword implements Weapon {
private int price = 10;
private int offensiveBonus = 12;
private int defensiveBonus = 6;

public int getPrice() {
return price;
}

public int getOffensiveBonus() {
return offensiveBonus;
}

public int getDefensiveBonus() {
return defensiveBonus;
}
}

Main

Resta solo da scrivere un piccolo test client:

public static void main(String[] args) {
Shop shop = new Armory("Kill Safe");

System.out.println("Tom wants a sword from " + shop.getName());

Character tom = new Monster("Tom");
Good good = shop.buy(tom, 1001, 5);
if(good == null) {
System.out.println("Tom goes somewhere else.");
}

System.out.println("Tom wants a something from " + shop.getName());
Character tim = new Human("Tim");
good = shop.buy(tim, 1, 100);

if(good == null) {
System.out.println("Tim looks for a sword");
}
good = shop.buy(tim, 1001, 4);

if(good == null) {
System.out.println("Tim increases his offer");
}
good = shop.buy(tim, 1001, 5);

if(good instanceof Weapon) {
Weapon w = (Weapon)good;

System.out.println("The offensive bonus is " + w.getOffensiveBonus());
}
}

Che produce questo output:

Tom wants a sword from Kill Safe
Kill Safe: I won't sell you anything
Tom goes somewhere else.
Tom wants a sword from Kill Safe
Kill Safe: Sorry I can't sell you that
Tim looks for something else
Kill Safe: Price is high, but quality too!
Tim increases his offer
Kill Safe: here is your Simple Sword!
The offensive bonus is 12

Strategy

Pattern descritto in Design Pattern.

Lo scopo del pattern Strategy (aka Policy) é quello di definire una famiglia di algoritmi, incapsularli e renderli intercambiabili. Con l'uso di Strategy é possibile rendere l'algoritomo variabile in modo indipendente dall'oggetto che lo usa.

Partecipanti
  • Strategy: dichiara una interfaccia comune a tutti gli algoritmi supportati.
  • ConcreteStrategy: implementa gli algoritmi usando l'interfaccia Strategy.
  • Context: configurato con un oggetto di tipo ConcreteStrategy, mantiene un riferimento all'interfaccia Strategy, può definire un interfaccia per l'accesso di Strategy ai suoi dati.

Esempio

Nella nostra applicazione fantasy i personaggi spesso combattono fra loro. Il nostro problema é che esistono diversi modi di combattere, alcuni brutti ceffi fanno ricorso quasi esclusivamente alla forza bruta, altri, che possono essere non meno pericolosi, integrano la loro forza, solitamente limitata, con l'uso della loro astuzia, altri ancora, sapienti maghi, se provocati utilizzano le loro ben allenate facoltà mentali, e solo marginalmente la propria forza e abilità per causare danni al loro oppositore. Insomma, ogni carattere ha la sua diversa strategia nel combattimento.

E questo non é tutto. Ne succedono di tutti i colori in quel mondo, può persino accadere che un valoroso guerriero decida di abbandonare l'uso delle armi e mettersi a praticare la magia. Ovvero, un carattere non ha una strategia di combattimento immutabile, al contrario questa può cambiare nel corso del tempo.

Contesto astratto

I nostri personaggi saranno umani, mostri, e chissà che altro. Modifichiamo quindi leggermente il pattern inserendo una classe astratta Character che ne sintetizza le qualità comuni e fornisce una referenza alla strategia di combattimento:

package gof.strategy;

public abstract class Character {
protected String name;
protected int force;
protected int intelligence;
protected int ability;

protected CombatStrategy combat;

public Character(String name, int force, int intelligence, int ability) {
this.name = name;
this.force = force;
this.intelligence = intelligence;
this.ability = ability;
}

public void setCombatStrategy(CombatStrategy combat) {
this.combat = combat;
}

public void fight() {
System.out.print("For fighting " + name + " uses the ");
System.out.println(combat.getClass().getSimpleName() + ": ");

combat.fight();
}
}

Il cuore del pattern é nel fatto che il modo di combattere é definito da una gerarchia di classi che ha origine nell'interfaccia CombatStrategy, e il Context usa una ConcreteStrategy non derivando da essa ma possedendo un'istanza della classe. In termini Object-Oriented: il Character non é, ma puttosto ha una CombatStrategy.
Questo ci permette di cambiare facilmente il modo di combattere del nostro personaggio, basta usare il metodo setCombatStrategy() che aggiorna la sua strategia di combattimento.
Quindi il metodo fight() semplicemente delega l'attività alla CombatStrategy corrente.

Contesto concreto

Gli umani entrano in gioco, nel nostro mondo fantasy, con una certa propensione alla violenza. Questo viene descritto dal costruttore della classe, che gli assegna come strategia di combattimento iniziale quella propria di guerriero brutale:

package gof.strategy;

public class Human extends Character {
public Human(String name, int force, int intelligence, int ability) {
super(name, force, intelligence, ability);

combat = new BrutalWarriorStrategy(this);
}
}

Strategia

Nella versione iniziale la nostra strategia é una interfaccia molto semplice, mettendo a disposizione solo il metodo fight(), che permette di combattere:

package gof.strategy;

public interface CombatStrategy {
public void fight();
}


Strategie concrete

Vediamo un paio di possibili strategie che implementano la nostra interfaccia, cominciando dal default per gli umani, il guerriero brutale:

package gof.strategy;

public class BrutalWarriorStrategy implements CombatStrategy {
private Character character;

public BrutalWarriorStrategy(Character character) {
this.character = character;
}

public void fight() {
System.out.println("90% force: " + character.force * 0.90);
System.out.println("5% ability: " + character.ability * 0.05);
System.out.println("5% intelligence: " + character.intelligence * 0.05);
System.out.println("---");
}
}

Quello che cambia, rispetto alla strategia del mago, che vediamo a seguire, e solo nel modo in cui vengono gestite le caratteristiche del carattere che sua la strategia:

package gof.strategy;

public class WizardStrategy implements CombatStrategy {
private Character character;

public WizardStrategy(Character character) {
this.character = character;
}

public void fight() {
System.out.println("10% force: " + character.force * 0.1);
System.out.println("20% ability: " + character.ability * 0.2);
System.out.println("70% intelligence: " + character.intelligence * 0.7);
System.out.println("---");
}
}

Main

Ci resta da scrivere un piccolo tester, che crea un umano, ne verifica il modo di combattere, gli cambia la strategia e vede come ciò impatta sul di lui:

public static void main(String[] args) {
Character tom = new Human("Tom", 50, 40, 30);

tom.fight();

CombatStrategy combat = new WizardStrategy(tom);
tom.setCombatStrategy(combat);

tom.fight();
}

Questo l'output che ci attendiamo:

For fighting Tom uses the BrutalWarriorStrategy:
90% force: 45.0
5% ability: 1.5
5% intelligence: 2.0
---
For fighting Tom uses the WizardStrategy:
10% force: 5.0
20% ability: 6.0
70% intelligence: 28.0
---

Observer

Pattern descritto in Design Pattern.

Lo scopo del pattern Observer (aka Publish/Subscribe) é quello di definire una dipendenza uno a molti tra oggetti in modo che quando lo stato dell'uno cambia, tutti i dipendenti ricevono una notifica del fatto.

Partecipanti
  • Subject: il soggetto osservato, sa chi lo osserva, non vi sono limiti teorici sul numero degli osservatori. Fornisce un'interfaccia per connettere e disconnettere gli osservatori.
  • Observer: definisce l'interfaccia dell'osservatore, mettendo a disposizione un metodo per notificare i cambiamenti del Subject.
  • ConcreteSubject: tiene traccia dello stato del soggetto
  • ConcreteObserver: mantiene un riferimento al Subject; implementa l'interfaccia Observer;
Esempio

In Java esiste una implementazione standard di questo pattern, nel package java.util é infatti definita la classe Observable e l'interfaccia Observer.

Notiamo che Observable é una classe, mentre il pattern prevederebbe l'uso di un'interfaccia. Questo, considerando inoltre le limitazioni di Java che non permette l'ereditarietà multipla, porta a dover sottostare a dei compromessi se si usa questa implementazione.

Nel nostro caso non abbiamo problemi particolari e quindi usiamo volentieri il codice che ci troviamo a disposizione.

Consideriamo nella nostra applicazione fantasy il caso di un personaggio che riesca con il suo carisma a raggruppare intorno a sé un manipolo di seguaci. Il seguace guadagna da questa relazione un bonus che lo renderà più forte in certe situazioni, ma paga questo con un certo assoggettamento ai voleri del master che, notificandolo ai seguaci, ottiene che questi lo combattano al posto suo.

Questo vale solo se il carisma del master é superiore al desiderio di indipendenza del seguace. Altrimenti il seguace non ci pensa due volte a lasciare il master per conto suo.

Master

Il master della situazione sarà un oggetto di tipo derivato da java.util.Observable, avrà un nome, e un carisma (rappresentato da un intero). Il nome del master non può cambiare nel corso della sua vita, percui avremo un getter ma non un setter per questa proprietà.

Il nostro master può fare fondamentalmente tre cose che hanno un influsso sui suoi seguaci:
cambiare il livello del proprio carisma: il seguace dovrà decidere se essere ancora legato al master in seguito a questo cambiamento;
cercare di aggiungere un seguace: se il seguace é d'accordo, gioirà della relazione, altrimenti annullerà immediatamente l'iscrizione;
attaccare un nemico: l'istanza sgradita al master viene passata ai seguaci, che lo attaccheranno al posto suo.

Vediamo dunque il codice per la classe Master risultante:

package gof.observer;

import java.util.Observable;
import java.util.Observer;

public class Master extends Observable {
private final String name;
private int charisma;

public Master(String name, int charisma) {
this.name = name;
this.charisma = charisma;
}

public String getName() {
return name;
}

public int getCharisma() {
return charisma;
}

public void setCharisma(int charisma) {
this.charisma = charisma;

System.out.println(name + ": \"My charisma has changed.\"");
setChanged();
notifyObservers();
System.out.println("---");
}

@Override
public synchronized void addObserver(Observer o) {
super.addObserver(o);
setChanged();
notifyObservers();
System.out.println("---");
}

public void attack(Warrior enemy) {
System.out.println(name + ": \"I have an enemy!\"");
setChanged();
notifyObservers(enemy);
System.out.println("---");
}
}

Warrior

In questa prima versione il master può raccogliere seguaci solo tra i Guerrieri, quindi abbiamo una classe Warrior che implementa l'interfaccia java.util.Observer, dichiarando inoltre un riferimento al master corrente per il guerriero (inizialmente sarà null, e il guerriero sarà libero), il suo livello di indipendenza (un intero) e il bonus che ottiene in cambio del suo legame al master (zero in caso di indipendenza)

Le tre attività peculiari del guerriero in quanto dipendente a un master sono quelle di elevare una lode al master, abbandonarlo (il che comporta annullare il proprio bonus), e combattere un guerriero segnalatogli dal master stesso. Sono tutti metodi privati, dato che verranno richiamati internamente alla classe stessa.

Il metodo chiave del pattern é update(), dichiarato nell'interfaccia Observer, e che implementiamo qui in questo modo: ci accertiamo che la richiesta stia arrivando da un Master, altrimenti la rifiutiamo (sdegnosamente, un guerriero accetta solo Master come master); se il guerriero non ha già un master, lo accetta, altrimenti declina l'offerta (un guerriero é fedele al proprio master, finchè ha un adeguato carisma); controlliamo il carisma del nostro nuovo master, se inferiore alle aspettative, lo abbandoniamo; aggiorniamo il nostro bonus, in funzione del carisma del master (un quarto del suo carisma, in questa versione); se il master non ci ha passato lavoro sporco da fare, eleviamo la nostra lode a lui; altrimenti controlliamo cosa ci ha passato, se si tratta di un guerriero lo combattiamo.

Ecco il codice per questa classe:

package gof.observer;

import java.util.Observable;
import java.util.Observer;

public class Warrior implements Observer {
Master master;
int indipendence;
int bonus = 0;

private String name;

public String getName() {
return name;
}

public Warrior(String name, int indipendence) {
this.indipendence = indipendence;
this.name = name;
}

public void update(Observable o, Object arg) {
if(!(o instanceof Master)) {
return;
}
if(master == null)
master = (Master)o;
else if(o != master) {
return;
}

if(master.getCharisma() < indipendence) {
master.deleteObserver(this);
leave();
return;
}

bonus = master.getCharisma() / 4;
if(arg == null) {
prise();
return;
}

if(arg instanceof Warrior) {
fight((Warrior)arg);
}
}

private void prise() {
System.out.println(name + ": \"" + master.getName() + " is cool.\"");
}

private void leave() {
System.out.println(name + ": \"I don't like " + master.getName() + ".\"");
master = null;
bonus = 0;
}

private void fight(Warrior w) {
System.out.println(name + ": \"" + w.getName() + " is my enemy!\"");
}
}

Main

Ci resta da scrivere un piccolo test client in cui creiamo un master, un paio di guerrieri, li associamo al master, creiamo un terzo guerriero che viene attaccato dal master, in seguito a questa disdicevole azione il master perde buona parte del suo carisma, il che lo porterà a perdere un membro del suo seguito, nonostante ciò il master itera il suo attacco contro il guerriero:

public static void main(String[] args) {
Master master = new Master("MasterPlaster", 12);

Warrior tom = new Warrior("Tom", 6);
Warrior bob = new Warrior("Bob", 8);

master.addObserver(bob);
master.addObserver(tom);

Warrior bill = new Warrior("Bill", 12);
master.attack(bill);

master.setCharisma(7);
master.attack(bill);
}

Questo l'output del nostro test client:

Bob: "MasterPlaster is cool."
---
Tom: "MasterPlaster is cool."
Bob: "MasterPlaster is cool."
---
MasterPlaster: "I have an enemy!"
Tom: "Bill is my enemy!"
Bob: "Bill is my enemy!"
---
MasterPlaster: "My charisma has changed."
Tom: "MasterPlaster is cool."
Bob: "I don't like MasterPlaster."
---
MasterPlaster: "I have an enemy!"
Tom: "Bill is my enemy!"

Topic vs Queue

Facciamo un esempio per sperimentare la differenza tra topic e queue.

Costruiremo un producer che metterà messaggi in una destinazione di tipo javax.jms.Topic e altri in una javax.jms.Queue; a questo affiancheremo un consumer capace di ricevere messaggi da Topic e da Queue.

Per rendere le cose più interessanti lavoreremo con java SE, lavorando quindi sostanzialmente in remoto.

Admin Console

Avevamo già creato per i precedenti esempi una Connection Factory e una Destination di tipo Queue, se guardiamo nella nostra console dell'amministratore per Glassfish sotto Resources, JMS Resources le dovremmo trovare sotto Connection Factories (con JNDI Name jms/tConnectionFactory) e Destination Resources (con JNDI Name jms/tQueue).

Creiamo una nuova destinazione, il Topic che ha nome JNDI "jms/tTopic", destinazione fisica a nome "tConnectionFactory" (che poi é la connection factory che abbiamo già definito), tipo di risorsa "javax.jms.Topic", e una descrizione a nostro piacere.

Applicazione Java SE

Creaiamo ora un progetto per una applicazione Java SE, che chiamemo JmsSimple (o come altro ci sembra meglio).

Dobbiamo aggiungere dieci jar che ci permettono di gestire JMS pur non essendo in un Java EE Container:
  • appserv-rt.jar,
  • javaee.jar,
  • appserv-admin.jar,
  • appserv-launch.jar
  • appserv-ext.jar
  • appserv-deployment-client.jar
  • imqjmsra.jar,
  • jms.jar,
  • imq.jar,
  • imqbroker.jar.
Nella nostra applicazione vogliamo due package: producer e synchConsumer. Il producer creerà messaggi per JMS, synchConsumer fornirà le funzionalità per consumarli.

Producer

La differenza fondamentale tra l'applicazione remota che generava messaggi per JMS che abbiamo sviluppato in precedenza e questa, é che qui gestiamo anche la destinazione JMS Topic ma, come si vede nel codice sorgente che segue, si tratta proprio di una differenza minima, in pratica dobbiamo solo avere il nome della nostra destinazione, e sapere che é di tipo Topic:

package producer;

import java.util.Hashtable;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.Queue;
import javax.jms.Topic;
import javax.jms.Connection;
import javax.jms.Session;
import javax.jms.MessageProducer;
import javax.jms.TextMessage;
import javax.jms.JMSException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Producer {
private static final String AS_URL = "iiop://127.0.0.1:3700";
private static final String AS_CLASS = "com.sun.appserv.naming.S1ASCtxFactory";
private static final String CONNECTION = "jms/tConnectionFactory";
private static final String QUEUE = "jms/tQueue";
private static final String TOPIC = "jms/tTopic";

private static Queue queue;
private static Topic topic;
public enum Type {QUEUE, TOPIC};

private int messages;
private Type destType;
private Connection connection;
private Session session;

private static Context ctx;
static {
System.out.println("initializing the context");

Hashtable properties = new Hashtable(2);
properties.put(Context.PROVIDER_URL, AS_URL);
properties.put(Context.INITIAL_CONTEXT_FACTORY, AS_CLASS);
try {
ctx = new InitialContext(properties);
}
catch (NamingException ex) {
ex.printStackTrace();
}
}

public void setMessages(int messages) {
this.messages = messages;
}

public void setDestination(Type destType) {
this.destType = destType;
}

public Producer() throws JMSException, NamingException {
messages = 4;
destType = Type.QUEUE;

ConnectionFactory f = (ConnectionFactory)ctx.lookup(CONNECTION);
connection = f.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
}

private Destination getDestination() throws NamingException {
if(destType == Type.QUEUE) {
if(queue == null) {
System.out.println("Looking for JMS queue");
queue = (Queue)ctx.lookup(QUEUE);
}
return queue;
}
else { // Type.TOPIC
if(topic == null) {
System.out.println("Looking for JMS topic");
topic = (Topic)ctx.lookup(TOPIC);
}
return topic;
}
}

public void sendMessages() throws JMSException, NamingException {
Destination dest = getDestination();

MessageProducer producer = session.createProducer(dest);
TextMessage message = session.createTextMessage();

for (int i = 0; i < messages; i++) {
message.setText("This is message " + (i + 1));
System.out.println("Sending message: " + message.getText());
producer.send(message);
}

// Send a non-text control message indicating end of messages
producer.send(session.createMessage());
}

public void close() throws JMSException {
System.out.println("Closing JMS connection");
if(connection != null)
connection.close();
}
}

La particolarità implementativa é che, in coda ai messaggi di tipo testo, viene spedito un messaggio vuoto, di tipo MessageImpl (come dire, non un messaggio reale), a indicare che la trasmissione é stata completata.

La classe Main del package producer si limita a fornire il metodo statico main, che si occupa di instanziare il producer e usarlo per mandare messaggi:

public static void main(String[] args) {
try {
Producer producer = new Producer();

// default: 4 messages for queue
producer.sendMessages();

// 2 messages for topic
producer.setDestination(Producer.Type.TOPIC);
producer.setMessages(2);
producer.sendMessages();

producer.close();
}
catch(Exception ex) {
System.err.println("Production failed: " + ex.toString());
ex.printStackTrace();
System.exit(1);
}
System.exit(0);
}


Consumer

Le classi che consumano i messaggi sono organizzate in modo parallelo al produttore.

La classe Consumer si mette in attesa sulla risorsa destinazione JMS e continua a leggere messaggi fino a che non riceve il messaggio fittizio che indica il termine delle trasmissioni:

package synchConsumer;

import java.util.Hashtable;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.Queue;
import javax.jms.Topic;
import javax.jms.Connection;
import javax.jms.Session;
import javax.jms.Message;
import javax.jms.TextMessage;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Consumer {
private static final String AS_URL = "iiop://127.0.0.1:3700";
private static final String AS_CLASS = "com.sun.appserv.naming.S1ASCtxFactory";
private static final String CONNECTION = "jms/tConnectionFactory";
private static final String QUEUE = "jms/tQueue";
private static final String TOPIC = "jms/tTopic";

private static Queue queue;
private static Topic topic;
public enum Type {QUEUE, TOPIC};

private Type destType;
private Connection connection;
private Session session;

private static Context ctx;
static {
System.out.println("initializing the context");

Hashtable properties = new Hashtable(2);
properties.put(Context.PROVIDER_URL, AS_URL);
properties.put(Context.INITIAL_CONTEXT_FACTORY, AS_CLASS);
try {
ctx = new InitialContext(properties);
}
catch (NamingException ex) {
ex.printStackTrace();
}
}

public void setDestination(Type destType) {
this.destType = destType;
}

public Consumer() throws JMSException, NamingException {
destType = Type.QUEUE;

ConnectionFactory f = (ConnectionFactory)ctx.lookup(CONNECTION);
connection = f.createConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
}

private Destination getDestination() throws NamingException {
if(destType == Type.QUEUE) {
if(queue == null) {
System.out.println("Looking for JMS queue");
queue = (Queue)ctx.lookup(QUEUE);
}
return queue;
}
else { // Type.TOPIC
if(topic == null) {
System.out.println("Looking for JMS topic");
topic = (Topic)ctx.lookup(TOPIC);
}
return topic;
}
}

public void getMessages() throws JMSException, NamingException {
Destination dest = getDestination();

MessageConsumer consumer = session.createConsumer(dest);
connection.start();

System.out.println("Waiting for messages");
while (true) {
Message m = consumer.receive(1);

if (m != null) {
if (m instanceof TextMessage) {
TextMessage message = (TextMessage) m;
System.out.println("Reading message: " + message.getText());
}
else {
System.out.print("Not a text message: " + m.getClass().getName());
System.out.println(", teminating...");
break;
}
}
}
System.out.println("Messages received.");
}

public void close() throws JMSException {
System.out.println("Closing JMS connection");
if(connection != null)
connection.close();
}
}

La classe Main del package synchConsumer si limita anche qui a fornire il metodo statico main che si occupa di istanziare il consumer. L'utente che lancia questa classa decide se stare in ascolto sulla Queue o sul Topic specificando un argomento in ingresso:

public static void main(String[] args) {
boolean isTopic = (args.length == 1 && args[0].startsWith("t")) ? true : false;

try {
Consumer consumer = new Consumer();

// default: wait for queue messages
if(isTopic)
consumer.setDestination(Consumer.Type.TOPIC);
consumer.getMessages();

consumer.close();
}
catch(Exception ex) {
System.err.println("Consuming failed: " + ex.toString());
ex.printStackTrace();
System.exit(1);
}
System.exit(0);
}

Differenze

La differenza tra una Queue e un Topic é che la Queue tiene i messaggi in attesa di essere consumati, mentre il Topic distribuisce i messaggi a chi é in attesa.

Per vedere la cosa nei fatti basta eseguire producer e consumer in ordine diverso. Se per Queue cambiando l'ordine dell'esecuzione il risultato finale non cambia, per Topic se eseguiamo il producer prima del consumer i messaggi generati vanno persi.

Un client JMS remoto

Continuo a seguire il blog di Masoud Kalali e costruisco ora un client remoto per la coda JMS che abbiamo costruito in questo post.

Lo scopo di questo client sarà quello di mandare dei messaggi alla coda che verranno gestiti dal Message-Driven Bean che abbiamo scritto.

Un controllo preventivo

Conviene controllare che il servizio JMS sia configurato correttamente nel nostro application server.
Verifichiamo quindi che GlassFish sia in esecuzione, lanciamo l'Admin Console, selezioniamo il nodo Configuration, Java Message Service, e facciamo un ping del servizio.
Dovremmo ottenere un messaggio di successo. Nel mio caso avevo un errore, dovuto al fatto che, lavorando su un laptop, il mio indirizzo di rete era cambiato da quando avevo configurato GlassFish a quando mi sono messo a lavorare su questo esercizio.

Per superare questo problema (nel caso capiti anche al lettore) basta intervenire sulla lista di JMS Host, creando un nuovo host o modificando l'esistente, e aggiornando, se é il caso, il "Default JMS Host" per il JMS Service.

Il contesto e le librerie

Il punto nell'accesso remoto a JMS sta nel fatto che non abbiamo modo di accedere al contesto iniziale già inizializzato correttamente con i parametri opportuni per l'Application Server e dobbiamo usare alcune librerie per permettere la connessione.

Dobbiamo aggiungere perciò al nostro progetto una decina di jar che ci andiamo a prendere nelle librerie di glassfish, qui di fianco un immagine che riporta la libreria che mi sono creato per l'occorrenza e che include questi file:
  • appserv-rt.jar,
  • javaee.jar,
  • appserv-admin.jar,
  • appserv-launch.jar
  • appserv-ext.jar
  • appserv-deployment-client.jar
  • imqjmsra.jar,
  • jms.jar,
  • imq.jar,
  • imqbroker.jar.
I parametri per il contesto dobbiamo reperirli noi da qualche parte e poi passarli al contesto.

Insomma non possiamo fare come nel nostro client locale:

Context ctx = new InitialContext();

ma dobbiamo piuttosto passargli un paio di parametri.

Creiamo dunque un semplice progetto JavaSE, una sola classe Main dove il contesto come é membro statico che viene inizializzato staticamente:

...
private static final String AS_URL = "iiop://127.0.0.1:3700";
private static final String AS_CLASS = "com.sun.appserv.naming.S1ASCtxFactory";

private static Context ctx;
static {
Hashtable properties = new Hashtable(2);
properties.put(Context.PROVIDER_URL, AS_URL);
properties.put(Context.INITIAL_CONTEXT_FACTORY, AS_CLASS);
try {
ctx = new InitialContext(properties);
}
catch (NamingException ex) {
ex.printStackTrace();
}
}

Al nostro InitialContext abbiamo passato l'indirizzo del provider e il nome della classe specializzata per fare da Context Factory iniziale per il nostro provider. Nel mio caso ho specificato l'indirizzo locale, in un caso reale va messo l'indirizzo della macchina a cui ci vogliamo connettere.

Fatto questo, poco cambia rispetto all'uso in locale, ecco infatti la funzione main:

private static final String CONNECTION = "jms/tConnectionFactory";
private static final String QUEUE = "jms/tQueue";
...

public static void main(String[] args) {
try {
ConnectionFactory f = (ConnectionFactory)ctx.lookup(CONNECTION);
Connection c = f.createConnection();
Session session = c.createSession(false, Session.AUTO_ACKNOWLEDGE);

Queue q = (Queue)ctx.lookup(QUEUE);
MessageProducer messageProducer = session.createProducer(q);

for(int i=0; i<5; i++) {
TextMessage message = session.createTextMessage("A message /" + i);
System.out.println("JmsClient message: " + message.getText());
messageProducer.send(message);
}
}
catch(Exception ex){
ex.printStackTrace();
}

System.exit(0);
}

Come al solito, abbiamo bisogno di una sessione, che viene creata a partire da una connessione, che arriva dalla ConnectionFactory che é reperita mediante lookup dal contesto.
Il MessageProducer viene creato a partire dalla sessione facendo riferimento alla coda che viene reperita tramite lookup dal contesto.

Creiamo un messaggio di testo per mezzo della sessione e lo spediamo alla coda per mezzo dell'oggetto MessageProducer.

Rivediamo il codice completo:

package jmsclient;

import java.util.Hashtable;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Main {
private static final String AS_URL = "iiop://127.0.0.1:3700";
private static final String AS_CLASS = "com.sun.appserv.naming.S1ASCtxFactory";
private static final String CONNECTION = "jms/tConnectionFactory";
private static final String QUEUE = "jms/tQueue";

private static Context ctx;
static {
System.out.println("static ctor");

Hashtable properties = new Hashtable(2);
properties.put(Context.PROVIDER_URL, AS_URL);
properties.put(Context.INITIAL_CONTEXT_FACTORY, AS_CLASS);
try {
ctx = new InitialContext(properties);
}
catch (NamingException ex) {
ex.printStackTrace();
}
}

public static void main(String[] args) {
try {
ConnectionFactory f = (ConnectionFactory)ctx.lookup(CONNECTION);
Connection c = f.createConnection();
Session session = c.createSession(false, Session.AUTO_ACKNOWLEDGE);

Queue q = (Queue)ctx.lookup(QUEUE);
MessageProducer messageProducer = session.createProducer(q);

for(int i=0; i<5; i++) {
TextMessage message = session.createTextMessage("A message /" + i);
System.out.println("JmsClient message: " + message.getText());
messageProducer.send(message);
}
}
catch(Exception ex){
ex.printStackTrace();
}

System.exit(0);
}
}

Iniziare con JMS

C'é una certa carenza di esempi pratici che spieghino come iniziare ad usare JMS però, con un po' di pazienza, si trova qualcosa di interessante, come questo post del blog di Masoud Kalali su java.net, che risale al maggio 2006 ma é tuttora utile come riferimento.

Lo uso perciò per scrivere una semplice applicazione per JMS, usando NetBeans 6.7 come ambiente di sviluppo e GlassFish v2 come Application Server, il cui scopo é generare messaggi per mezzo di una pagina JSP e riceverli con un MDB (Message-Driver Enterprise Java Bean).

GlassFish

Ho aperto NetBeans, per prima cosa mi accerto che GlassFish sia attivo: vado sulla pagina dei services, e, sel server GlassFish non sta già correndo, ci faccio sopra un click di destra e do il comando "Run".

Poi guardo la console dell'amministratore di Glassfish: "View Admin Console". Il che equivale, se si sono tenuti i settaggi standard, ad aprire con il browser la pagina

http://localhost:4848/

Nella sezione Resources, JMS Resources seleziono il nodo Connection Factories. Ne creo una nuova, New JMS Connection Factory:
  • JNDI Name: jms/tConnectionFactory
  • Resource Type: javax.jms.ConnectionFactory
  • Description: Test JMS Connection
Per il resto lascio i valori di default indicati.

Passo ora alle Destination Resources e ne creo una nuova, New JMS Destination Resource:
  • JNDI Name: jms/tQueue
  • Physical Destination Name: tConnectionFactory
  • Resource Type: javax.jms.Queue
  • Description: Test JMS Destination
Nota che la destination é il nome (senza il prefisso jms) della connection factory che abbiamo appena creato. In pratica abbiamo legato la risorsa destinazione alla factory delle connessioni.

Torniamo a NetBeans, facciamo un refresh del server GlassFish e vediamo tra le JMS Resources la nostra connection factory e destination resource.

Enterprise Application

Creo una applicazione enterprise, a nome JmsOne, accetto i default e lascio che NetBeans generi la struttura del progetto.

La mia applicazione JmsOne contiene due moduli Java EE, un war per l'applicazione web e un jar per gli EJB. Questi due moduli mi vengono messi a disposizione anche come applicazioni a se stanti: JmsOne-ejb e JmsOne-war.

Message-Driven Bean

Costruiamoci per prima cosa il bean che riceve il messaggio da JMS. Faccio perciò un click di destra su JmsOne-ejb e dico di voler creare un nuovo Message-Driven Bean.

Scelgo un nome per il bean (OneMDBean) e per il package (beans), ma la cosa importante é la "Server Destination", ovvero la connessione a JMS, scelgo jms/tQueue, il nome JNDI della destinazione JMS che abbiamo creato poco fa.

Guardiamo il codice generato da NetBeans per OneMDBean, che vediamo essere una classe Java che implementa l'interfaccia MessageListener ed é annotata @MessageDriven specificando il riferimento a jms/tQueue.

Il grosso del lavoro é già fatto: quando JMS riceve un messaggio sulla coda, lo passa a questo bean invocando il metodo onMessage(), ci resta dunque da definire cosa deve fare il bean di questo messaggio.

In questa implementazione il bean fa solo un po' di logging sullo standard output, così che io possa vedere, nella finestra dell'output di GlassFish v2, se tutto ha funzionato come ci aspettiamo:

public void onMessage(Message message) {
System.out.println(this.getClass().getSimpleName() + ".onMessage()");

try {
if(message instanceof TextMessage) {
TextMessage msg = (TextMessage)message;
System.out.println("Message received: " + msg.getText());
}
else {
System.out.println("Unexpected message type: " +
message.getClass().getName());
}
}
catch (Throwable te) {
te.printStackTrace();
}
}

Nota che ci aspettiamo che il messaggio sia un TextMessage, se non lo é non sappiamo come gestirlo, e segnaliamo la situazione anomala.

Web Application

Costruiamo una piccola applicazione web per generare i messaggi che mandiamo su JMS.

L'interfaccia utente é una semplice pagina html, che richiama una servlet:

<pre>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JMS Message Producer</title>
</head>
<body>
<h1>Enter a message:</h1>
<form action="SendMessage">
<input type="text" name="message" value="A message" size="30" />
<input type="submit" value="Send The message" name="send" />
</form>
</body>
</html>
</pre>


Creiamo quindi la servlet che viene richiamata dalla pagina html. Faccio un click di destra sulla applicazione web JmsOne-war, "New", "Servlet..." e specifico come "Class Name" SendMessage (é l'action della form della nostra pagina html), come "Package" servlet (o quello che mi par meglio), passo alla pagina successiva, accetto i valori di default proposti da NetBeans per il nome della servlet e il pattern URL, e confermo.

Modifichiamo ora il metodo processRequest(). Perché mandi il messaggio sulla coda JMS e mostri un report all'utente.

protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println(this.getClass().getSimpleName() + ".processRequest()");

response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
String message = request.getParameter("message");
String queue = sendMessage(message);
System.out.println("Message sent: " + message);

// servlet report
out.println("<html>");
out.println("<head>");
out.println("<title>Message Sending Report</title>");
out.println("</head>");
out.println("<body>");
out.print("The message "<i>" + message + "</i>"" ));
out.print(" has been sent to the JMS queue <b>");
out.println(queue + "</b>.");
out.println("</body>");
out.println("</html>");
}
catch(Exception ex){
ex.printStackTrace();
}
finally {
out.close();
}
}

Il lavoro specifico per JMS l'abbiamo raccolto nel metodo sendMessage(), che prende in input il messaggio e ritorna il nome della coda a cui viene mandato, che vediamo qui di seguito:

private final String FACTORY = "jms/tConnectionFactory";
private final String QUEUE = "jms/tQueue";

private String sendMessage(String msg) throws Exception {
Context ctx = new InitialContext();
ConnectionFactory factory = (ConnectionFactory)ctx.lookup(FACTORY);
Connection connection = factory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

Queue queue = (Queue)ctx.lookup(QUEUE);
MessageProducer messageProducer = session.createProducer(queue);

TextMessage message = session.createTextMessage(msg);
messageProducer.send(message);

return queue.getQueueName();
}

Dal contesto iniziale accedo alla connection factory, che mi permette di creare una connessione, che uso per creare una sessione. Accedo anche alla coda via contesto iniziale, e posso quindi crearmi l'oggetto MessageProducer che uso per mandare il messaggio.

Ritorno il nome della coda per fare un po' di logging.

E questo é tutto.

Testing

Eseguiamo l'applicazione, il che equivale a lanciare la nostra applicazione web, ovvero ad accedere http://localhost:8080/JmsOne-war/ (se abbiamo tenuto i default suggeriti per GlassFish e per JmsOne-war), ci viene mostrata dunque index.html. Cliccando sul bottone mandiamo il messaggio a JMS che, a sua volta, chiama OneMDBBean.onMessage(), come possiamo vedere dal log di GlassFish.