Il background del modello del messaggio

Nella messaggistica enterprise i messaggi sono entità che consistono di una intestazione (header) e un corpo (body).

L'header contiene i campi che sono usati per il routing e l'identificazione del messaggio; il body contiene i dati che sono spediti.

Ogni sistema tende a implementare questo modello in un modo diverso, dove le maggiori differenze sono nel contenuto e nella semantica dei campi dell'intestazione.

Obiettivi del modello del messaggio JMS
  • Fornire una API unica che sia adatta alla creazione di messaggi che sia in linea con il formato usato da applicazioni esistenti non JMS
  • Supportare lo sviluppo di applicazioni eterogenee che operino su differenti macchine, sistemi operativi, linguaggi di programmazione.
  • Supportare l'uso di oggetti Java
  • Supportare XML
Messaggi JMS

I messaggi JMS sono composti da queste parti:
  • Header: i campi dell'header contengono valori usati dai clienti e dal provider per identificare e fare il routing dei messaggi.
  • Proprietà: oltre ai campi standard dell'header é possibile aggiungere campi aggiuntivi all'header.
    • Proprietà specifiche dell'applicazione
    • Proprietà standard
    • Proprietà specifiche del provider
  • Body: sono definiti da JMS svariati tipi di body che coprono la maggior parte dei tipi di messaggi correntemente in uso.

Head First - Design Patterns

Abbandono la rilettura di Design Patterns, Elements of Reusable Object-Oriented Software di Erich Gamma et al., comunemente noti come the Gang of Four, ottimo libro edito dalla Addison-Wesley, per passare alla prima lettura di Head First - Design Patterns dei Freeman, edito da O'Reilly che, a quanto ho visto sfogliandolo rapidamente, sembra una riscrittura aggiornata ai tempi (é del 2004) del classico dei GoF. Il linguaggio utilizzato per implementare gli esempi é Java.

I toni solo leggeri, ma il contenuto mi pare ci sia e sia molto ben trattato. Unico appunto che mi sento di fare é che é rivolto a un pubblico molto americano, e quindi una parte dei giochi di parole e dei riferimenti extra informatici restano abbastanza oscuri al lettore europeo.

Una buona citazione da questo libro penso sia questa: "Un bravo progettista software pensa a come creare design flessibili che siano manutenibili e che possano adattarsi al cambiamento".

Request/Reply

Il campo JMSReplyTo specificato nell'header del messaggio permette di specificare al Destination dove debba essere mandata una risposta.

Il campo JMSCorrelationID della risposta può essere usato per dare un riferimento a quale fosse la richiesta originale. Parleremo meglio più avanti dei campi nell'header dei messaggi.

Inoltre JMS fornisce anche funzionalità per create code e topic temporanei che possono essere usati come destinazione univoca per le risposte.

JMS definisce alcune classi di base per la gestione di Request/Reply, implementazioni più specializzate sono lasciate alla cura del provider JMS e dei client.

Multithreading

Per ridurre la complessità del sistema JMS richiede che sia supportato l'accesso concorrente solo per quegli oggetti che viene naturale pensare vengano condivisi da client che operano in multithreading.

Gli oggetti JMS che supportano uso concorrente sono: ConnectionFactory, Connection e Destination.

Gli oggetti JMS progettati per essere acceduti sequenzialmente da diversi thread sono: Session, MessageProducer, MessageConsumer.

Per gli oggetti di tipo Session sono definite da JMS alcune specifiche regole per limitarne l'uso in condizioni di concorrenza. Ne parleremo in un post successivo, quando ne sapremo abbastanza di JMS per poterne parlare.

Ci sono due motivi per limitare l'accesso in concorrenza a oggetti Session.
  1. JMS supporta le transazioni via Session, ed é molto difficile implementare transazioni in condizioni di multithreading.
  2. Session supporta il consumo asincrono di messaggi. Implementare la gestione del consumo asincrono di messaggi in ambiente multithreading sarebbe estremamente complesso sia per il server JMS che per il client.
