Architetture e tecniche di progettazione EJB

I parte: Architettura di una applicazione tipodi

Conoscere a fondo la tecnologia EJB è solo il primo passo per poter procedere nella realizzazione di una applicazione enterprise che sia performante ma sopratutti facilmente gestibile, modificabile e manutenibile. Presentiamo in questa serie di articoli alcune delle più famose best practice e pattern design utili per il mondo EJB: dalla scelta della architettura alla individuazione dei componenti principali.

Introduzione

Lo scopo di questa nuova serie di articoli è quella di guidare il lettore nell‘apprendimento delle tecniche di base necessarie per progettare e realizzare una comune architettura EJB basata su session ed entity beans. Prendendo spunto da una applicazione di riferimento verrà  mostrata quale sia l‘approccio migliore per strutturare i vari strati applicativi e come implementare la comunicazione e lo scambio dati fra i vari layer.
L‘obiettivo finale non è tanto quello di introdurre alla teoria di base di EJB, per la quale si può far riferimento con efficacia ai titoli riportati in bibliografia ([EJB] e [MB]), ma piuttosto come utilizzare la tecnologia individuando in modo opportuno i vari componenti e progettando nel migliore dei modi la architettura complessiva.
Lo scenario che si andrà  a raffigurare è quello riportato in figura 1: un generico client (applicazione web, Struts, Swing o altro) tramite l‘invocazione remota dei metodi di un session bean di front end potrà  sfruttare la business logic contenuta nello strato EJB, ed agire in modo indiretto sui dati rappresentati dai vari entity beans.

Data la vastità  degli argomenti e dei punti salienti presenti in una architettura di questo genere è certamente impossibile affrontare in modo dettagliato ogni singolo aspetto della tecnologia e delle possibili soluzioni.
E‘ sempre bene tenere a mente che il taglio offerto è espressamente di tipo pratico, e per questo alcune assunzioni sono frutto dell‘esperienza sul campo e non hanno la pretesa di aderire in modo stretto alle indicazioni della teoria.
Ogni variazione sul tema finalizzata ad adattamenti a casi specifici o inclusioni di tecnologie alternative (esempio Hibernate al posto di CMP) sono incoraggiate al fine di sperimentare altre strade o possibilità .

Alcune ipotesi di partenza

Nel momento in cui si procede nella realizzazione di una applicazione J2EE più o meno complessa è necessario introdurre alcune semplificazioni o vincoli strutturali al fine di rendere realistica l‘implementazione finale.
E‘ ormai statisticamente dimostrato che ogni programmatore o progettista incontri maggiori difficoltà  nella risoluzione di determinati problemi, in genere legati a le medesime problematiche: come stratificare l‘applicazione, come mettere in comunicazione i vari componenti e strati, quali componenti utilizzare, quale framework di persistenza utilizzare, come gestire le eccezioni ed i log applicativi.
Questi quesiti spesso non trovano una sola risposta e tantomeno è possibile appellarsi al "super pattern" o la mirabolante "best practice", ma è necessario realizzare una serie di indagini progettuali, sulle specifiche funzionali e requisiti applicativi per poter trovare la soluzione che si adatta al meglio al caso in questione.
Di seguito sono elencati alcuni dei punti dove tipicamente il progettista J2EE incontra i maggiori dubbi: le ipotesi ed assunzioni fatte sono in parte frutto dell‘esperienza comune, in parte sono dettate dal buon senso, in parte sono legate a scelte di carattere editoriale (per dovere di sintesi e di chiarezza non avrebbe avuto senso affrontare ogni possibile casistica).
Alcune scelte ricalcano la struttura della applicazione presa in esame.

Il framework di persistenza

L‘applicazione prevede per lo strato di persistenza l‘utilizzo del framework CMP, che in questo momento storico è causa di accese discussioni sui vari forum web tanto che si sono formate vere e proprie scuole di pensiero pro e contro l‘utilizzo di entity beans (contrapposti a framework di mapping alternativi come JDO, Hibernate o semplici DAO).
La scelta qui fatta (utilizzare CMP) non è espressione diretta di una preferenza a scapito di altre tecnologie, ma piuttosto segue quella che è l‘impostazione di questa serie di articoli: fornire gli strumenti per comprendere ed imparare a realizzare applicazioni EJB.

