MokaByte 68 - 9mbre 2002 

MokaShop il negozio online di MokaByte
Progettare applicazioni J2EE multicanale

VI parte - L'integrazione modulare tramite con JMS. Dalla gestione manuale dei messaggi agli EJB Message Driven Beans
di
Giovanni Puliti
Nei mesi scorsi abbiamo visto come costruire una web application utilizzando il famoso pattern MVC; il cammino intrapreso, è da ricordare, è rivolto alla realizzazione di applicazioni multicanale, in cui la parte web non è altro che uno dei tre canali che prenderemo in considerazione. E' giunto quindi il momento di concludere la parte dedicata alla parte web per passare agli altri canali previsti (quello delle applicazioni stand alone basate su Swing ed infine il canale mobile).
Prima di concludere questa prima sezione è forse utile un ultimo accenno ad una API di Java molto utile fra le altre cose per l'integrazione dei vari moduli.

Come si è infatti più volte sottolineato, l'architettura che abbiamo presentato, oltre ad essere stratificata e multicanale è anche composta da una serie di moduli intercambiabili e connettibili fra loro in vario modo. In particolare lo strato web è stato pensato per permettere l'integrazione fra le varie web application, ma al contempo anche la separazione dei vari pezzi.
Ad esempio in [jump] è stata presentata una tecnica che mostra come si possa consentire il salto fra due web application: in quel caso la tecnica dei salti permetteva di dar vita ad un'unica applicazione web, composta però da diverse web applications: dopo aver effettuato il login nella application A si può passare in modo trasparente nella application B senza dover nuovamente effettuare il login. Le due applicazioni sono effettivamente due cose differenti, ma possono essere eseguite e navigate come se si trattasse della stessa cosa.
Nell'ambito delle applicazioni J2EE il problema dell'integrazione dei moduli è in realtà molto sentito e molte sono le situazioni in cui applicazioni o componenti differenti debbano essere integrati in un solo blocco.
Si riconsideri per un momento il caso della applicazione di community: all'interno di un comune portale dinamico probabilmente essa rappresenta il punto centrale, dato che le varie applicazioni dovranno poter utilizzare i dati dell'utente registrato per poterne effettuare l'autenticazione e l'autorizzazione alle varie operazioni possibili.
E' quindi piuttosto probabile, data la sua importanza, che le operazioni effettuate dall'utente sulla web application di community, abbiano risvolti importanti anche su altri componenti. Ad esempio si consideri una altra applicazione, MokaList: questa applicazione molto semplice permette alla redazione di MokaByte di inviare messaggi di news agli iscritti, ma al contempo permette anche agli iscritti di modificare il proprio profilo utente, scegliendo se e quali messaggi ricevere.
Il punto di unione delle due applicazioni è dato dalla coppia di valori uid+email di ogni utente.
La soluzione ideale è sicuramente quella di tenere le informazioni dell'utente nel db di community e quelle dei newsletter nel db di MokaList: una banale operazione di join fra le due tabelle o i due database, permetterà di ricavare l'elenco degli iscritti alla community interessati ad un determinato tipo di messaggi.
E comunque vero che un legame fra i due db esiste e non è facilmente eliminabile: si pensi ad esempio al caso in cui un utente si registri (aggiunta in Community e MokaList) e poi decida di rimuovere il suo profilo. Discorso analogo nel caso di modifiche su Community.
Ad ora abbiamo sempre considerato il caso migliore: il nostro gruppo di sviluppatori può modificare in ogni singolo pezzo sia l'applicazione Community che MokaList, così come ogni altra applicazione del portale. Si consideri solo per un momento un caso peggiore in cui non sia possibile modificare il comportamento dell'applicazione MokaList, e che quindi essa disponga di un suo database indipendente non integrabile con quello centrale utilizzato per le altre applicazioni del portale.
Si rende quindi necessario implementare un qualche meccanismo di interazione asincrona fra le due applicazioni per sincronizzare in qualche modo i due database.
Il database di Community continuerà ad essere il db principale del sito, ma adesso le modifiche su tale db devono essere propagate verso altre destinazioni, secondo una modalità master slave.
Per cercare di porsi nella situazione più completa e flessibile si può ipotizzare che il sistema slave possa essere anche qualcosa di molto diverso da un semplice database relazionale: si potrebbe pensare di permettere l'integrazione con database ad oggetti, code di messaggi, file, indirizzi di posta, web services.
La nuova architettura che si deve realizzare dovrà quindi permettere di riflettere su tutti i vari slave le modifiche effettuate sul sistema master (Communiy).
Per motivi di efficienza e di architettura, la propagazione delle informazioni in questo caso potrà essere di tipo asincrono: l'applicazione master non dovrà rimanere in attesa che tutti gli slave abbiano ricevuto e processato le modifiche.
Questo è uno scenario in cui l'utilizzo di messaggi asincroni può risolvere in modo egregio il problema. Questo è quindi lo scenario in cui JMS può realizzare l'obiettivo preposto.

 

