Introduzione
Nella
puntata precedente [1] si sono analizzati gli entity bean di tipo
container managed persistence (CMP beans), ovvero quel particolare tipo
di EJB in cui la persistenza viene direttamente gestita dal container in
modo automatico utilizzando un database relazionale o ad oggetti per la
memorizzazione dei dati.
Grazie
alla gestione da parte del container, essi sono particolarmente utili nel
caso in cui non si abbiamo particolari necessità o quando sia semplice
effettuare un mapping bean-database.
In
tutti quei casi in cui invece il processo di persistenza del componente
richieda un trattamento particolare, sia per la maggiore complessità
del caso, sia per la necessità di soluzioni personalizzate, si può
passare ad implementare una gestione manuale della sincronizzazione fra
componenti e dati nel database: questi componenti sono detti entity beans
di tipo Bean Managed Persistence (BMP).
In
questo caso appare ovvio che il prezzo da pagare sia dato dalla maggiore
complessità di cui si deve tener conto: fortunatamente però
il passaggio da un tipo di componente ad un altro è relativamente
semplice grazie alla filosofia di base del modello EJB. Infatti il tutto
si traduce nel dover implementare i vari metodi di callback invocati dal
server, ovvero i vari metodi creazionali, come l’ejbCreate(), quelli di
sincronizzazione ejbStore() ed ejbLoad() e quelli di ricerca.
Dal
punto di vista invece del client e di tutto il resto dell’applicazione,
non si ha nessun impatto, e questo rappresenta probabilmente il maggior
vantaggio di tutta la filosofia EJB.
Gestione delle
eccezioni
Dato
che dover gestire la persistenza in modo manuale significa dover implementare
metodi che secondo il modello RMI sono remoti, uno degli aspetti più
importanti di cui si deve tener conto è la gestione delle eccezioni.
La
prerogativa principale di un metodo remoto è quella di poter generare
eccezioni remote, principalmente di tipo RemoteException.
Nella
versione 1.0 la specifica EJB impone che tutti i metodi remoti possano
generare una eccezione di questo tipo; nel caso in cui all’interno dei
vari metodi ejbCreate() o ejbStore() possano essere generate altri tipi
di eccezioni, queste dovranno essere catturate e prorogate all’interno
di eccezioni remote.
Questa
tecnica, tipica nel mondo RMI prende il nome exception wrapping, ed particolarmente
utile in tutti quei casi in cui la business logic del bean si appoggia
a sistemi sottostanti per la gestione di particolari risorse, come nel
caso di JDBC o JNDI o JavaMail.
Un
esempio di tale tecnica molto semplice potrebbe essere
public
void ejbLoad() throws RemoteException{
…
try{
… Esegue una ipotetica operazione con una sorgente JDBC
}
catch(SQLException sqle){
throw new RemoteException(sqle);
}
}
Con
il passaggio alla versione 1.1, le eccezioni generati devono essere wrappate
in EJBException, che discendendo dalla RuntimeException, non richiede la
dichiarazione della clausola throws nella firma del metodo. Il pezzo di
codice appena visto quindi, nella versione 1.1 potrebbe essere riscritto
nel seguente modo
public
void ejbLoad(){
…
try{
… Esegue una ipotetica operazione con una sorgente JDBC
}
catch(SQLException sqle){
throw new EJBException(sqle);
}
}
Entity Context
1.0 e 1.1
Il
contesto di esecuzione offre al bean una serie di informazioni e servizi
molto utili durante il processo di salvataggio o di caricamento dei dati
del bean.
Un
EntityContext è assegnato alla istanza del bean ogni volta che un
nuovo bean viene creato, all’inizio del suo ciclo di vita, prima che sia
reso disponibile per servire i vari client.
Tale
reference in genere viene posto in una variabile di istanza del bean
e mantenuto per tutto il suo ciclo di vita.
Se
si riconsidera il ciclo di vita di un componente si rammenterà il
meccanismo di swapping messo in atto sui vari componenti in esecuzione
sul container. Durante tale cambio di contesto il container agisce anche
sull’ EntityContext del bean tramite il metodo setEntityContext().
Sia
l’interfaccia SessionContest che la EntityContext estendono la EJBContext;
ecco la definizione di tale interfaccia
public
interface EntityContext extends EJBContext{
public EJBObject getEJBObject() throws IllegalStateException;
public Object getPrimaryKey() throws IllegalStateException;
}
L’EJBContext
Il
contesto comune a tutti i tipi di componenti è dato dalla interfaccia
EntityContext dalla quale discendono direttamente sia l’EJBContext che
il SessionContext.
I
metodi della EntityContext sono specificatamente pensati per consentire
una migliore gestione del ciclo di vita, della sicurezza e gestione delle
transazioni di un entity.
I
metodi propri della EJBContext invece sono più generici, ma ugualmente
importanti: il getEJBObject() restituisce un reference remoto al bean object,
per poter essere utilizzata dal client o da un altro bean. Dal punto
di vista del componente esso può essere utilizzato per avere un
riferimento a se stesso tutte le volte che si deve effettuare una chiamata
in loopback, ovvero quando un bean invoca un metodo di un altro bean passando
come parametro un reference a se stesso. Ad esempio
public
class MyBean extends EntityBean{
public EntityContext context;
public myMethod(){
YourBean yb= …
EJBObject obj= Context.getObject() ;
MyBean mb= (MyBean) PortableObject.narrow(obj, MyBean.class);
yb.yourMethod(mb);
}
}
Come
si può notare il passaggio di un reference a se stesso non è
stato fatto tramite la keyword this, cosa non permessa, ma tramite un reference
remoto ricavato dal contesto. Questo comportamento è dovuto alla
particolarità delle chiamate rientranti come si è visto in
precedenza [2]. I session bean a tal proposito definiscono un metodo getObject()
nella interfaccia EntityContext, il cui funzionamento è esattamente
lo stesso.
Anche
il metodo getEJBHome(), disponibile sia per i session che per gli
entity beans, è definito nella EJBContext: tale metodo restituisce
un reference remoto al bean, e può essere utilizzato sia per creare
nuovi bean, sia per effettuare delle ricerche nel caso di entity bean.
Infine
il metodo getPrimaryKey(), restituisce una copia della chiave primaria
assegnata: sebbene, il suo utilizzo al di fuori dei metodi ejbLoad()
ed ejbStore() sia piuttosto raro, così come accade per il metodo
ejbHome(), poter disporre della chiave primaria è un utile accessorio
dove sia necessaria una gestione particolare del componente, proprio come
accade usualmente nei bean BMP.
Durante
il ciclo di vita all’interno del suo contesto, le varie informazioni accessibili
tramite l’EJBContext possono variare, ed è per questo che tutti
i metodi possono generare una IllegaStateException: ad esempio perde di
significato ricorrere alla chiave primaria quando un bean si trova in stato
di swapped, ovvero quando non è assegnato a nessun EJBObject, anche
se può disporre di un EJBContext.
L’EntityContext
Questa
interfaccia mette a disposizione alcuni metodi utili durante il ciclo di
vita del componente. Ecco la sua definizione
public
interface EJBContext{
public EJBHome getEJBHome();
// metodi per la security
public java.security.Principal getCallerPrincipal();
public boolean isCallerInRole(String roleName);
// metodi deprecati in 1.1
public java.security.Identity getCallerIdentity();
public boolean isCallerInRole(java.security.Identity id);
public Properties getEnvironment();
// metodi transazionali
public UserTRansaction getUserTransaction()
throws IllegalStateException;
public boolean getRollbackOnly()throws IllegalStateException;
public boolean setRollbackOnly()throws IllegalStateException;
}
Il
metodo getEJBHome() restituisce un reference alla propria HomeInterface.
Questo può essere utile per effettuare ricerche o per ricavare reference
di altri bean.
Il
getCallerPrincipal(), che dalla versione 1.1 sostituisce il getCallerIdentity(),
permette di ricavare il reference al client invocante.
Quando
invece si deve implementare un controllo a grana più fine sulla
sicurezza, il metodo isCallerInRole() permette di controllare in modo veloce
ed affidabile il ruolo di esercizio del client invocante.
Per
quanto riguarda i metodi transazionali invece, essi saranno affrontati
in seguito, quando si parlerà in dettaglio della gestione delle
transazioni.
L’interfacciamento
con il database: gestione di JDBC
Per
poter gestire i dati relativi al bean memorizzati nel database, si
deve utilizzare una una connessione verso il database, la quale connessione
può essere ottenuta direttamente tramite le tecniche standard JDBC,
oppure ricavata per mezzo di JNDI da una sorgente dati opportunamente predisposta.
Utilizzando
questa seconda soluzione, il metodo getConnection(), utilizzato in seguito
negli altri metodi del bean per poter leggere e scrivere i dati nel database,
potrebbe essere così definito
private
Connection getConnection() throws SQLExcepion {
try{
Context context= new InitialContext();
DataSource source= (DataSource)context.lookup(SourceName);
return source.getConnection();
}
catch(NamingException ne){
throw new EJBException(ne);
}
}
dove
SourceName è il nome JNDI dato alla sorgente dati: ogni bean infatti
può accedere ad una serie di risorse definite all’interno del JNDI
Environment Naming Context (ENC), il quale viene definito al momento del
deploy del componente tramite il cosiddetto deployment descriptor.
Questa
organizzazione, introdotta con la versione 1.1 della specifica, e valida
non solo per le sorgenti JDBC, ma anche per il sistema JavaMail o
JMS, rende il componente ulteriormente indipendente dal contesto di esecuzione,
rimandando al momento del deploy la sua configurazione tramite semplici
file XML editabili con il tool di deploy utilizzato.
Ad
esempio, supponendo che sia
SourceName
= “java:comp/env/jdbc/myDb”;
allora
nel file deployment descriptor XML si avrebbe
…
<resource-ref>
<description>DataSource
per il MyBean</description>
<res-ref-name>jdbc/myDb</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
…
La
modalità da seguire nel caso di EJB 1.0 è differente, ed
anche se più standard per quanto riguarda la parte JDBC, vincola
maggiormente il codice Java prodotto: anche in questo caso però
è utile notare quale sia la procedura da seguire.
Nel
metodo getConnection() in questo caso si utilizzerà l’EJBContext
per poter accedere alla nome della sorgente JDBC: ad esempio
private
Connection getConnection() throws SQLExcepion {
Properties Props= context.getEnvironment();
String JdbcUrl=Props.getProperty("jdbcUrl");
return DriverManager.getConnection(JdbcUrl);
}
Dato
che il deploy in EJB 1.0 deve avvenire tramite un programma Java apposito,
comunemente detto MakeDD.java, si dovrà provvedere ad aggiungere
in tale classe qualcosa del tipo
Properties
Props= new Properties();
Props.put("jdbcUrl","jdbc:<subprotocol>:<name>");
La nuova gestione
del ciclo di vita
La
gestione della persistenza secondo la modalità manuale impatta su
tutti i metodi relativi al ciclo di vita, visto che ogni volta che un componente
subisce un cambio di contesto, che viene creato o distrutto, devono
essere modificati i dati presenti nel database.
Nella
versione CMP dei bean non era necessario preoccuparsi troppo di questi
aspetti: nei BMP invece una buona implementazione è di fondamentale
importanza.
Fortunatamente,
pur passando ad una modalità manuale, non ci si deve preoccupare
della gestione del ciclo di vita: il programmatore infatti dovrà
concentrare l’attenzione ed i propri sforzi sul funzionamento dei
vari ejbCreate(), ejbLoad() o ejbStore(), ma non del momento in cui
tali metodi debbano essere invocati, compito svolto dal server.
Di
seguito sono riportate le considerazioni più importanti relative
ad ognuno di questi metodo:
-
ejbCreate():
tale metodo viene invocato indirettamente dal client quando questo invoca
il metodo create della interfaccia home, ovvero quando genera un nuovo
componente. Nel caso dei BMP questo metodo è responsabile di creare
i dati relativi al bean all’interno del database. Oltre alla diversa implementazione
del corpo del metodo, rispetto ai CMP la differenza principale è
il diverso valore ritornato: in questo caso infatti è obbligatorio
che sia ritornata la chiave primaria del nuovo entity, e non void
(null in EJB 1.0) come per i bean CMP.
Da
un punto di vista strettamente implementativo, nel caso in cui il database
d’appoggio scelto sia di tipo relazionale, all’interno del metodo
troveremo delle istruzioni insert SQL, che effettueranno una o più
insert delle varie variabili d’istanza del componente. Si ricordi che in
questo caso tutte le eccezioni SQL dovranno essere wrappate e propagate
come EJBExceptions o RemoteExceptions come detto in precedenza.
-
Il metodo
ejbPostCreate()
invece, non è particolarmente interessato dalla strategia di persistenza
utilizzata e deve sempre ritornare void.
-
ejbLoad()
ejbStore():
questi due metodi sono invocati tutte le volte che il container decide
che sia necessaria una sincronizzazione fra i dati memorizzati nel database,
e le variabili del componente. Il primo verrà invocato in concomitanza
di una transazione o prima di un business method, in modo da avere la certezza
che il bean contenga i dati più recenti contenuti nel database.
Il
corpo del metodo non sarà molto differente da ejbCreate(), tranne
che in questo caso, sempre nell’ipotesi di utilizzare un database relazionale,
si troveranno delle SQL update piuttosto che delle insert.
Continuano
a valere anche in questo caso le considerazioni sulla gestione wrappata
delle eccezioni.
-
ejbRemove():
considerazioni praticamente analoghe a quelle dei casi precedenti
sono quelle relative al metodo ejbRemove(), dove di fatto si deve provvedere
alla cancellazione dei dati nel database.
-
Metodi
di ricerca: per ogni metodo di ricerca presente nella home interface,
dovrà essere presente un analogo nel bean. Si ricordi infatti che
il client effettua le invocazioni direttamente sulla home che poi inoltra
tali chiamate direttamente al bean. Nel caso dei BMP, i metodi di
ricerca sono responsabili di trovare nel database i vari record che
corrispondono ai criteri di ricerca impostati.
Ad
esempio se nella home è presente una cosa del tipo
public
interface MyHome extends javax.ejb.EJBHome{
public MyBean findByPrimaryKey(MyBeanPK pk)
throws FinderException, RemoteException;
public Enumration findByName(String name)
throws FinderException, RemoteException;
}
allora
si avrà
public
class MyBean extends javax.ejb.EntityBean{
public MyBeanPK ejbFindByPrimaryKey(MyBeanPK pk)
throws FinderException, RemoteException{}
public Enumration ejbFindByName(String name)
throws FinderException, RemoteException{}
}
dove
in questo caso FinderException
è stata definita appositamente per segnalare l’insorgere di problemi
durante la fase di ricerca.
Si
noti inoltre anche i differenti tipi di valori ritornati dai metodi dell’interfaccia
e del bean.
Anche
in questo caso la differenza principale fra EJB 1.0 e 1.1 è data
dal tipo ritornato nel caso in cui nessun elemento sia trovato: nella versione
precedente infatti si ottiene un null, mentre nella 1.1 sono ritornate
collezioni vuote di elementi.
Ciclo di vita
di un entity bean
Concludendo
la sezione dedicata agli entity bean, sia CMP che BMP, è necessario
affrontare nel dettaglio il ciclo di vita di questi componenti, in modo
da avere chiaro quale sia il significato dei vari metodi visti fino
a questo punto. Si ricordi infatti che, tralasciando per un momento i dettagli
legati alla sintassi o alla differente modalità di invocazione,
da un punto di vista generale, non esiste particolare differenza fra un
CMP e BMP se non per quello che sta dietro alla differente modalità
di gestione del database sottostante.
Vedremo
quindi di analizzare tutto il percorso che un componente compie durante
i passaggi di stato all’interno del ciclo di vita per comprendere come
e quando i vari metodi siano invocati.
Come
prima cosa è bene ricordare che il ciclo di vita del bean è
gestito in modo automatico dal container sia in funzione delle chiamate
da parte del client sulla interfaccia remota, sia direttamente sui metodi
della home che poi si riflettono in callback sul bean stesso.
-
Stato
pre-caricamento
In
questo stato il bean ancora non esiste come entità astratta, ma
piuttosto come collezione di file .class e di descriptor files: devono
essere forniti al server la primari key, la remote interface e la home,
oltre a tutti i file generati i modo automatico dal deploy.
-
Pooled
State
Nel
momento della partenza del server, questo carica in memoria tutti i bean
di cui sia stato effettuato correttamente il deploy, posizionandoli nel
pool dei bean pronti (vedi [2]). In tale fase viene creata un’istanza
del componente utilizzando il metodo bean.newIstance(), il quale a sua
volta invoca il costruttore del bean: come accade in RMI, anche in questo
caso per il bean non deve essere specificato nessun costruttore, e l’invocazione
avviene su quello di default. In questa fase tutte le variabili d’istanza
assumono il loro valore di default, ed il container assegna l’EntityContext
tramite il metodo setEntityContext(). In questo momento il componente è
disponibile per essere utilizzato quando ci sarà bisogno di servire
le richieste del client. Tale situazione perdurerà fino a quando
non verrà invocato un metodo di ricerca.
Nessun
bean in questo caso è assegnato ad un EJBObject, e non contiene
informazioni significative.
-
Ready
State
Il
passaggio al Ready State può avvenire o per esplicita chiamata di
una find da parte del client o per creazione diretta per mezzo dell’invocazione
del metodo create.
Nel
secondo caso per prima cosa viene creato un EJBObject, lo skeleton dell’oggetto
secondo la terminologia RMI, ed assegnazione di un bean prelevato dal pool.
In questo momento caso l’invocazione del metodo create da parte del client,
viene propagata all’ejbCreate(): al suo termine una chiave primaria
viene generata ed assegnata all’EJBObject. In questo momento sono effettuate
tutte le operazioni di preparazione dei dati sul database in uno dei due
modi, a seconda della politica scelta per la gestione della persistenza
(CMP/BMP). Successivamente il controllo passa al metodo ejbPostCreate()
per terminare la fase di inizializzazione.
Infine
il client riceve lo stub dell’oggetto remoto, con il quale potrà
effettuare tutte le invocazioni da remoto in perfetto accordo con quanto
avviene nel modello RMI.
Tutto
questo processo viene realizzato in modo analogo nel caso di una invocazione
di una find, su tutti i componenti trovati.
Leggermente
differente è invece il caso relativo al passaggio al ready state
tramite attivazione. In questo caso il bean viene prelevato dal pooled
state dove era finito in seguito ad una passivazione: qui, benché
il bean non fosse presente in memoria, il container aveva mantenuto
un legame fra lo stub del client e l’EJBObject: durante l’attivazione
quindi i dati sono prelevati dal database e rassegnati al bean appena creato
prima che questo sia accoppiato con l’EJBObject.
Analogamente
il passaggio inverso, ovvero dal ready al pooled avviene ugualmente sotto
il verificarsi di vari eventi, ovvero sia su invocazione diretta del client
oppure in base al meccanismo di passivazione attuato dal server.
Anche
in questo caso i metodi ejbStore() ed ejbPassivate() sono invocati in modo
da mantenere sincronizzato il bean con i record del database. La sequenza
precisa con cui tali invocazioni sono effettuate dipende dalla implementazione
del server.
Anche
in questo caso, le cose sono analoghe sia nel caso di CMP che BMP.
Conclusione
Si
conclude qui la trattazione degli entity bean. Dalla prossima puntata inizieremo
a parlare di session beans, analizzandone sia il funzionamento che lo scopo
principale.
Ricordo
che fra le molte pagine pubblicate questo mese ed i precedenti è
possibile trovare approfondimenti ed esempi applicativi della tecnologia
EJB.
Bibliografia
[1]
– “Corso di EJB: III parte - Gli entity CMP” di Giovanni Puliti,
MokaByte
47 – www.mokabyte.it/2000/12
[2]
– “Corso di EJB – II parte: i servizi di sistema” di Giovanni Puliti,
MokaByte
46 – www.mokabyte.it/2000/11
[3]
– “ EJB’s Design Techniques – I parte” – di Raffaele Spazzoli,
MokaByte
47 – www.mokabyte.it/2001/01
[4]
– “ EJB’s Design Techniques – II parte” – di Raffaele Spazzoli,
MokaByte
48 – www.mokabyte.it/2001/02 |