Prosegue la trattazione relativa alla progettazione dell‘architettura tipica di un‘applicazione Java EE basata su EJB. Questo mese parliamo di come mettere in comunicazione lo strato client con il session façade il cui deploy è avvenuto all‘interno dell‘application server EJB. Il Business Delegate è il pattern che risolve elegantemente questo problema grazie anche all‘utilizzo del Service Locator presentato nella puntata precedente.
Introduzione
Nella puntata precedente ([EJBAPPS-1]) è stata fatta una introduzione per una tipica architettura applicativa basata su EJB dando una prima visione di insieme dello strato remoto basto su session beans.
Si è visto come, i vari componenti deployati nel container EJB potessero interagire fra loro in modo da realizzare lo strato di business logic (tramite i session beans) ed il data model (entity beans).
In particolare si è potuto vedere come l‘adozione del pattern session faà§ade tramite uno o più session beans di facciata potesse semplificare enormemente la gestione sia della business logic remota che l‘accesso in modo corretto ai bean dati.
Un client per svolgere le operazioni di connessione ed invocazione della logica remota in escuzione all‘interno di un container EJB, dovrebbe effettuare una serie di operazioni ripetitive spesso non proprio semplici o intuitive: la lookup dell‘oggetto remoto, il marshalling dei parametri remoti, la gestione delle eccezioni con relativo wrapping.
Dal punto di vista del client questo genere di problematiche e quindi le relative tecnologie coinvolte introduce un livello di complessità e una maggiorazione dei tempi e dei costi di sviluppo non giustificabili nell‘ottica di industrializzare il processo di sviluppo.
Se lo strato client è una applicazione Swing, Struts o peggio ancora una midlet in funzione in un palmare o smart-phone, il team di sviluppo non vorrà certo (o non potrebbe avere i mezzi o le conoscenze per) gestire tutti gli aspetti relativi a gestione delle transazioni, passaggio di parametri remoti, lookup, JNDI etc…
Nel corso degli anni sono stati quindi individuate alcune soluzioni tipo unitamente ad alcuni pattern per isolare gli strati applicativi l‘uno rispetto all‘altro e semplificare l‘assemblamento della architettura nel suo complesso.
In questo caso viene preso in considerazione il pattern Business Delegate (parente stretto di pattern analoghi come il Command o il Proxy) il quale permette di replicare i concetti di base della parte server side: isolamento, limitazione dello scope applicativo al layer in esame, maggior pulizia complessiva.
Limitare il legame fra gli strati
Già nella puntata precedente si era cercato di fornire un approccio di questo genere: se ad esempio si desidera esporre un servizio tramite un session (faà§ade oppure no), si è potuto vedere come l‘utilizzo di componenti stateful (che di fatto sono session beans, ma concettualmente il discorso non cambia se si volesse realizzare e utilizzare un qualsiasi componente server side che mantiene il cosiddetto conversational state con il client) permettano allo strato server di offrire un supporto molto più potente; mantenere la sessione però lega tali oggetti remoti all‘utilizzo di un client che deve essere informato di tale sessione.
Più semplicemente si potrebbe dire che un client che voglia utilizzare un session bean stateful dovrebbe essere al corrente che tale componente mantiene i dati di sessione, al fine di semplificare la gestione dei dati e non incorrere in pericolosi disallineamenti dello stato.
Viceversa realizzare una piattaforma server side che offra servizi transazionali ma di tipo stateless, permette un maggior riutilizzo e riadattabilità a scenari differenti.
Lo stesso approccio deve essere quindi portato anche sullo strato client ed in particolare nel livello di intercomunicazione client-server: i pattern server side (principalmente Service Locator e Session Faà§ade) vengono per questo motivo affiancati da altrettanti pattern sul layer client e per la gestione della comunicazione dei dati fra i vari livelli. In figura 1 è riportata questa schematizzazione
Dalla parte del cliente: il business delegate
Il pattern che viene qui presentato, il Business Delegate, ha come scopo principale quello di mascherare la complessità legata alla intercomunicazione client-server RMI/EJB.
Per rendere più semplice la comprensione del funzionamento di questo pattern verrà al solito presa in esame una applicazione di esempio, MokaByte CMS, della quale in questo momento non è necessrio ai fini dell‘articolo comprenderne il funzionamento nel suo complesso.
Fra le varie funzioni che questa applicazione è in grado di svolgere troviamo quella relativa alla creazione e modifica degli articoli che poi verranno pubblicati via web (o in altri formati come il PDF) secondo una tipica logica di CMS.
Nello strato EJB di MokaByte CMS si trova quindi un session faà§ade ArticleManagerSFRemote un session bean che svolge il compito di session faà§ade i cui metodi vengono invocati dallo strato client (una applicazione web basata sul pattern MVC) per effettuare le opportune operazioni relative alla gestione di articoli del sito.
Tale session espone quindi alcuni metodi remoti tramite la interfaccia remota ArticleManagerSFRemote della quale qui di seguito è riportata una breve porzione
public interface ArticleManagerSFRemote extends EJBObject {public void createArticle(ArticleDTO articleDTO) throws EJBException, RemoteException;
il metodo createArticle() viene invocato dallo strato web per la creazione di un articolo. Il metodo riceve in input un oggetto DTO del quale si avrà modo di parlare in seguito.
Come da specifica l‘interfaccia remota rappresenta l‘insieme delle funzionalità che un session bean permette di eseguire da remoto.
Il business delegate rappresenta quindi un wrapper che espone le stesse funzionalità dell‘oggetto remoto e tramite il quale il client può eseguire le stesse operazioni ma in modalità locale.
Il business delegate svolge quindi due importanti operazioni: si connette all‘oggetto remoto tramite una operazione di lookup sul nome JNDI con cui il session viene registrato, secondariamente espone una serie di metodi equivalenti a quelli esposti dalla interfaccia remota del session.
Vediamo prima la parte relativa alla connessione.
Il costruttore del BD riceve un oggetto properties contenente i parametri di connessione JNDI. Il costruttore, nella implementazione qui in oggetto invoca il metodo interno al BD initializeArticleSFHome() il quale si preoccupa di effettuare l‘operazione di lookup sul nome remoto e di memorizzare la home interface del session.
public class ArticleManagerBD implements Serializable {private ArticleManagerSFHome articleManagerSFHome;private ArticleManagerSFRemote articleManagerSFRemote;public ArticleManagerBD(Properties jndiProperties) throws Exception {initializeArticleSFHome(jndiProperties);articleManagerSFRemote = articleManagerSFHome.create();}...}private void initializeArticleSFHome(Properties jndiProperties) throws Exception {String FACADE_NAME = "ArticleManagerSF";Class FACADE_CLASS = com.mokabyte.cms.ejb.ArticleManagerSFHome.class;if (articleManagerSFHome == null) {try {ServiceLocator locator = ServiceLocator.getInstance(jndiProperties);articleManagerSFHome = (ArticleManagerSFHome) locator.getEjbHome(FACADE_NAME, FACADE_CLASS);if (articleManagerSFHome == null) {throw new Exception("Did not get home for " + FACADE_NAME);}} catch (ServiceLocatorException e) {throw new Exception(e.getMessage());}}}
Come si potrà notare il BD contiene due variabili di classe che rappresentano i riferimenti alla home e remote del session.
In questo particolare momento (ovvero subito dopo l‘inizializzazione della home), per come funziona la comunicazione client server, il BD non ha ancora instaurato nessun legame definitivo con il container; la home rappresenta infatti il factory tramite il quale ricavare l‘oggetto remoto e non il puntatore all‘oggetto remoto.
Nota: la scelta di implementare un BD serializzabile è legata alla possibilità di utilizzare tale oggetto in una sessione HTTP. Come noto gli oggetti serializzabili sono gestiti meglio dal container web che ad esempio è in grado di farli migrare in caso di una sessione distribuita tipoca di una architettura cluster.
Appare evidente che il passaggio importante è quello relativo alla creazione della remote a partire dalla home.
Per svolgere questa operazione si possono seguire due strade: la prima ha come obiettivo primario la massimizzazione delle prestazioni, riducendo fra le altre cose le operazioni inutili; in questo caso si effettua l‘istanziazione della remote direttamente nel metodo initializeArticleSFHome() .
E‘ in questo particolare momento che il BD viene associato ad un particolare stub associato alla interfaccia remota. Anche se questo risulta particolarmente vero solo in caso di un session stateful, è comunque vero che il reference alla remote rimarrebbe memorizzato nella classe una nuova creazione del BD potrà forzare la riconessione al server remoto.
Questo legame stretto non è da considerarsi un problema per una possibile caduta della connessione fra client e server, ma piuttosto potrebbe essere fonte di malfunzionamenti o cattiva gestione in uno scenario clustered. Infatti gli application server EJB in configurazione cluster spesso realizzano meccanismi di cache distribuita e di pooling geografico degli EjbObject associati ad un interfaccia remota. In questo caso l‘utilizzo di un reference fisso della remote interface di fatto blocca meccanismi come il round robin della prima reference disponibile e simili. Nel caso in cui un oggetto non sia più disponibile l‘application server non potrà effettuare le debite operazioni di ricerca del sostituto nel cluster e il client dovrà ripartire da capo nel suo vorkflow applicativo; ad esempio ripetere il login e riprendere dalla pagina iniziale una eventuale operazione di acquisto ondine (ovviamente il client deve essere opportunamente scritto per gestire queste casistiche e in particolare essere in grado di operare con le eccezioni che arrivano dallo strato server).
Non è il caso di approfondire ulteriormente tali aspetti in questa sede (si veda la bibliografia) ma per onor di sintesi si potrebbe semplicemente dire che la prima soluzione (dove il BD memorizza sia la home che la remote durante la fase di inizializzazione ovvero nel costruttore dell‘oggetto) è da scegliere nel caso in cui si l‘obbietivo principale siano le performance e non si desideri preoccuparsi troppo di cosa possa accadere in caso di errore in uno scenario cluster (ma nella peggiore delle ipotesi l‘effetto è quello che il client perde lo stato della sessione e deve ricominciare dall‘inizio la procedura).
La seconda strada (che verrà mostrata negli esempi che seguono) è certamente più conservativa e probabilmente leggermente meno performante. Personalmente reputo che il degrado delle prestazioni sia in questo caso ininfluente, tanto da far preferire per una soluzione certamente più stabile al variare dello scenario di deploy.
Dopo la procedura di inizializzazione all‘interno del BD ha a disposizione il reference alla home interface (ed eventualmente anche quello alla remote).
A questo punto per ogni metodo presente nella remote interface del session bean, se ne dovrà scrivere uno nella classe BD.
Ad esempio si potrebbe pensare di creare nel BD un metodo wrapper del corrispondente sul session:
public void removeArticle(String id) throws RemoteException {articleManagerSFRemote.removeArticle(id);}
per cui un client per creare un articolo remoto, potrà utilizzare il BD ignaro della presenza di un layer remoto
// si carica il file jndi.properties contenente i parametri di connessione// al context EJBProperties jndiProperties = new Properties();jndiProperties.load(new FileInputStream ("jndi.properties"));System.out.println("Caricato il file jndi.properties "+jndiProperties);ArticleManagerBD aticleManagerBD = new ArticleManagerBD(jndiProperties);try{articleManagerBD.createArticle(articleVO.getDTO());}catch(RemoteException re){Gestione della eccezione remota}
Come si può notare, dato che l‘invocazione remota obbliga alla gestione di una RemoteException questa versione del BD soffre di due importanti limitazioni. Quando un client scrive
articleManagerBD.createArticle(articleVO.getDTO());
deve di fatto premunirsi dall‘insorgere di una eventuale RemoteException, e deve gestirla opportunamente. Se questo è in prima analisi un aspetto scomodo ad una analisi più attenta ci si può rendere conto di quanto sia errato progettualmente: infatti una midlet per palmare o una applicazione Swing non hanno interesse a conoscere i dettagli o le motivazioni che hanno causato il problema. Inoltre obbligano lo strato client a importare il package javax.rmi e quindi si devono legare allo strato RMI/EJB.
Questo è un grave errore che va contro la filosofia di disaccoppiamento del BD. Se a esempio volessimo nascondere dietro il delegate un tipo di server differente (esempio un web service) si sarebbe costretti a riscrivere il codice sul client: è questa la negazione della programmazione per pattern e la violazione del concetto di disaccoppiamento.
Con poche modifiche si può pensare di utilizzare una versione leggermente differente dei metodi di wrap effettuando anche il wrapping della eccezione.
public void createArticle(ArticleDTO articleDTO) throws FacadeInvocationException {try {articleManagerSFRemote.createArticle(articleDTO);} catch (RemoteException re) {String msg = "Errore di invocazione remota sul metodo di faà§ade createArticle() " + re;throw new FacadeInvocationException(msg);}}
In questo caso si può notare come la remoteException sia bloccata e rilanciata sotto forma di Faà§adeInvocationException, una eccezione applicativa appositamente creata dal programmatore (non esiste nel JDK) al momento della creazione del BD.
Questa soluzione offre due vantaggi: per prima cosa il wrapping della eccezione permette di modificare a proprio piacimento “cosa” e “come” notificare il cliente di un eventuale problema avvenuto sul server.
Secondariamente offre una completa separazione degli strati. Il nome della Faà§adeInvocationException suggerisce infatti che si è verificato un problema relativo alla invocazione di qualcosa che sta “là dietro”: non è importante per il cliente sapere cosa ci sia là dietro. Per concludere si noti come all‘interno del metodo initializeArticleSFHome sia stata inserita la riga
ServiceLocator locator = ServiceLocator.getInstance(jndiProperties);
L‘oggetto ServiceLocator è stato presentato nella precedente puntata della serie e serve per realizzare la connessione vera e propria verso lo strato remoto.
Il metodo getInstance(), che riceve i parametri di connessione incapsulati all‘interno di un properties, al suo interno da qualche parte effettuerà la connessione al context JNDI del container. Ricordando quanto già visto in precedenza si potrebbe scrivere
public static synchronized ServiceLocator getInstance(Properties jndiProperties)throws ServiceLocatorException {if (serviceLocator == null) {serviceLocator = new ServiceLocator(jndiProperties);}return serviceLocator;}private Context getInitialContext(Properties jndiProperties) throws NamingException {if (jndiProperties == null){Context context = new InitialContext();}Context context = new InitialContext(jndiProperties);return context;}
Nel caso in cui il client sia eseguito all‘interno della stessa JVM del container EJB (es. una web application deployata nello stesso file .ear che contiene anche la parte EJB), il parametro jndiProperties potrebbe essere tranquillamente impostato a null in modo da eseguire la InitialContext() senza parametri (in questo caso l‘application server ridirige le operazioni di lookup verso il context di default).
Per onor di completezza invece si consideri il caso in cui si voglia esplicitamente utilizzare i parametri di connessioni JNDI: in questo caso sarà sufficiente provvedere a caricare un file jndi.properties da qualche parte. La specifica dice che il client può effettuare la connessione se il file è presente nel classpath del client, ma forse la prudenza non è mai troppa. Meglio procedere al caricamento forzato di tale file. Ecco un esempio di jndi.properties preso dagli esempi di JBoss.
### JBossNS client properties for connection from the localhostjava.naming.factory.initial=org.jnp.interfaces.NamingContextFactoryjava.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfacesjava.naming.provider.url=jnp://localhost:1099/# The jnp protocol socket factory classjnp.socketFactory=org.jnp.interfaces.TimedSocketFactory# The TimedSocketFactory connection timeout in milliseconds(0 == blocking)#jnp.timeout=0# The TimedSocketFactory read timeout in milliseconds(0 == blocking)#jnp.sotimeout=0
Conclusione
In questo articolo è stato presentato un altro piccolo componente indispensabile per la buona progettazione di una applicazione a strati. Il prossimo articolo sarà dedicato ad uno degli aspetti tecnologicamente meno sconvolgenti, il pattern DTO, ma che ricopre spesso una importanza trategica nella buona riuscita di un progetto enterprise.
[EJBAPPS-1]
Architetture e tecniche di progettazione EJB – I parte: architettura di una applicazione tipo
https://www.mokabyte.it/2005/07/ejbarchitectures-1.htm