Mantenimento dello stato conversazionale fra strati

E‘ spesso impossibile stabilire un elevato numero di assunzioni sulla tipologia del client e sulla sua natura. Per questo motivo è bene cercare di non vincolare troppo la modalità  operativa dello strato EJB.
Questa valutazione generica porta ad una serie di considerazioni pratiche che aiutano nella scelta delle varie soluzioni. Ecco di seguito alcune considerazioni riportate in ordine più o meno sparso.
L‘invocazione remota di un session bean è una operazione inefficiente e quindi la tendenza è quella di limitare il più possibile il suo utilizzo. Questa tematica si ripercuote fra le altre cose anche sulla modalità  utilizzata per il mantenimento della sessione.
L‘utilizzo di session beans stateful è una scelta forte che semplifica molto la parte relativa al mantenimento dello stato ma che può risultare controproducente nel momento in cui lo strato client sia di tipo web: in questo caso infatti si rende necessario tenere traccia dello stato su due layer differenti con ovvi problemi di sincronizzazione o nel migliore dei casi con una replicazione inutile di informazioni (concernenti lo stato appunto).
Molte possono essere le strade da seguire e le varie alternative spaziano da quella che si potrebbe definire EJB blackbox (tutto il lavoro di business cosଠcome il mantenimento della sessione di lavoro è nascosto nello strato EJB che espone alcuni metodi invocati dal thin-client) ad una strategia opposta in cui lo strato EJB non mantiene sessione ed anzi espone metodi che sono utilizzati dal client come servizi necessari per svolgere i comuni compiti di busines logic.
Nel primo caso ogni chiamata remota da parte del client su un metodo remoto corrisponde ad un macro use case di alto livello: il client ignora le possibili implicazioni della invocazione di un ipotetico metodoRemoto(), la sequenza effettiva delle operazioni e conseguenza ad esso associato.
In questo modo si sposa completamente la filosofia del thin client con ovvi vantaggi per quanto riguarda le prestazioni (si riduce al massimo il flusso di invocazioni e dei dati fra client e parte EJB).
E‘ ovvio che l‘approccio black box pur offrendo innegabili vantaggi dal punto di vista delle prestazioni, potrebbe non essere sufficientemente flessibile nel caso in cui si volesse avere una interazione a grana più fina.
Con la soluzione alternativa è compito del client decidere quali metodi invocare sullo strato EJB, la sequenza esatta delle invocazioni e valutare il da farsi in funzione dei risultati ottenuti.
Questa soluzione certamente più flessibile ha come controindicazione una eccessivo traffico di dati in rete nonchà© uno spostamento non giustificato della responsabilità  sul client che proprio in considerazione di quanto detto in precedenza (thin client) potrebbe essere non adeguato a prendere determinate decisioni.
E‘ spesso impossibile poter dare una risposta definitiva ed universalmente valida data la moltitudine di aspetti e requisiti che caratterizzano e differenziano ogni caso specifico ed ogni applicazione.
La tendenza per questo motivo è quella di dare sempre una impostazione che sia ragionevolmente valida per la maggior parte dei casi, consci del fatto che di volta in volta adattamenti e personalizzazioni sono sempre necessarie.
Nel caso specifico è bene fornire sempre, almeno in prima istanza, una implementazione che sia una media fra il modello black-box e quello service provider, lasciando ai raffinamenti successivi (o iterazioni come nel processo RUP, vedi [RUP]) le personalizzazioni in una o nell‘altra direzione.
Personalmente, adottando una scelta forse un po‘ forte, prediligo utilizzare session bean di tipo stateless che offrano al client non macro funzionalità  (macro-usecase) ma operazioni di medio livello relative alla ricerca, modifica ed assemblaggio dei dati elementari da esporre poi come DTO più o meno articolati.
Si immagini il caso in cui una Action Struts debba effettuare una ricerca di alcune informazioni strutturate per poi permettere la composizione di una pagina JSP; si immagini che tali informazioni non siano banalmente ottenibili con una semplice ricerca, ma che implichino la composizione di un aggregato di dati tramite una o più di una operazione di ricerca, modifica e trasformazione. In tale scenario lo strato session espone i metodi necessari per fornire al client tale aggregato di dati mascherando come esso sia stato ottenuto. Il client dal canto suo prende in consegna tali informazioni, mantenendole in sessione ed eventualmente aggregandole con altre per permettere la composizione della pagina JSP.

