Introduzione
Da quando la specifica EJB è stata presentata
al grande pubblico ha riscosso un grosso successo grazie
alle sue caratteristiche che in ottica enterprise ne
fanno lo strumento preferito per lo sviluppo di applicazioni
distribuite, transazionali, sicure.
Il concetto fondamentale di questa piattaforma è
rendere disponibili oggetti remoti i cui metodi possano
essere invocati in modo sincrono da client remoti, sotto
una stretta policy di sicurezza e controllo da parte
del container.
Con il dilagare delle reti geografiche e l'allargarsi
del concetto di distribuito, si è fatto sempre
più pressante il bisogno di eseguire operazioni
in modo asincrono sotto il vincolo di operare in un
contesto sicuro e transazionale.
Fondamentalmente la modalità asincrona si traduce
in pratica nella possibilità di scambiare messaggi
in grado poi di scatenare altre operazioni o eventi.
Java già da tempo forniva con la JMS API, la
possibilità di interagire con middleware di messaggistica
distribuiti (i cosiddetti MOM), anche se ad oggi mancava
la possibilità di gestire in modo automatico
lo scambio di messaggi in chiave J2EE (o più
precisamente all'interno di EJB).
Anche se tale collegamento poteva essere realizzato
in modo più o meno manuale, mancava il la possibilità
di poter disporre di un bean (session o entity) in grado
di restare dormiente fino alla ricezione di un messaggio
e di svegliarsi automaticamente per effettuare determinate
operazioni.
Con la specifica 2.0 questa carenza è stata colmata.
Adesso infatti, grazie alla presenza dei cosiddetti
Message Driven Bean (MDB) è possibile mettere
in comunicazione tutto il mondo J2EE transazionale.
In questo articolo tratteremo i MDB senza entrare nel
dettaglio della JMS API; probabilmente questa trattazione
potrà risultare incompleta per quanto riguarda
la parte di gestione dei messaggi in senso stretto.
Essendo questi argomenti che esulano dalla teoria dei
MDB, per chi fosse interessato a maggiori approfondimenti
su JMS, rimandiamo alla bibliografia ed in particolare
a [JMS].
I
Message Driven Bean
Un MDB è un bean in cui il concetto di interfaccia
remota è del tutto assente, ed il solo meccanismo
di interazione con il mondo esterno è quello
basato sulla ricezione di messaggi JMS. Si tratta quindi
di componenti stateless transaction aware, il cui unico
compito è quello di processare messaggi JMS provenienti
da topic o code.
Per questo motivo un MDB è un enterprise bean
che funziona come ascoltatore di una particolare coda
o topic, in modo del tutto simile a quello che potrebbe
fare un listener JMS scritto ad hoc (vedi [MBJMS-2])
Il valore aggiunto che si ha dall'usare un MDB al posto
di una normale applicazione client JMS è che
in questo caso l'ascoltatore, essendo un enterprise
bean, vive completamente all'interno di un contesto
sicuro, transazionale e fault tolerant. Un altro grosso
vantaggio è che rispetto ad un semplice client,
essi sono in grado di processare i messaggi in modo
concorrente: similmente a quanto accade per i servlet,
lo sviluppatore si deve concentrare sullo sviluppo di
un solo componente o meglio nella logica di processamento
di un messaggio. Al resto pensa il container che potrà
eseguire i vari bean in modo concorrente.
Un MDB può ricevere centinaia di messaggi e processarli
anche tutti nello stesso istante, dato che il container
eseguirà più istanze dello stesso MDB
gestiti in modalità di pool.
In modo del tutto analogo a quanto avviene per i session
o gli entity, anche gli MDB sono gestiti in tutto il
loro ciclo di vita e durante l'esecuzione dei metodi
di business dal container. Per questo, sebbene essi
siano in grado di processare messaggi JMS, l'effettiva
ricezione viene gestita dal container, che poi in modalità
di callback inoltra la chiamata al bean.
Utilizzare i MDB
Per chi si avvicina per la prima volta a MDB uno dei
dubbi più ricorrenti è cercare di comprendere
come e quando un MDB debba essere utilizzato.
La modalità corretta di utilizzo di un MDB è
anche la più semplice che si possa immaginare.
Se in un contesto EJB gli entity rappresentano entità
astratte, i session degli oggetti di servizio in cui
eseguire metodi di business logic, gli MDB sono oggetti
il cui solo scopo è quello di operare come intermediari
per l'esecuzione della business logic in concomitanza
di determinati eventi (ovvero all'arrivo di un messaggio).
Per questo motivo un MDB dovrebbe implementare solamente
la logica di gestione del messaggio e dell'analisi delle
sue caratteristiche e dar vita così ad un flusso
di operazioni invocando la business logic contenuta
nei metodi remoti di un sessione bean: ad esempio un
MDB potrebbe estrapolare i campi dall'header del messaggio
e successivamente stabilire quale metodo di un session
invocare passandogli gli opportuni parametri.
I MDB possono avere campi di istanza come i session,
e sono mantenuti durante il loro ciclo di vita, ma essendo
stateless e non potendo essere associati ad un client
in particolare, devono essere gestiti dal container
in modo autonomo secondo la logica del pool.
I MDB dall'interno
Per definire un Message Driver Bean è necessario
implementare l'interfaccia MessageDrivenBean e la MessageListener.
La prima definisce essenzialmente i metodi di callback
sui quali il container interviene per effettuare alcune
delle principali operazioni durante il ciclo di vita
del bean. La definizione di tale interfaccia è
riportata qui di seguito
public
abstract interface MessageDrivenBean extends EnterpriseBean{
void ejbRemove() throws EJBException;
void setMessageDrivenContext(MessageDrivenContext
mdc)
throws
EJBException;
}
Il
metodo setMessageDrivenContext() invocato dal container
per impostare il contesto all'inizio del ciclo di vita
dell'MDB
MessageDrivenContext
ejbContext;
Context jndiContext;
public
void setMessageDrivenContext(MessageDrivenContext mdc)
{
ejbContext = mdc;
try {
jndiContext = new InitialContext();
}
catch(NamingException ne) {
throw new EJBException(ne);
}
}
invece
il metodo ejbRemove() viene eseguito al termine del
ciclo di vita del bean e può essere utile proprio
per ripulire i campi del bean, oltre a chiudere eventuali
connessioni con sistemi sottostanti. Per quanto detto
nel paragrafo precedente è bene ricordare che
un MDB non dovrebbe effettuare connessioni con altri
sottosistemi.
Da tenere presente che il metodo potrebbe non essere
invocato nel caso in cui si generi una eccezione da
qualche parte. In tal caso il bean viene immediatamente
rimosso dalla memoria e la transazione annullata.
L'interfaccia MessageDrivenContext deriva dalla EJBContext
alla quale non aggiunge nessun metodo ma anzi ne nasconde
quei metodi non utilizzabili perché non avrebbe
senso. Si tratta dei metodi come getEJBHome() e getEJBLocalHome()
(non esiste una interfaccia home o local home), o getCallerPrincipal()
o isCallerInRole() (non esiste nessun client invocante
per un MDB). Dato che un MDB è asincrono il concetto
di CallerRole perde di significato dato che il contesto
di sicurezza per ovvi motivi non può e non avrebbe
senso, essere propagato dal client JMS al bean.
Un MDB inoltre deve implementare l'interfaccia MessageListener
che fornisce al bean la facoltà di interagire
con messaggi di tipo JMS. Ecco la sua definizione
public
abstract interface MessageListener {
void onMessage(Message message);
}
Come
si può notare essa è molto semplice, tanto
da far sorgere il dubbio sulla sua effettiva utilità:
ci si potrebbe chiedere infatti perché definire
una interfaccia apposita per la definizione di un solo
metodo. Si sarebbe potuto inserire onMessage() direttamente
nella interfaccia MessageDrivenBean rendendo il tutto
più compatto e semplice.
In realtà nella primissima specifica di EJB 2.0
la scelta fatta fu proprio questa: successivamente però
ci si rese conto che questo vincolava un MDB alla possibilità
di interagire solamente con messaggi JMS, mentre in
futuro potrebbe essere utile creare listener per messaggi
di posta elettronica (SMTP listener), JAXM (Java API
for XML Messaging) o ebXML. Questi nuovi sistemi richiederebbero
differenti implementazioni del metodo onMessage().
Il metodo onMessage() è il luogo dove inserire
la business logic del bean: esso viene invocato in modalità
di callback non appena il server riceve un messaggio
dal topic o queue su cui il bean si è registrato.
Non è possibile prevedere a priori in alcun modo
quando ciò avvenga, per cui lo sviluppatore deve
considerare tale esecuzione del tutto passiva.
Per l'implementazione della business logic al suo interno
è altresì importante anche tener presente
la filosofia di base sia di JMS che dei MDB.
Un messaggio infatti deve essere considerato come uno
strumento per propagare eventi sulla base dei quali
poi verranno effettuare determinate operazioni. In una
architettura distribuita JMS può essere considerato
come uno dei collanti utilizzabili per tenere insieme
una struttura eterogenea (in [MBSHOP-6] viene mostrato
come utilizzare i messaggi per coordinare più
web-application ed applicazioni EJB).
Il metodo onMessage() non dovrà quindi eseguire
direttamente le funzionalità core di una applicazione
EJB, ma piuttosto demandare altri elementi (tipicamente
session bean) l'esecuzione della business logic. Con
una metafora piuttosto elementare, un MDB può
essere considerato come il controllore di una squadra
di operai: è lui che riceve gli ordini dall'alto,
ma non esegue nessuna operazione concreta, se non quella
di far partire il lavoro dei suoi subordinati.
Infine
da tenere presente che un MDB è una classe Java
e come tale può inviare messaggi JMS. Questo
aspetto non è molto interessante in ottica EJB,
dato che l'implementazione della logica di spedizione
verso topic o code non è dissimile da quella
presente in ogni altri client Java che si interfacci
con un JMS service.
Deployment
Descriptor XML
Come
ogni altro bean enterprise, anche gli MDB utilizzano
per il deploy un meccanismo basato su XML, il quale
serve per definire alcune informazioni più o
meno fisse (il nome del bean) ma altre importanti di
tipo variabile (nome del topic/coda, filtri di selezione
ed altro ancora).
Il tag principale è sicuramente <message-driven>
il quale, definito all'interno di <enterprise-beans>,
definisce il nome del bean (tag <ejb-name>), la
sua classe (tag <ejb-class>) e il modello transazionale
(tag <transaction-type>). Per ovvi motivi all'interno
della coppia <message-driven>
</message-driven>
non sono presenti tag relativi alle interfacce (es.
<home>
</home> o <remote>
</remote>) dato che queste non esistono.
Il
tag <message-selector>
Il tag <message-selector> è particolarmente
importante e tipico di un MDB: esso permette di specificare
un selezione sulla coda/topic, tramite il quale filtrare
quei messaggi che dovranno essere ricevuti dal bean.
Il concetto di filtro in JMS è molto utile sia
per ottimizzare il traffico di rete (riducendo drasticamente
il numero di messaggi da inviare ad un determinato ascoltatore),
sia per creare ascoltatori più specifici e quindi
migliorare l'architettura complessiva ed il codice applicativo.
Un filtro è una regola definita sul server tramite
la quale solo determinati messaggi verranno recapitati
al client JMS e quindi al MDB.
La sintassi con cui si definisce un selector è
molto simile all'SQL-92, e di seguito sono riportati
alcuni esempi presentati in [EJB3] a cui si rimanda
per ulteriori approfondimenti.
<message-selector>
<![CDATA[PhysicianType IN ('Chiropractic','Psychologists','Dermatologist')
AND PatientGroupID LIKE 'ACME%']]>
</message-selector>
<message-selector>
<![CDATA[ TotalCharge >500.00 AND ((TotalCharge
/ItemCount)>=75.00) AND State IN ('MN','WI','MI','OH')]]>
</message-selector>
Molto
spesso per poter definire un message selector si devono
utilizzare caratteri speciali, come il segno maggiore
di (>), minore di (<) o come dollaro e asterisco.
Dato che tali caratteri non sono consentiti all'interno
di un normale file XML, per poterli inserire senza che
il parser generi un errore, è necessario includere
tutte le espressioni che contengono i caratteri speciali
all'interno di particolari sezioni CDATA. In modo simile
a quanto avviene con gli statements <ejb-ql> di
EJB QL, tutto quello che è inserito all'interno
di una sezione CDATA verrà semplicemente ignorato
dal parser XML.
Per chi fosse interessato ad approfondire ulteriormente
la teoria JMS e sui filtri si rimanda a [JMS] oltre
che alla serie di articoli pubblicati su MokaByte da
Stefano Rossini ([MBJMS-2]).
Il
tag <acknowledge-mode>
Quando un messaggio viene inviato da un client verso
una coda o topic di un JMS provider, quest'ultimo provvede
ad inoltrarlo verso tutti i listener registrati su tale
destinazione. La consegna del messaggio viene certificata
dal ricevente che invia al provider una notifica di
ricezione, una specie di ricevuta di ritorno. Nel caso
in cui il provider non riceva risposta in un determinato
lasso di tempo, il messaggio verrà nuovamente
inoltrato fino a quando la notifica non sia inviata.
In
ambito EJB è il container che comunica al service
JMS che un MDB ha ricevuto e consumato correttamente
il messaggio, ovvero il metodo onMessage() è
stato eseguito correttamente.
In fase di deploy è possibile scegliere la modalità
di notifica della ricezione (immediata o ritardata).
Nel caso in cui vi sia una transazione in ballo, l'impostazione
scelta per la notifica della ricezione viene ignorata,
dato che la notifica avviene in funzione dell'esecuzione
della transazione stessa: se la transazione avviene
con successo la notifica viene effettuata automaticamente,
in caso contrario non viene inviata nessuna notifica.
Se la transazione è di tipo container managed,
come nella maggior parte dei casi, l'impostazione scelta
nel deploy viene ignorata dal container che procede
secondo le policy interne del server.
Nel caso in cui la transazione sia a carico del bean
o NotSupported, allora viene utilizzato il valore di
acknowledge-mode, che può assumere uno dei due
valori: Auto-acknowledge e Dupsok-acknowledge.
Nel primo caso la notifica è automatica ed effettuata
dal container subito dopo che il messaggio è
stato processato, mentre con Dups-ok-acknowledge il
container può inoltrare la notifica in un secondo
momento, quando si ritiene più opportuno farlo
(ovvero ad esempio quando non vi sono altre operazioni
importanti da effettuare con risposta immediata).
Questa seconda possibilità in genere viene scelta
per ridurre il carico di lavoro del container ed anche
per ridurre al minimo il traffico di rete: dato però
che il server EJB ed il provider JMS comunicano in modo
molto stretto e frequente, tale variazione non influisce
più di tanto sulle prestazioni complessive. Inoltre
siccome il ritardo con cui la notifica viene inviate
non è prevedibile, potrebbe accadere che il provider
JMS ipotizzi la perdita del messaggio, provvedendo a
rinviarlo nuovamente.
In questo caso si dovrà provvedere a livello
applicativo all'interno del metodo onMessage() ad evitare
di processare nuovamente il messaggio.
Per questi motivi quindi in genere questa modalità
di notifica non viene utilizzata spesso.
Specificare
le destinazioni: il tag <message-drive-destination>
Per la definizione della coda o topic su cui il bean
resterà in ascolto si utilizza il tag <message-drive-destination>,
per il quale sono previsti i valori javax.jms.Queue
e javax.jms.Topic.
Nel caso in cui si specifichi un topic come sorgente
dei messaggi, si deve includere anche il tag <subscription-durability>
che può assumere i valori Durable e NonDurable.
Il concetto di durable e non durable in questo caso
rispecchia fedelmente quanto valido per un normale sistema
JMS non EJB: nel primo caso la sottoscrizione a quel
topic è di tipo duraturo, ovvero nel caso di
una disconnessione temporanea del bean dal topic, alla
riconnessione verranno recapitati anche quei messaggi
prodotti durante il tempo in cui il bean non era in
ascolto. E' questo un meccanismo che permette ad un
client JMS, e quindi anche ad un MDB, di ricevere tutti
i messaggi prodotti, senza nessuna perdita.
I motivi per cui un MDB potrebbe disconnettersi dal
topic potrebbero essere molteplici, sia per esplicita
richiesta del bean, che per un problema accorso al server
EJB.
Nel caso in cui la sottoscrizione sia NonDurable, tutti
i messaggi inviati durante il periodo di disconnessione
verranno persi; in questo caso si ha un minor livello
di sicurezza, anche se le performance sono maggiori.
Nel caso in cui si utilizzi una coda, per il modello
di messaggistica di tipo p2p, il concetto di durabilità
di un messaggio non ha importanza.
Infine
nel file XML di deploy sono presenti altri elementi
di tipo <ejb-ref> e ejb-local-ref> il cui significato
dovrebbe essere ormai noto: essi permettono al bean
di accedere alle proprietà del JNDI ENC, in modo
molto simile a quanto accade per i sessione e per gli
entity.
Ciclo
di vita di un Message Driven Bean
Il ciclo di vita di un MDB è sicuramente molto
più semplice rispetto a quello degli entity o
dei session. Infatti, dato che un MDB non è associato
a nessun client ma lavora completamente in modalità
passiva su invocazione da parte del container, perdono
di significato i concetti di attivazione o di passivazione.
I due stati possibili quindi sono solamente Does Not
Exist e Method Ready Pool. Il significato di entrambi
è piuttosto intuitivo: nel primo caso il bean
non esiste in memoria, mentre nel secondo esso è
stato creato ed è pronto per essere eseguito
all'interno di un pool di oggetti, la cui dimensione,
determinata dal container, varia in funzione del carico
di lavoro.
Durante il passaggio dallo stato Does Not Exist a quello
di Method Ready Pool vengono effettuate fondamentalmente
tre operazioni: per prima cosa viene creata una nuova
istanza tramite il metodo Class.newIstance(); successivamente
viene invocato dal container il metodo setMessageDrivenContext(),
con il quale il bean riceve il contesto di esecuzione:
memorizzato in un campo del bean.
Infine viene invocato il metodo ejbCreate() del quale
esiste una sola versione senza parametri.
Dato che non esistono attivazione e passivazione, un
MDB potrebbe in teoria mantenere aperte le connessioni
per tutto il periodo di vita, anche se per quanto detto
in precedenza un bean di questo tipo non dovrebbe dover
aprire connessioni verso sottosistemi particolari.
Nel momento in cui un bean è nel pool esso è
in grado di processare tutti i messaggi inviati presso
un topic o coda. Il processamento simultaneo avviene
grazie alla presenza di più istanze nel pool,
dato che ogni istanza può processare un solo
messaggio per volta.
Durante il passaggio inverso da Method Ready Pool a
Does Not Exist verranno effettuate le operazioni inverse
rispetto al processo opposto. Il processo parte dalla
invocazione del metodo ejbRemove() dove dovranno essere
eseguite tutte le operazioni di chiusura necessarie.
All'interno di ejbRemove() continuano ad essere disponibili
il MessageDrivenContext e l'ambiente JNDI ENC.
Figura
1- ciclo di vita di un MDB
Di seguito è riportato un esempio completo di
un deployment descriptor XML preso da [MBShop-6].
<message-driven>
<display-name>MokalistSubscriberBean</display-name>
<ejb-name>MokalistSubscriberBean</ejb-name>
<ejb-class>MokalistSubscriberBean</ejb-class>
<transaction-type>Bean</transaction-type>
<acknowledge-mode>Auto-acknowledge</acknowledge-mode>
<message-driven-destination>
<destination-type>javax.jms.Topic</destination-type>
<subscription-durability>NonDurable</subscription-durability>
</message-driven-destination>
<resource-ref>
<description />
<res-ref-name>jms/TCF</res-ref-name>
<res-type>javax.jms.RMIConnectionFactory</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</message-driven>
Un
esempio completo
Per dare giustificazione dell'importanza dei MDB e per
comprenderne meglio il funzionamento, è sicuramente
utile considerare un esempio completo. Per mancanza
di spazio in questo articolo e per non creare inutili
ripetizioni si rimanda all'articolo pubblicato su MokaByte
nel numero di ottobre 2002 in cui una applicazione basata
su MDB funziona come collante fra due web application
(comprendenti a loro volta lo strato EJB), Community
e MokaList. La prima serve come applicazione per la
registrazione degli utenti di una community virtuale
(nel caso specifico quella del sito web MokaByte) la
seconda invece per consentire agli iscritti di modificare
il proprio profilo di ricezione dei newsletter di MokaByte.
Quando un utente si inserisce un nuovo profilo utente
oppure modifica o rimuove il suo profilo verranno generati
messaggi JMS che poi verranno gestiti da un MDB della
applicazione MokaList. In questo caso infatti la notifica
di una modifica al database di community può
essere propagata anche non immediatamente ad altri soggetti:
la presenza di un topic apposito in questo caso infatti
consente di aggiungere anche altri ascoltatori in futuro,
quando si rendesse necessario notificare anche ad altri
soggetti/applicazioni le modifiche effettuate in Community.
In tal modo l'applicazione centrale non verrebbe in
alcun modo modificata. Per maggiori approfondimenti
si consiglia la lettura di [MBShop-6].
Bibliografia
[JMS] - "Specifica JMS di Sun"
[MBJMS-2] - "Java Message Service" di Stefano
Rossini. Serie di articoli pubblicata su MokaByte febbraio
2002 e successivi - www.mokabyte.it/2002/02
[MBShop-6] - "MokaShop - il negozio online di MokaByte:
Realizzare applicazioni J2EE multicanale - VI parte:
l'integrazione tramite JMS" di Giovanni Puliti,
pubblicata su MokaByte ottobre 2002 - www.mokabyte.it/2002/10
[EJB3] - "Enterprise Java Beans" di R. Monson
Haefel, Ed. O'Reilly-Hops libri
|