Imporre che la Session non sia multithreading ne semplifica l'uso per il client tipico. Un client più evoluto può ottenere il livello di concorrenza desiderato usando più sessioni.

Command (e Null Object)

Pattern descritto in Design Pattern.

Lo scopo del pattern Command (aka Action, Transaction) é incapsulare una richiesta in un oggetto, in questo modo si parametrizzano clienti con differenti richieste e si supportano operazioni reversibili.

Si usa Command:
  • per parametrizzare oggetti con una azione da eseguire. Nota che l'uso di Command in questo modo ricorda quello del meccanismo di callback, dove una funzione viene registrata per essere eseguita in seguito;
  • per specificare, mettere in coda, ed eseguire comandi successivamente;
  • per supportare l'undo di una operazione;
  • per permettere il log di cambiamenti in modo da poterli riapplicare in caso di bisogno. Aggiungendo i metodi load() e store() all'interfaccia si può tenere una traccia persistente dei cambiamenti;
  • per implementare le transazioni.
Esempio

Nel nostro mondo fantasy la potente corporazione dei maghi ha l'esclusiva sull'uso della magia. Ma c'é un modo per i non maghi di eseguire alcuni sortilegi: comprando una bacchetta magica su cui un mago ha registrato alcune magie ben delimitate.

Ad esempio si può chiedere a un mago di registrare l'incantamento che fa aprire una porta ben specificata e quello che la fa chiudere. E' prevista anche la possibilità di annullare la magia, nel caso la si sia invocata per sbaglio.

Alla base della nostra implementazione c'é l'interfaccia Command, che viene definita in questo modo:

package gof.command;

public interface Command {
public void execute();
public void undo();
}

E la classe Door, che rappresenta una porta, con i metodi che la fanno aprire e chiudere:

package gof.command;

public class Door {
private final String name;
private boolean open = false;

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

public String getName() {
return name;
}

public boolean isOpen() {
return open;
}

public void open() {
open = true;
}

public void close() {
open = false;
}

@Override
public String toString() {
return getName() + " is " + (isOpen() ? "open" : "closed");
}
}

La magia che ci permette di aprire una porta é descritta dalla classe OpenDoorSpell:

package gof.command;

public class OpenDoorSpell implements Command {
Door door;

public OpenDoorSpell(Door door) {
this.door = door;
}

public void execute() {
System.out.println(door.getName() + ": Open Sesame!");
door.open();
}

public void undo() {
System.out.println(door.getName() + ": Undo Open Sesame!");
door.close();
}
}

L'incantamento che ci permette di chiuderla é questo:

package gof.command;

public class CloseDoorSpell implements Command {
Door door;

public CloseDoorSpell(Door door) {
this.door = door;
}

public void execute() {
System.out.println(door.getName() + ": Close Sesame!");
door.close();
}

public void undo() {
System.out.println(door.getName() + ": Undo Close Sesame!");
door.open();
}
}

Vale la pena di crearsi anche una classe che rappresenta la non-magia. E' spesso utile avere un oggetto che non fa nulla, quando sappiamo a prescindere cosa fare nelle situazioni in cui non c'é un oggetto di quel tipo a disposizione. Nel caso delle magie é semplice: non succede nulla. L'uso di un oggetto che ha in sé la conoscenza di cosa fare in caso non vi sia alcun oggetto disponibile invece di lasciare all'utilizzatore il compito di decidere cosa fare in questa situazione viene descritto come il pattern del Null Object.

Null Object viene spesso implementato come un Singleton, come facciamo anche noi in questo caso:

package gof.command;

public class NullSpell implements Command {
private static NullSpell instance = new NullSpell();

public static NullSpell getInstance() {
return instance;
}

private NullSpell() {}

public void execute() {
System.out.println(this.getClass().getSimpleName() + ": no spell");
}

public void undo() {
System.out.println(this.getClass().getSimpleName() + ": nothing happen");
}
}