Java Message Service
JMS è una API Java molto importante e permette la gestione di messaggi asincroni (code e topic) in modo molto semplice ed efficace. Similmente a JDBC o JNDI, JMS non si preoccupa di implementare il sistema di gestione dei messaggi, ma solo di offrire una interfaccia applicativa verso i più popolari e potenti sistemi di messaging attualmente disponibili come Fiorano, Websphere MQ (ex IBM MQSeries), Weblogic o SonicMQ.
Negli esempi che si vedranno in questo articolo si farà riferimento al sistema incapsulato in JBoss, il JBossMQ.
Ovviamente questo articolo non vuole affrontare in modo esaustivo la teoria e le API JMS, per i quali si rimanda alla bibliografia (in particolare [JMS], [MB-JMS] e [JBoss]), ma piuttosto mostrare come JMS possa risolvere brillantemente un problema come quello appena descritto.
In Java i messaggi asincroni possono essere gestiti in modo per così dire manuale tramite la API JMS, oppure in ottica enterprise all'interno della specifica EJB tramite i Message Driver Beans (MDB).
Nella architettura che si presenta si vedranno entrambe le modalità di interazione da parte di una applicazione Java con una coda di messaggi: tramite una apposita action all'interno della applicazione Community si genererà un messaggio ogni volta che verrà effettuata una modifica (add, modify, remove) ad un profilo utente, mentre nella applicazione MokaList, si utilizzerà un MDB per ricevere il messaggio ed eseguire le opportune modifiche al database della applicazione stessa.

 

Il lato Master dei messaggi
L'utilizzo delle actions all'interno di una web application, ed in particolare del meccanismo sorgente-ascoltare per le FireAction-ListenerAction (vedi [MokaShop5]) consente di aggiungere le nuove funzionalità di messaggistica lato master senza stravolgere l'applicazione già realizzata e presentata nelle puntate precedenti.
Si prenda ad esempio il caso della operazione di aggiunta di un nuovo profilo utente alla Community. Questa procedura prevede una serie di passaggi che brevemente possono essere riassunti in:

1. Immissione dei dati
2. Controllo per la correttezza degli stessi
3. Conferma dell'utente e salvataggio finale.

Parallelamente alla operazione di salvataggio finale, si dovrà inviare un messaggio JMS verso un topic appositamente predisposto: il topic è da preferirsi alla coda dato che in questo caso ogni messaggio dovrà essere consumato da più soggetti, e non da uno solo. La coda infatti tipicamente predispone una comunicazione uno-a-uno, mentre il topic uno-a-molti.
Se si riconsidera quanto visto nelle puntate precedenti, per "attaccare" questa nuova azione alla applicazione principale sarà sufficiente definire una nuova azione listener che dovrà al suo interno inviare un messaggio JMS. Data la logica di broadcast di questo messaging, si è scelto di utilizzare un topic piuttosto che una coda (che invece è più adatta nel caso di scambio di messaggi uno-a-uno).


Figura 1 - Aggiungendo una azione listener all'interno della web application
è possibile con poco lavoro implementare un meccanismo di broadcast
JMS verso un numero arbitrario di listener per un determinato topic.