DTO leggeri o pesanti? Custom o standard?

La stratificazione delle applicazioni J2EE introduce nelle nostre applicazioni una serie non banale di benefici volti a semplificare le operazioni di stesura del codice, di separazione dei contesti e di manutenzione o rifattorizzazione successiva.
Questi vantaggi hanno in genere un costo che si paga in termini di complessità  delle procedure di invocazione e soprattutto di migrazione dei dati fra i vari strati.
Il pattern Data Transfert Object (DTO), dice che per ogni operazione di invocazione fra uno strato e l‘altro e fra un componente e l‘altro, i dati non dovrebbero fluire in modo sciolto ma dovrebbero sempre essere aggregati in appositi contenitori di informazioni. Per questo motivo per spostare le informazioni relative ad un articolo si dovrebbe sempre utilizzare un oggetto apposito che contenga tutte le informazioni relative cosଠcome i dati ad esso associati.
Molte sono le scelte da eseguire nel momento in cui si decide di realizzare il set dei DTO: utilizzare DTO pesanti (ovvero che contengano tutte le informazioni di una determinata entità ) o leggeri (spostare solo le informazioni strettamente necessarie relativamente ad un determinato use case); creare DTO custom (classi appositamente create) o utilizzare contenitori standard del JDK (Hashatable, Vector, List etc...).
Definire DTO di primo livello che non inglobino i DTO dipendenti (es. se devo spostare un DTO utente ci si deve chiedere se ha senso includere al suo interno anche tutti i DTO delle varie anagrafiche, profili utente etc...).
Data la vastità  dell‘argomento esso verrà  esaustivamente affrontato in un articolo apposito che pubblicheremo uno dei prossimi mesi.

Invocazione local vs remote

Anche se già  da tempo ogni produttore di application server inseriva all‘interno dei propri prodotti tecniche di ottimizzazione per le invocazioni in-JVM e in-container, con l‘introduzione delle interfacce locali nella specifica ufficiale EJB, il progettista ha a disposizione uno strumento in più per poter invocare componenti remoti deployati all‘interno del container.
Le possibili soluzioni in tale ambito sono in genere piuttosto semplici, tanto che non è lasciato molto spazio alla fantasia. Nella maggior parte dei casi, cosଠcome nella applicazione in esame si organizzano gli strati in modo che il client EJB parli con lo strato server EJB per mezzo di un session di front-end (pattern session faà§ade) per mezzo di invocazioni remote.
Il session faà§ade a sua volta comunica con gli altri session e con gli entity beans per mezzo di invocazioni locali (più efficienti).
Il client non ha possibilità  di comunicare con altri session bean o con gli entity beans della applicazione remota: questa soluzione è frutto di una precisa scelta progettuale frutto di molteplici considerazioni.

Per quanto riguarda la centralizzazione delle invocazioni in un unico bean di front end, questa soluzione permette di sfruttare tutti i benefici tipici del pattern session faà§ade.
Ad esempio il client non viene informato sulla logica organizzativa ed applicativa della parte sottostante e per questo non sono necessarie modifiche estese se si decide di cambiare qualcosa sotto il faà§ade.
Parallelamente tutte le politiche di accesso, sicurezza, ottimizzazione del flusso dei dati e quant‘altro sono concentrate in un solo punto.
Infine da un punto di vista dell‘analisi funzionale, il client deve poter utilizzare use-case di alto livello per svolgere il suo lavoro quotidiano, mentre non dovrebbe mai accedere a funzionalità  a grana più fine offerte dai session bean sottostanti il faà§ade.
Per un motivo del tutto analogo una applicazione client EJB non dovrebbe mai accedere ad un rappresentante dei dati (in questo caso si è scelto il framework entity beans CMP, ma discorso analogo se si volesse adottare Hibernate, JDO o DAO). I dati sono un patrimonio importante tanto che non deve essere esposta al client la modalità  di aggregazione, modifica, variazione etc.

I pattern coinvolti