La nostra semplice bacchetta magica sarà un oggetto di questa classe:

package gof.command;

public class EasyWand {
public enum Type { ONE, TWO };
private static final int NR_SPELLS = 2;
private Command[] spells = new Command[NR_SPELLS];

public EasyWand() {
Command nothing = new NullSpell();

for(int i = 0; i < NR_SPELLS; ++i) {
spells[i] = nothing;
}
}

public void setSpell(Type type, Command spell) {
spells[type.ordinal()] = spell;
}

public void spell(Type type) {
spells[type.ordinal()].execute();
}
}

Notiamo che inizialmente le magie contenute nella bacchetta sono il Null Object. Come da aspettative, la bacchetta non fa nulla, bisogna esplicitamente associare magie alla bacchetta.

Test

Per prima cosa costruiamo una bacchetta secondo la richiesta del nostro utente, che vuole poter aprire e chiudere una porta di cui ci passa un riferimento:

private static void easyWand(Door door) {
Command open = new OpenDoorSpell(door);
Command close = new CloseDoorSpell(door);
EasyWand wand = new EasyWand();

System.out.println("A new wand, no spell on it");
wand.spell(EasyWand.Type.ONE);

wand.setSpell(EasyWand.Type.ONE, open);
wand.setSpell(EasyWand.Type.TWO, close);

System.out.println("The wand is ready to be used");
// ...

L'output di questa prima parte dovrebbe essere questo:

A new wand, no spell on it
NullSpell: no spell
The wand is ready to be used

Passiamo dunque al test vero e proprio della bacchetta. Apriamo, chiudiamo la porta, e poi cerchiamo due volte di fare l'undo. Siccome nella nostra implementazione é possibile fare l'undo solo dell'ultima operazione, il secondo undo non avrà successo:

// ...
System.out.println("Opening " + door.getName());
wand.spell(EasyWand.Type.ONE);
System.out.println(door.toString());

System.out.println("Closing " + door.getName());
wand.spell(EasyWand.Type.TWO);
System.out.println(door.toString());

System.out.println("Undoing Closing " + door.getName());
wand.undo();
System.out.println(door.toString());

wand.undo();
System.out.println(door.toString());
}

L'output atteso é il seguente:

Opening Front Door
Front Door: Open Sesame!
Front Door is open
Closing Front Door
Front Door: Close Sesame!
Front Door is closed
Undoing Closing Front Door
Front Door: Undo Close Sesame!
Front Door is open
NullSpell: nothing happen
Front Door is open

Sviluppare applicazioni JMS

In generale una applicazione JMS consiste di uno o più client JMS che scambiano messaggi. L'applicazione può includere anche client non-JMS che facciano uso della API nativa del provider JMS invece che usare JMS.

Una applicazione JMS può essere progettata e rilasciata come un'unità ma spesso i client JMS sono aggiunti incrementalmente a una applicazione esistente.

La definizione del messaggio usato da una applicazione può venire sia dalla parte JMS dell'applicazione sia dalla non-JMS.

Sviluppo di un client JMS

Un tipico client JMS esegue la seguente procedura di setup JMS:
  • trova un oggetto ConnectionFactory via JNDI;
  • trova uno o più oggetti Destination via JNDI;
  • usa la ConnectionFactory per creare una JMS Connection con la consegna dei messaggi non abilitata;
  • usa la Connection per creare una o più JMS Session;
  • usa la Session e le Destination per creare i MessageProducer e i MessageConsumer necessari;
  • far partire la consegna dei messaggi sulla Connection.
A questo punto il client é pronto per produrre e consumare messaggi.

Proxy

Pattern descritto in Design Pattern.

Lo scopo del pattern Proxy (aka Surrogate) é quello di fornire un segnaposto per un altro oggetto per controllarne l'accesso.