La attivazione delle operazioni di broadcast dei messaggi avverrà, in concomitanza di particolari eventi, sullo strato web tramite l'esecuzione della azione BroadcastAddNewListenerAction. In accordo con quanto visto fino a questo momento, la parte web della applicazione non include business logic, per cui il broadcast del messaggio non verrà effettuato direttamente dalla action ma da un metodo remoto di un session bean (CommunityManager) in esecuzione all'interno di un EJB container. La parte web e la parte EJB anche in questo caso lavorano in simbiosi per realizzare l'obiettivo comune.
Anche se la parte EJB è sicuramente più interessante per quanto riguarda JMS, è utile fare una veloce analisi del codice della action, riportato qui di seguito:

public class BroadcastAddNewListenerAction extends ActionImpl {
public String perform(HttpServlet httpServlet, HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws Exception {
String ret = this.getFailedCode();
Properties applicationProperties;
applicationProperties = (Properties)httpServletRequest.getSession().
                        getAttribute("applicationProperties");

ServletContext sc = httpServlet.getServletContext();
// ricava il contesto JNDI leggendo le proprietà di
// connessione da un apposito file
String jndiFilename = (String)applicationProperties.get("jndiFilename");
// si utilizza il factory per la creazione del contesto
Context ctx = ContextFactory.getContext(sc.getResourceAsStream(jndiFilename));

// lookup dell'oggetto remoto EJB di gestione della community
Object ref = ctx.lookup("community.CommunityManager");
CommunityManagerHome communityManagerHome = (CommunityManagerHome)
PortableRemoteObject.narrow(ref,CommunityManagerHome.class);
CommunityManagerRemote CommunityManager = communityManagerHome.create();

// ricavo dalla sessione l'oggetto CommunityApplicationUser
// che contiene tutte le informazioni sull'utente per la
// registrazione in atto
CommunityApplicationUser communityApplicationUser
communityApplicationUser = (CommunityApplicationUser)httpServletRequest.
                           getSession().getAttribute(
                                        "communityApplicationUser");
String userId = communityApplicationUser.getUserId();
String userPassword = communityApplicationUser.getUserPassword();
// si invoca il metodo remoto del session bean per propagare
// il messaggio in broadcast
CommunityManager.broadcastRegistrationAddNew(userId, userPassword);

logger.info("fine metodo perform");
return ret;
}

Il metodo perform() della action verrà eseguito quando la action associata al salvataggio del profilo utente (AddNewSaveFireAction), propagherà il flusso delle operazioni a tutti i listener, fra cui appunto anche la BroadcastAddNewListenerAction.
Si noti come tale metodo non effettua nessuna assunzione su chi e come potrà avere bisogno delle informazioni dell'utente, passando come parametri al metodo remoto del session bean tutto quanto potrebbe essere utile (uid utente e Properties).
Creare tale legame sorgente-ascoltare, grazie alle tecniche di MVC basate su file di configurazione XML, è una cosa molto semplice e non richiede la modifica nemmeno di una riga di codice Java di Community. Per prima cosa è necessario aggiungere la definizione della classe BroadcastAddNewListenerAction e del codice ad essa associato

<action NAME="broadcast-addnew" CLASS="BroadcastAddNewListenerAction"/>

successivamente si può impostare in ascolto tale classe alla AddNewSaveFireAction tramite

<fireAction NAME="addnew-save" CLASS="com.mokabyte.community.mvc.actions.AddNewSaveFireAction">
<executed>addnew-save-ok</executed>
<failed>addnew-save-ko</failed>
<listener>broadcast-addnew</listener>
</fireAction>
<routing CODE="addnew-save-ok" PAGE="addnew-end.show" FORWARD="false"/>
<routing CODE="addnew-save-ko" PAGE="addnew-errormsg.show" FORWARD="false"/>

E' la porzione di XML <listener>broadcast-addnew</listener> crea il legame sorgente ascoltare.
La parte più interessante è sicuramente il metodo EJB corrispondente a tale azione, ovvero il metodo broadcastRegistrationAddNew(). Quest'ultimo, dopo aver ricavato i dati passati alla invocazione dallo strato web, effettua le operazioni di preparazione del messaggio e di invio vero e proprio.

public void broadcastRegistrationAddNew(String userId, String userPassword){
try {
Properties userProperties = this.loadUserProperties(userId, userPassword);

// Parte 1: ricava il connection factory tramite il factory JNDI
Context context = ContextFactory.getContext();
TopicConnectionFactory topicFactory = (TopicConnectionFactory)context.lookup("ConnectionFactory");
TopicConnection topicConnection = topicFactory.createTopicConnection();
TopicSession topicSession = topicConnection.createTopicSession(false,Session.AUTO_ACKNOWLEDGE);
// il topic di interesse dove inviare il messaggio è "topic/community"
Topic topic = (Topic)context.lookup("topic/community");

// Parte 2: crea il publisher
TopicPublisher topicPublisher = topicSession.createPublisher(topic);

// Parte 3: crea il messaggio
ObjectMessage AddNewMessage = topicSession.createObjectMessage(userProperties);

// Parte 4: imposta alcune proprietà per effettuare eventuali filtri in fase di ricezione del messaggio
AddNewMessage.setObjectProperty("area","community");
AddNewMessage.setObjectProperty("operationType","addnew");
AddNewMessage.setObjectProperty("userId",userId);

// Parte 5: pubblica il messaggio sul topic
topicPublisher.publish(topic, AddNewMessage);
}
catch (Exception ex) {
throw new EJBException("Exception in broadcastRegistrationAddNew \n"+ex);
}
}

Il codice mostrato è piuttosto semplice e per comodità lo si è suddiviso in sezioni. Nella prima viene creato il contesto JNDI da cui tutte le operazioni successive prenderanno spunto. Il factory utilizzato viene spiegato successivamente. Si noti invece la creazione del topic vero e proprio con il nome su cui i messaggi verranno pubblicati. In questo caso il nome scelto è "community"; se si utilizza come naming service JBoss allora la lookup nel client dovrà essere eseguita con il nome preceduto da "topic": il nome completo sarà quindi "topic/community".
Le parti 2 e 3 contengono le operazioni relative alla creazione e pubblicazione di un messaggio. Anche in questo caso per ulteriori approfondimenti è bene far riferimento alla bibliografia.
Nella sezione 4 si impostano alcune proprietà che andranno a finire nell'header del messaggio. In questo caso si memorizza il nome del tipo di operazione effettuata nella community ("add", "remove", "modify"), e l'user id dell'utente (utile eventualmente per velocizzare le operazioni lato client-slave).
Queste proprietà, oltre che essere ricavate in fase di processamento del messaggio da parte della applicazione client potranno essere utilizzate per creare dei veri e propri filtri in esecuzione all'interno del service system. In questo modo si potrebbe ottimizzare le operazioni di broadcast dei messaggi indirizzando ad esempio ad una determinata destinazione solo i messaggi relativi alle operazioni di aggiunta o modifica di un utente alla community (Si veda [MB-JMS-3] per l'utilizzo dei filtri sui messaggi JMS).
Infine nella sezione 5 si invia il messaggio, o più precisamente trattandosi di un topic se ne effettua la pubblicazione.
In entrambi i listati si è fatto riferimento ad un factory per poter ricavare il contesto JNDI. Questo oggetto per semplicità potrebbe avere le proprietà di connessione cablate direttamente nel codice o ricavarle da un file di testo: anche se per ovvi motivi la seconda soluzione sia quella da preferire, di seguito è riportato il codice con proprietà cablate nel si utilizzi JBoss come container EJB e come naming service

public static Context getContext() throws NamingException{
  System.out.println("Enter getContext()");
  try {
    if (context == null){
      Properties ContextProperties = new Properties();
      ContextProperties.put("java.naming.factory.initial",
                            "org.jnp.interfaces.NamingContextFactory");
      ContextProperties.put("java.naming.factory.url.pkgs",
                            "org.jboss.naming:org.jnp.interfaces");
      context = new InitialContext(ContextProperties);
    }
  }
  catch (NamingException ne) {
    System.out.println("NamingException in getContext() \n"+ne);
    throw new NamingException("NamingException in ContextFactory.getContext() \n"+ne);
  }
  System.out.println("getContext() - return context: "+context);
  return context;
}

L'attivazione del topic all'interno del message service segue a seconda del prodotto scelto particolari procedure che variano da prodotto a prodotto. Nel caso si JBoss è sufficiente modificare il file jboss.jcml in modo da aggiungere una riga con il nome del topic da attivare. Ad esempio nel caso in questione si potrebbe scrivere

<mbean code="org.jboss.mq.server.TopicManager"
       name="JBossMQ:service=Topic, name=community"/>

 

Implementazione del lato slave: il MDB di MokaList
Questo schema architetturali prevede l'utilizzo di un MDB con la funzione di listener JMS, il quale poi inoltrerà le chiamate ai metodi di session bean a seconda della situazione.
Il session bean svolge il compito di oggetto remoto di servizio: in questo caso verrà invocato da un MDB ma potrebbe essere utilizzato da un altro client, ad esempio da una action di una controparte web della applicazione MokaList.
Senza entrare nel dettaglio della teoria di MDB si può brevemente dire che un Message Driver Bean è un particolare tipo di EJB in cui continuano a valere concetti come transazionalità e di sicurezza tipici di EJB, ma in cui non esiste né il concetto di interfaccia remota né di invocazione da parte di un client. Un MDB mette a disposizione un unico meccanismo di interazione con il mondo esterno, ovvero tramite l'invocazione in call back da parte del container sul metodo onMessage() tutte le volte che un messaggio JMS arriva al container.
Al solito il comportamento del bean è generico e la sua customizzazione (su quale coda o topic restare in ascolto) è una operazione da effettuare tramite apposito file XML in sede di deploy.
Un MDB deve implementare le interfacce MessageDrivenBean e MessageListener

public class MokalistSubscriberBean implements MessageDrivenBean, MessageListener {

Il metodo ejbCreate() è il luogo ideale dove svolgere le operazioni di inizializzazione di JMS: ad esempio

public void ejbCreate() throws CreateException {
  try {
    Context ctx = ContextFactory.getContext();
    String res="java:comp/env/jms/TCF";
    Object o = ctx.lookup(res);
    TopicConnectionFactory tcf = (TopicConnectionFactory)o;
    TConnection = tcf.createTopicConnection();
    TSession = TConnection.createTopicSession(false,TopicSession.AUTO_ACKNOWLEDGE);
    TConnection.start();
  }
  catch (Exception ex) {
    // …exception handling
  }
}

La stringa res individua una risorsa definita all'interno dell'ejb-jar.xml ed anche nel file proprietario di deploy (nel caso di JBoss jboss.xml). Ad esempioin ejb-jar.xml oltre al resto dei tag di deploy si potrebbe pensare di scrivere

<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>

mentre in jboss.xml

<jboss>
<enterprise-beans>
<message-driven>
<ejb-name>MokalistSubscriberBean</ejb-name>
<destination-jndi-name>topic/community</destination-jndi-name>
<resource-ref>
<description />
<res-ref-name>jms/TCF</res-ref-name>
<jndi-name>RMIConnectionFactory</jndi-name>
</resource-ref>
</message-driven>
</enterprise-beans>
</jboss>


In questo caso il tag <destination-jndi-name> indica il nome del topic su cui il bean risulta in ascolto, mentre la risorsa jms/TCF indica il tipo di connection factory da utilizzare per ottenere la connessione verso il message service. JBoss offre diverse tipologie di comunicazione basate su socket, su socket overlapped, su comunicazione RMI, e su chiamate native nel caso di applicazioni in esecuzione all'interno dello stesso container e quindi della stessa JVM.
In modo del tutto simmetrico alla creazione all'interno di ejbRemove() si eseguono le operazioni di chiusura della sessione JMS

public void ejbRemove() {
  try {
    if (TConnection != null){
      TConnection.close();
    }
    if (TSession != null){
      TSession.close();
    }
  }
  catch (Exception ex) {
    // … exception handling
  }
}


infine il metodo onMessage() è il luogo dove inserire il codice da eseguire all'arrivo di un messaggio

public void onMessage(Message msg) {
  try {
    ObjectMessage CommunityMessage = (ObjectMessage)msg;
    String operationType = CommunityMessage.
                           getStringProperty("operationType");
    String userId = CommunityMessage.
                    getStringProperty("userId");
    Properties userProperties = (Properties)
                                CommunityMessage.getObject();

    // controlla il tipo di operazione associata al
    // messaggio ricevuto e di conseguenza
    // invoca il metodo relativo del session bean
    // se si tratta di una nuova registrazione
    if (operationType.equals("addnew")){
      this.addUserSubscription(userId, userProperties);
    }
    // se si tratta di una rimozione di un profilo utente
    if (operationType.equals("remove")){
      this.removeUserSubscription(userId, userProperties);
    }
    // se si tratta di una modifica di un profilo utente già presente
    if (operationType.equals("modify")){
      this.modifyUserSubscription(userId, userProperties);
    }
  }
  catch (Exception ex) {
    // … exception handling
  }
}

In questo caso il metodo onMessage() controlla il campo "operation" inserito nell'header del messaggio ed in funzione del valore ritornato invoca l'opportuno metodo del session bean.
Ad esempio nel caso di una aggiunta di una nuova registrazione il metodo onMessage() invocherà il metodo addUserSubscription() del session bean MokaListManager.

private void addUserSubscription(String UserId,
                                 Properties UserProperties){
  try {
    Context ctx = ContextFactory.getContext();
    Object ref = ctx.lookup("mokalist.MokalistManager");
    MokalistManagerHome mokalistManagerHome = (MokalistManagerHome)     PortableRemoteObject.narrow(ref, MokalistManagerHome.class);
    MokalistManagerRemote mokalistManagerRemote =     mokalistManagerHome.create();
    // invoca il metodo del session
    mokalistManagerRemote.addUserSubscription(UserId, UserProperties);
  }
  catch (Exception ex) {
    // … exception handling
  }
}

Se i due bean (MDB e session) sono in esecuzione all'interno dello stesso container EJB, per chi fosse disposto ad utilizzare le interfacce locali di EJB 2.0 (i puristi non sono per niente convinti del fatto che sia una buona cosa il loro uso in fase di progettazione dato che dovrebbe essere il server a decidere in modo trasparente al programmatore) l'invocazione fra l'MDB ed il metodo del session potrebbe avvenire in modo locale per aumentare le prestazioni complessive.
In questo esempio, per semplicità, il controllo sul messaggio viene effettuato direttamente all'interno del bean, ovvero sul client ricevente il messaggio. Questo implica che tutti i messaggi indirizzati al topic "community" verranno recapitati al bean (ed agli altri eventuali listeners). In presenza di grossi volumi di traffico di messaggi, al fine di ripartire meglio il carico, si sarebbe potuto creare diversi bean MDB (eventualmente in esecuzione in una architettura cluster), ognuno con un compito differente: con un lavoro minimo di configurazione si potrebbe creare una serie di filtri agenti sui campi degli header del messaggio in modo da indirizzare i messaggi solo ai bean interessati.

 

Conclusione
Quanto visto questo mese, pur non dando nessuna spiegazione approfondita né di JMS né dei MDB, offre una giustificazione su come queste tecnologie possano essere utilizzate in maniera utile e proficua al fine di realizzare applicazioni J2EE in modo modulare e componibile.
Perfettamente in linea con la filosofia J2EE, tutto il codice visto è poco o per niente dipendente dal particolare sistema di messaging o all'EJB container scelti. Per questo motivo una volta di più rimandiamo alla documentazione del prodotto o dei prodotti scelti per capire come creare code o topic e per comprendere come effettuare il deploy di un MDB.

 

Bibliografia
[MShop] - "MokaShop il negozio online di MokaByte Progettare applicazioni J2EE multicanale", di Giovanni Puliti
[JUMP] - II puntata della serie MokaShop www.mokabyte.it/2002/ 03/devj2ee_2.htm
[JMS] - http://java.sun.com/products/jms/
[MB-JMS] - "JMS La gestione dei messaggi in Java" di Stefano Rossini, Febbraio 2002 e successivi
[JBossWeb] - Documentazione JBoss - www.jboss.org/docs
[JBoss] - "JBoss administration and development", di Scott Stark e Marc Fleury
ISBN: 0-672-32347-8
[MokaShop5] - V puntata della serie MokaShop www.mokabyte.it/2002/ 06/devj2ee_5.htm

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