MokaByte 69 - Dicembre 2002 
La specifica EJB 2.0
I Message Driven Beans
di
Giovanni Puliti
Con il rilascio della specifica EJB 2.0 molte sono state le novità introdotte. Una di questa è volta a colmare una importante carenza, la possibilità di gestire componenti EJB in modo asincrono tramite messaggi JMS

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

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it