Ci sono svariati utilizzi per il proxy:
  • Il proxy remoto offre una rappresentazione locale di un oggetto che in realtà risiede altrove. In Java un proxy di questo tipo lo si trova nell'implementazione del modello sottostante alla RMI (Remote Method Invocation) dove il proxy é chiamato stub e l'oggetto remoto a cui si riferisce skeleton.
  • Il proxy virtuale permette di ritardare la creazione di un oggetto costoso in termini di risorse.
  • Il proxy protettivo permette di diversificare l'accesso ad un oggetto a seconda delle diverse situazioni.
  • La smart reference sostituisce il semplice puntatore con una struttura che permette operazioni aggiuntive. Tipico esempio é lo smart pointer comunemente usato in C++.
Esempio

Nel nostro mondo fantasy é prevista l'esistenza di un oracolo a cui é possibile sottoporre delle domande che otterranno, dopo adeguata meditazione, una risposta.

L'interfaccia che descrive l'oracolo é molto semplice:

package gof.proxy;

public interface Oracle {
public String getAnswer(String question);
}

L'implementazione che diamo per l'oracolo reale é decisamente semplicistica, ma l'idea che c'é dietro é che si tratti di codice molto complesso che richiede grandi risorse e un notevole tempo per essere eseguito:

package gof.proxy;

public class RealOracle implements Oracle {
private String answer = "I don't know";

public RealOracle() {
try{Thread.sleep(500);} catch(InterruptedException ie) {}
System.out.println("[Oracle] I'm ready.");
}

public String getAnswer(String question) {
System.out.println("[Oracle] You asked me: " + question);
try{Thread.sleep(2000);} catch(InterruptedException ie) {}

return answer;
}
}

Per verificare il funzionamento del nostro oracolo scriviamo questa funzione:

private String direct(String question) {
System.out.println("My question is: " + question);

Oracle oracle = new RealOracle();
return oracle.getAnswer(question);
}

Che dovrebbe dare come output:

My question is: What is the sense of life?
[Oracle] I'm ready.
[Oracle] You asked me: What is the sense of life?
The oracle's answer is: I don't know

A parte la delusione per la risposta che otteniamo dall'oracolo, il fatto fastidioso é che restiamo appesi in attesa del risultato senza poter far altro.
Questo ci fa pensare di usare un proxy al nostro oracolo, che permetta al cliente di fare altro mentre l'oracolo pensa:

package gof.proxy;

public class OracleProxy implements Oracle {
private OracleThread oracle = null;

public String getAnswer(String question) {
if(oracle == null) {
oracle = new OracleThread(question);
oracle.start();
}

String answer = oracle.getAnswer();

if(answer == null) {
System.out.println("Oracle needs more time to answer.");
}
else {
try { oracle.join(); } catch (InterruptedException ex) {}
}
return answer;
}
}

Il nostro oracolo é stato messo in un altro thread, il proxy lo interroga e, se ha elaborato una risposta, la passa al chiamante, altrimenti ritorna un null dopo aver stampato un messaggio che avverte che l'oracolo é ancora perso nelle sue elucubrazioni.

Questo é il codice della classe d'appoggio per l'oracolo multithreading:

package gof.proxy;

public class OracleThread extends Thread {
private RealOracle oracle = new RealOracle();
private String answer = null;
private String question;

public OracleThread(String question) {
this.question = question;
}

public String getAnswer() {
return answer;
}

@Override
public void run() {
answer = oracle.getAnswer(question);
}
}

La funzione che ci permette di testare il proxy é questa:

private String proxy(String question) {
System.out.println("My question is: " + question);

Oracle oracle = new OracleProxy();
String answer = null;
do {
try { Thread.sleep(500); } catch(InterruptedException ie) {}
System.out.println("I have time to do other stuff...");
} while((answer = oracle.getAnswer(question)) == null );

return answer;
}

Che dà questo risultato:

My question is: What is the sense of life?
I have time to do other stuff...
[Oracle] I'm ready.
Oracle needs more time to answer.
[Oracle] You asked me: What is the sense of life?
I have time to do other stuff...
Oracle needs more time to answer.
I have time to do other stuff...
Oracle needs more time to answer.
I have time to do other stuff...
Oracle needs more time to answer.
I have time to do other stuff...
The oracle's answer is: I don't know

OK. Il risultato finale é lo stesso, però almeno il cliente dell'oracolo ha avuto la possibilità di impiegare più utilmente il suo tempo nell'attesa della risposta.

Due stili per i messaggi

Una applicazione JMS può usare due modalità diverse per scambiare messaggi: la point-to-point (PTP) o la publisher/subscriber (Pub/Sub).

Naturalmente é possibile usare entrambe le modalità nella stessa applicazione, basta far riferimento a destinazioni diverse.

Nell'ambito JMS, la destinazione PTP é chiamata Queue (coda) e quella Pub/Sub Topic.

In JMS é possibile usare sia interfacce generiche sia specializzate per gestire i differenti elementi del modello.

Interfacce comuniPTPPub/Sub
ConnectionFactoryQueueConnectionFactoryTopicConnectionFactory
ConnectionQueueConnectionTopicConnection
DestinationQueueTopic
SessionQueueSessionTopicSession
MessageProducerQueueSenderTopicPublisher
MessageConsumerQueueReceiver / QueueBrowserTopicSubscriber

Come consigliano le buone pratiche di progettazione Object-Oriented, bisognerebbe cercare di lavorare con le astrazioni più generiche, e quindi con le interfacce comuni, per permettere una maggiore indipendenza del codice scritto dagli oggetti sottostanti.

Ecco una breve descrizione delle interfacce:

  • ConnectionFactory: un oggetto amministrato usato dal client per creare una connessione;
  • Connection: una connessione a JMS;
  • Destination: un oggetto amministrato che incapsula l'identità della destinazione di un messaggio;
  • Session: un contesto single-threaded usato per mandare e ricevere messaggi;
  • MessageProducer: un oggetto creato da una sessione che viene usato per mandare messaggi a una destinazione;
  • MessageConsumer: un oggetto creato da una sessione che viene usato per ricevere messaggi da una destinazione.
Il flusso di esecuzione segue solitamente uno schema come questo:

La ConnectionFactory crea una Connection, che crea una Session, che crea un Message.

La Session crea anche un MessageProducer che manda un Message a una Destination, e crea pure un MessageConsumer che riceva un Message da una Destination.

Si dice che una applicazione JMS consuma un messaggio quando un cliente manda una ricevuta di accettazione per un messaggio. Si dice che produce un messaggio quando il cliente manda un messaggio ad una destinazione JMS.

Amministrazione

Ci sono due tipi di oggetti amministrati:
  • ConnectionFactory: usato da un client per creare una connessione ad un provider;
  • Destination: usato da un client per specificare la destinazione di una messaggio che sta mandando e l'origine di un messaggio che riceve.
Gli oggetti amministrati sono messi in un namespace JNDI da un amministratore. Un client JMS dichiara nella sua documentazione gli oggetti amministrati JMS che richiede e come i nomi JNDI di questi oggetti gli devono essere messi a disposizione.

Cos'é una applicazione JMS?

Il documento "Java Message Server Specification", un pdf di 140 pagine, é disponibile sul sito Sun.

Una applicazione JMS é composta da quattro parti:
  • Client JMS: i programmi Java che mandano e ricevono messaggi.
  • Messaggi: ogni applicazione definisce un insieme di messaggi che sono usati per comunicare informazioni tra i suoi client.
  • Provider JMS: un sistema di messaggistica che implementa JMS in aggiunta alle altre funzionalità amministrative e di controllo richieste da un prodotto per la gestione della messaggistica.
  • Oggetti amministrati: sono oggetti JMS preconfigurati creati dall'amministratore per essere utilizzati dai client.