Durante questa trattazione verranno presentati alcuni dei più famosi pattern J2EE utili per la aggregazione degli strati applicativi e per la comunicazione dei vari componenti.
Fra questi troviamo certamente il Session Faà§ade per l‘isolamento dello strato server side e l‘accentramento delle chiamate RMI, pattern che in genere è utilizzato in stretto contatto con il Business Delegate il quale offre al client una visione semplificata della invocazione remota RMI.
Come accennato in precedenza il DTO è probabilmente il pattern più importante relativamente al trasferimento dei dati.
Data la natura intrinsecamente distribuita multistrato e genericamente eterogenea dei soggetti coinvolti nella applicazione è di grande utilità  poter disporre di un servizio centralizzato ed automatizzato per ricavare le varie risorse del sistema. Il Service Locator è il componente che svolge questo compito. Al suo interno spesso si nascondono le operazioni di lookup JNDI, e si possono inserire in modo del tutto trasparente tecniche di cache o pool per migliorare le prestazioni del sistema.
Ovviamente pattern "spiccioli" come Factory Singleton e proxy sono un po‘ ovunque utilizzati e data la loro popolarità  non verranno presentati in modo esaustivo, rimandando alla bibliografia per maggiori approfondimenti.

Service Locator: la localizzazione delle risorse locali e remote

Prima di procedere con la analisi dei vari elementi che compongono la parte EJB è utile presentare un oggetto molto importante, sia per il funzionamento della parte server che del client.
Il componente in questione è basato sul famoso pattern Service Locator. La classe ServiceLocator viene utilizzata sia all‘interno del client per ricavare le interfacce remote di un session bean deployato nell‘application server, ma è usato anche da vari session beans per ricavare le interfacce locali di altri session o entities.
I due metodi principali che la classe offre sono i getEjbHome() e getEjbLocalHome() tramite i quali è possibile ricavare le home interface di bean remoti o locali e da queste poi procedere alla creazione delle interfacce local e remote vere e proprie.
Il metodo getEjbHome() consente di ricavare l‘interfaccia home remota di un bean deployato all‘interno dell‘application server e il cui nome sia corrispondente a quello passato come parametro.
Analogamente il metodo getEjbLocalHome() consente di ricavare con una semplice operazione di lookup la local home interface come oggetto registrato presso il JNDI tree dell‘application server. Questa differenza implica che nel primo caso è necessaria una vera e propria operazione di ricerca remota su un albero secondo la tipica logica RMI/IIOP, con tanto di narrowing e cast esplicito: per questo al metodo viene anche passata la classe sulla quale verrà  invocata la conversione di casti in tipico stile RMI/IIOP. Ecco il metodo relativo alla versione remota:

public static EJBHome getEjbHome(String ejbName, Class ejbClass) throws ServiceLocatorException {try {Object object = context.lookup(ejbName);EJBHome ejbHome = null;ejbHome = (EJBHome) PortableRemoteObject.narrow(object, ejbClass);if (ejbHome == null) {throw new ServiceLocatorException("Could not get home for " + ejbName);}return ejbHome;}catch (NamingException ne) {throw new ServiceLocatorException(ne.getMessage());}}

ed il corrispondente relativo alla lookup della interfaccia locale:

public static EJBLocalHome getEjbLocalHome(String ejbName) throws ServiceLocatorException {try {Object object = context.lookup(ejbName);EJBLocalHome ejbLocalHome = null;ejbLocalHome = (EJBLocalHome) object;if (ejbLocalHome == null) {throw new ServiceLocatorException("Could not get local home for " + ejbName);}return ejbLocalHome;}catch (NamingException ne) {throw new ServiceLocatorException(ne.getMessage());}}

Elemento chiave della classe è la procedura di inizializzazione del context JNDI presso il quale effettuare la chiamata remota. Il costruttore è fornito in due versioni con e senza parametri. Nel secondo caso verrà  effettuata una chiamata al metodo getInitialContext() senza parametri (quindi eseguendo la connessione verso il JNDI tree di default: questo in genere corrisponde a quello messo a disposizione dall‘application server entro la quale viene eseguita l‘applicazione chiamante oppure quello che può essere ricavato in modo automatico perchà© attivo sull‘host locale).
Il costruttore con parametri invoca il getInitialContext() passando tutti i parametri di connessione JNDI. Ecco le due implementazioni dei due costruttori:

protected ServiceLocator(Properties jndiProperties) throws ServiceLocatorException {try {context = getInitialContext(jndiProperties);}catch (Exception e) {throw new ServiceLocatorException(e.getMessage());}}protected ServiceLocator() throws ServiceLocatorException {try {context = getInitialContext();}catch (NamingException e) {throw new ServiceLocatorException(e.getMessage());}}

entrambi i costruttori sono protetti in quanto le istanze di ServiceLocator verranno ricavate tramite i metodi

public static synchronized ServiceLocator getInstance() throws ServiceLocatorException {if (serviceLocator == null) {serviceLocator = new ServiceLocator();}return serviceLocator;}public static synchronized ServiceLocator getInstance(Properties jndiProperties)throws ServiceLocatorException {if (serviceLocator == null) {serviceLocator = new ServiceLocator(jndiProperties);}return serviceLocator;}

i quali operano secondo la classica modalità  del pattern Singleton.

Ecco infine l‘implementazione dei metodi di inizializzazione vera e propria del context JNDI:

private Context getInitialContext(Properties jndiProperties) throws NamingException {if (jndiProperties == null){throw new NamingException("Impossibile inizializzare il context JNDI: jndiProperties è nullo");}Context context = new InitialContext(jndiProperties);return context;}private Context getInitialContext() throws NamingException {Context context = new InitialContext();return context;}

Da notare che per come funziona la classe ServiceLocator con poco lavoro aggiuntivo si potrebbe implementare senza troppa fatica un qualche meccanismo di cache all‘interno in modo da evitare il lookup per quelle interfacce già  ottenute in precedenza e memorizzate in una apposita tabella in memoria. Per quello che si avrà  modo di vedere in uno dei successivi articoli e soprattutto per le possibilità  di conflitto che una cache locale può avere con i meccanismi di round robin cluster (vedi [CLU]), spesso si preferisce rinunciare a questo piccolo vantaggio in termini di prestazioni per poter sfruttare al meglio le potenzialità  dell‘application server EJB senza complicarsi troppo la vita.

Conclusione

In questo primo appuntamento sono stati affrontati alcuni concetti basilari necessari per poter successivamente affrontare e comprendere il funzionamento e la strutturazione dei vari componenti della architettura complessiva. In particolare è stato presentato il ServiceLocator, elemento fondamentale di tutta l‘applicazione in quanto permette di centralizzare e di isolare la logica di lookup JNDI.
Per quanto riguarda invece le varie ipotesi ed assunzioni qui proposte si ricorda, se ancora ce ne fosse bisogno, che data la vastità  dell‘argomento si è volutamente scelto di fornire una possibile soluzione per raggiungere l‘obiettivo finale (realizzare una architettura J2EE-EJB che sia sensata e facilmente gestibile) .
Probabilmente quanto presentato in questo articolo e nei successivi non rappresenta certamente la sola alternativa o necessariamente la migliore. I lettori che volessero sondare strade alternative potranno sicuramente sperimentare con successo soluzioni altrettanto valide.

Riferimenti

[EJB]
Monson Haefel, "Enterprise Java Beans", III Edizione, O‘Reilly

[MB]
Giovanni Puliti "Enterprise Java Beans", cap. 8 di "Manuale Pratico di Java", Tecniche Nuove, 2004
http://www.mokabyte.it/manualejava2

[RUP]
Graig Larman, "Appliyng UML and Patterns", Prentice Hall

[CLU]
Giovanni Puliti, "Clustering di applicazioni J2EE - I parte: architetture Cluster EJB con JBoss" e successivi, in MokaByte 89, Ottobre 2004
http://www.mokabyte.it/2004/10

Risorse

Scarica la classe ServiceLocator nel menu in alto a sinistra

Condividi

Pubblicato nel numero
98 luglio 2005
Giovanni Puliti lavora come consulente nel settore dell’IT da oltre 20 anni. Nel 1996, insieme ad altri collaboratori crea MokaByte, la prima rivista italiana web dedicata a Java. Da allora ha svolto attività di formazione e consulenza su tecnologie JavaEE. Autore di numerosi articoli pubblicate sia su MokaByte.it che su…
Articoli nella stessa serie
Ti potrebbe interessare anche