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
|