Introduzione
Il mese scorso [1] abbiamo introdotto la nuova Servlet
API 2.3 affrontando quella che forse è la novità
più interessante aggiunta dai progettisti di
Sun, i filtri HTTP.
Questo mese parleremo delle rimanenti modifiche ed
aggiunte alla API, delle quali l'introduzione del
meccanismo dei listeners rappresenta la novità
più interessante.
Ricordo che chi desiderasse provare ad implementare
gli esempi che accenneremo in questa sede, dovrà
utilizzare un servlet engine compatibile con la versione
2.3 della API. Fra tutti i prodotti attualmente disponibili,
pochissimi implementano le recenti novità della
API, per cui si consiglia di fare riferimento alla
documentazione del server. Le prove fatte per questo
articolo si riferiscono a Tomcat 4.0 [2], probabilmente
la migliore implementazione attualmente disponibile
sul mercato.
Ascoltatori
e ciclo di vita
Il ciclo di vita di un Servlet segue precise regole:
a partire dalla inizializzazione tramite il metodo
init(), la gestione delle richieste dei client, metodo
service(), e lo spegnimento del servlet tramite il
metodo destroy().
Da quando Sun ha introdotto con la nascita della piattaforma
Java2EE concetti come Container e ServletContext lo
scenario si notevolmente complicato: il servlet context
infatti permette a due servlet o ad una pagina JSP
ed un Servlet di scambiarsi oggetti, oppure ad un
servlet di ottenere maggiori informazioni circa l'oggetto
chiamante ed i parametri di sessione. Anche l'oggetto
Session, presente fin dalla prima release delle Servlet
API, ha assunto un ruolo molto più importante,
specie nel caso di web application basate su pagine
JSP e Servlet.
Per questo motivo, al fine di permettere una maggiore
e più semplice gestione di tutte queste funzionalità,
è stato aggiunto un utilissimo meccanismo basato
su ascoltatori sintonizzati sui vari cambiamenti che
interessano sia il ServletContext che l'oggetto Session.
Implementando alcune semplici interfacce un ascoltatore
adesso potrà essere informato tutte le volte
che un oggetto viene aggiunto al contesto o alla sessione.
Più precisamente, mentre la sessione viene
associata ad ogni client collegato, il context nasce
e muore insieme ad una web application. Visto quindi
che l'ascoltatore può essere notificato sugli
eventi di creazione e distruzione del ServletContext,
non è improprio, anche se formalmente non corretto,
dire che le modifiche apportate hanno dato vita ad
un nuovo ciclo di vita del servlet: in realtà
niente è cambiato nel ciclo di vita. Il meccanismo
di notifica degli eventi è lo stesso degli
eventi utilizzati in Swing o AWT, meccanismo che come
noto è basato sul cosiddetto Delegation Model
[3], o pattern Observer[4] .
Ascoltatori
di contesto
L'interfaccia che deve essere implementata per creare
un ascoltatore sincronizzato sul ciclo di vita del
ServletContext è la ServletContextListener,
che contiene i seguenti due metodi:
void
contextInitialized(ServletContentEvent sce)
void contentDestroyed(ServletContentEvent sce)
Il
primo viene richiamato quando la web application è
pronta per eseguire le chiamate dei client HTTP. In
particolare la Web Application non sarà pronta
fino a che questo metodo non abbia terminato la sua
esecuzione. Il secondo metodo invece viene invocato
dal server nel momento in cui la web application viene
spenta, o quando più genericamente il context
viene rimosso (ad esempio nel caso di applicazioni
mobili fra più application server). La gestione
delle richieste viene sospesa un attimo prima della
invocazione di tale metodo.
In tutti e due i casi l'oggetto ServletContextEvent,
tramite il metodo getServletContext(), permette di
ricavare un riferimento al contesto appena dopo di
essere inizializzato o prima di essere rimosso.
Nel caso in cui un oggetto sia interessato ad osservare
il movimento degli oggetti inseriti o rimossi dal
contesto, potrà implementare l'interfaccia
ServletContextAttributeListener. In questo caso i
metodi della interfaccia da implementare saranno:
void
attributeAdded(ServletContextAttributeEvent sceae)
void attributeRemoved(ServletContextAttributeEvent
sceae)
void attributeReplaced(ServletContextAttributeEvent
sceae)
Il
significato e funzionamento di tali metodi dovrebbe
apparire piuttosto intuitivo. L'oggetto ServletContextAttributeEvent,
che deriva direttamente dal ServletContextEvent estende
il padre aggiungendo i metodi getName() e getValue()
i quali permettono di avere maggiori informazioni
sugli oggetti rimossi, aggiunti o modificati.
Ascoltatori
di sessione
Per quanto riguarda la creazione di ascoltatori sincronizzati
sugli eventi delle sessioni, il meccanismo è
piuttosto simile. Anche in questo caso è stata
aggiunta una interfaccia, la HTTPSessionListener,
che mette a disposizione i seguenti metodi
void
sessionCreated(HttpSessionEvent hse)
void sessionDestroyed(HttpSessionEvent hse)
Il
primo viene invocato al momento della creazione della
sessione, mentre l'altro alla sua distruzione. Anche
in questo caso gli oggetti passati come parametri
permettono di ottenere maggiori informazioni circa
la sessione in esame. In particolare il metodo getSession()
consente di ottenere un oggetto Session.
Anche in questo caso è possibile implementare
un controllo più fine concentrando l'attenzione
di un ascoltatore solo sugli oggetti aggiunti o rimossi
dalla sessione: infatti implementando l'interfaccia
HttpSessionAttributeListener ed i metodi
void
attributeAdded(HttpSessionBindingEvent hsbe)
void attributeRemoved(HttpSessionBindingEvent hsbe)
void attributeReplaced(HttpSessionBindingEvent hsbe)
L'oggetto
HttpSessionBindingEvent estende la più generica
HttpSessionEvent, ed aggiunge I metodi getName() e
getValue per conoscere nome e valore dell'oggetto
inserito, rimosso o modificato nella sessione. La
scelta del nome al posto di HttpSessionAttibuteEvent
è stata fatta per mantenere la compatibilità
con le versioni precedenti della API, visto che era
già presente una classe con questo nome. E'
possibile che nelle versioni successive, Sun decida
di apportare modifiche in tal senso.
Implementazione
degli ascoltatori
Dopo aver brevemente accennato i concetti di base
relativamente agli ascoltatori, passiamo adesso a
vedere un possibile utilizzo pratico di questi oggetti.
Per prima cosa vedremo come implementare un ascoltatore
di contesto ed uno di sessione, cercando successivamente
di dare un senso ed una motivazione sull'utilizzo
di questi strumenti.
Per creare un ascoltatore la prima cosa da fare è
creare una classe che implementi una delle interfacce
appena viste. Vediamo prima il caso di un ascoltatore
di contesto: la classe potrebbe avere questa struttura
public
final class MyContextListener
implements ServletContextAttributeListener, ServletContextListener
{
...
}
In
questo caso l'ascoltatore che andremo a realizzare
sarà in grado di "ascoltare" sia
gli eventi relativi al contesto vero e proprio che
ai relativi attributi.
I vari metodi della interfaccia potrebbero in una
versione molto semplice di questo ascoltatore effettuare
un log su file tutte le volte che si verifica un evento.
Ad esempio potremmo scrivere
public
void attributeAdded(ServletContextAttributeEvent event)
{
fileLog("Aggiunto attributo
al contesto " +
event.getName()
+
", "
+ event.getValue());
}
dove
il metodo fileLog() potrebbe utilizzare le funzionalità
messe a disposizione dal contesto per effettuare log
su file; ad esempio
private
void fileLog(String message) {
if (context != null)
context.log("MyContextListener:
" + message);
else
System.out.println("MyContextListener:
" + message);
}
Gli
altri metodi dell'ascoltatore potrebbero effettuare
operazioni analoghe o del tutto differenti. In questo
caso tralasciamo questi aspetti rimandando alla fantasia
del programmatore.
Per la realizzazione di un ascoltatore di sessione
invece potremmo creare un oggetto che, come nel caso
precedente, possa funzionare come ascoltatore di tutti
i tipi di eventi riguardanti una sessione. Ad esempio
public
final class MySessionListener implements
HttpSessionAttributeListener, HttpSessionListener
{
...
//
uno dei metodi per la gestione degli eventi
public
void attributeAdded(HttpSessionBindingEvent event)
{
fileLog("Attributo
aggiunto alla sessione " + event.getSession().getId()
+
":
" + event.getName() + ", " + event.getValue()
);
}
}
Riconsiderando
per un momento il meccanismo implementato in Swing
o AWT circa la gestione degli eventi, si ricorderà
che per rendere operativo un ascoltatore lo si deve
registrare come listener di una determinata sorgente.
In questo caso il servlet container effettua una operazione
del tutto analoga: l'unica differenza è che
essendo la sorgente unica per tutti i listener, il
servlet container stesso, così come i tipi
di eventi. in questo caso la registrazione si limita
a comunicare al server il nome della classe associata
ad un listener. Per fare questo al solito si ricorre
ad un apposito tag XML, da inserire nel file web.xml
della web application che contiene l'oggetto ascoltatore.
Ad esempio si potrebbe pensare di scrivere:
<!-un
semplice ascoltatore di eventi di contesto -->
<listener>
<listener-class>
com.mokabyte.listeners.MyContextListener
</listener-class>
</listener>
Come
e quando utilizzare gli ascoltatori
Il significato di questa importante aggiunta alla
API potrebbe non essere immediatamente chiaro a tutti,
ma in realtà rappresenta un ulteriore passo
nella direzione della semplificazione della gestione
delle applicazioni Enterprise. Per potersi convincere
di questo si potrebbero elencare moltissimi casi in
cui l'utilizzo degli ascoltatori possa essere utile.
Per brevità si analizzeranno solo due semplici
casi.
Il primo riguarda la gestione della connessione verso
un database, e la sua ottimizzazione tramite l'utilizzo
di ascoltatori del contesto. In questo caso si potrebbe
utilizzare un connection pool al fine di ottimizzare
le risorse e ridurre i tempi di attesa nella apertura
e chiusura delle connessioni. Tale connection pool
potrebbe essere inizializzato (lettura dei parametri
di configurazione, apertura delle connessioni, e così
via) prima del suo utilizzo, tramite un metodo startup(),
e viceversa disattivato quando non più necessario,
metodo shutdown().
In questo scenario si dovrebbe consentire ad ogni
Servlet di ricavare una propria istanza di connection
pool, ma anche di effettuare l'operazione di inizializzazione
una sola volta, alla prima richiesta da parte di un
Servlet.
Tipicamente questo tipo di operazioni viene svolto
tramite l'utilizzo combinato del pattern Factory [5]
(per permettere ad ogni servlet di ricavare una propria
istanza del connection pool), e del Singleton (per
effettuare l'inizializzazione una volta soltanto).
Sebbene questa soluzione non sia particolarmente complessa,
obbliga ad inserire nei metodo init() e destroy()
di ogni Servlet una serie di operazioni concettualmente
non banali.
Si potrebbe pensare di sfruttare il ciclo di vita
del contesto tramite opportuni ascoltatori per effettuare
una inizializzazione universale per tutti i servlet.
Ad esempio
public void contextInitialized(ServletContextEvent
event) {
//
effettua la creazione del connection pool
//
invocando il costruttore con gli opportuni parametri
ConnectionPool
cp= new ...
cp.startup();
event.getServletContext().setAttribute("connectionpool",cp);
}
a
questo punto dopo la creazione del contesto, tutti
i servlet potranno ricavare il connection pool che
sarà disponibile come attributo di sistema.
Ad esempio:
public
void doGet(HttpServletRequest request,
HttpServletResponse
response)
throws
ServletException, IOException{
//
ricava il connectio pool come attributo di contesto
ServletContext
context = getServletContext();
ConnectionPool
cp; cp=(ConnectionPool)context.getAttribute("connectionpool");
con=cp.getConnection();
}
Anche
la gestione dello spegnimento del connection pool
in modo corretto, ovvero quando tutti i servlet verranno
rimossi dalla memoria, potrà essere eseguita
da un ascoltatore opportunamente sincronizzato:
public
void contextDestroyed(ServletContextEvent event) {
//
ricava il connectio pool come attributo di contesto
ServletContext
context = event.getServletContext();
ConnectionPool
cp; cp=(ConnectionPool)context.getAttribute("connectionpool");
cp.shutdown()
;
}
Un
altro interessante utilizzo dei listener, sia di sessione
che di contesto, potrebbe essere quello di realizzare
un sistema monitor che controlli in ogni momento il
numero di web application in funzione e di sessioni
attive, ovvero di client connessi. Non entreremo nei
dettagli di una applicazione di questo tipo, visto
che lo spazio a disposizione non lo permette. Invece
per comprendere meglio quello che potrebbe essere
il risultato finale, si può vedere in figura
1, l'output prodotto dalla web application di gestione
remota di Tomcat 4.0.
Figura
1 - La web application "Manager" di
esempio fornita in
Tomcat 4.0 consente di monitorare ogni singola web
application
in funzione ed il numero di sessioni attive
Selezione
della codifica dei caratteri
Con la versione 2.3 della API è possibile adesso
specificare il set di caratteri con cui il servlet
deve interpretare la request del client. Il metodo
request.setCharacterEncoding(String
encoding)
consente
di effettuare tale impostazione e di permettere in
modo corretto ad esempio una successiva lettura dei
parametri di invocazione. Ad esempio se il client
utilizza il set di caratteri Shift_JIS, uno dei set
di caratteri utilizzati per la lingua giapponese,
per poter correttamente interpretare un parametro
passato all'invocazione, si potrebbe pensare di scrivere
request.setCharacterEncoding("Shift_JIS");
String param=req.getParameter("param");
Questo
genere di operazioni è necessario in tutti
quei casi in cui il set di caratteri di default, il
Latin-1 (ISO 8859-1) può portare ad errori
di interpretazione, come nel caso della lingua giapponese
appunto. Normalmente infatti il set di caratteri se
diverso da quello di default dovrebbe essere specificato
come intestazione nell'header della richiesta, ma
tale assunzione non è detto che sia sempre
rispettata dai vari browser o client HTTP.
Per questo motivo il set di caratteri deve essere
specificato prima di ogni operazione di tipo getParameter()
o getReader(); la chiamata al metodo setCharacterEncoding()
può generare una eccezione di tipo java.io.UnsupportedEncodingException
se la codifica non è supportata.
Gestione
separata dei vari classloaders
Una modifica piuttosto importante è stata apportata
al classloader delle web applications. Adesso ogni
web application esegue il caricamento delle classi
in uno spazio di memoria indipendente dalle altre,
ma anche e soprattutto indipendente da quello del
server. Questo secondo aspetto, apparentemente non
rilevante, ha invece importanti ripercussioni in fase
di deploy di una applicazione. Infatti prima non era
infrequente che una qualche libreria caricata dalla
JVM del server andasse in conflitto con quelle necessarie
alle varie web application, a discapito di queste
ultime (dato che il server carica in memoria le classi
prima delle singole web application). Il caso più
eclatante e frequente era quello del conflitto di
parser XML: dato che il server spesso utilizza file
XML per la configurazione complessiva, utilizza strumenti
come Xerces o simili, che spesso però sono
utilizzati anche all'interno della web application
(magari con una versione differente).
E' per questo motivo che infatti dalla versione 4.0
Tomcat non fornisce più nessun parser XML di
default alle varie web application le quali devono
provvedere a contenerne uno in modo autonomamente,
ad esempio tramite un file .jar all'interno della
directory lib della applicazione.
Nuovi
attributi di protezione
Nell'ambito delle connessioni in modalità protetta
tramite protocollo HTTPS, la nuova api fornisce due
nuovi attributi in grado di determinare il livello
di sicurezza relativamente della chiamata in atto.
I due attributi sono
javax.servlet.request.chiper_suite
javax.servlet.request.key_size
il
primo permette di ricavare la chiper suite utilizzata
da HTTPS se presente, mentre il secondo indica il
numero di bit utilizzati dall'algoritmo di crittografia.
Utilizzando tali informazioni un servlet adesso può
rifiutare una connessione se il livello di sicurezza
o l'algoritmo utilizzato non risultano idonei. Ad
esempio
public
void service(HttpServletRequest req,
HttpServletResponse
res)
throws
ServletException, IOException {
Integer
CriptSize =(Integer)
req.getAttribute("javax.servlet.request.key_size");
if(CriptSize
== null || CriptSize.intValue() < 128){
response.setContentType("text/html");
PrintWriter
out = response.getWriter();
out.println("<html>");
out.println("<head><title>HTTPS
Servlet </title></head>");
out.println("<body>");
out.println("<p>
La chiamata non è sufficientemente
sicura
e verrà ignorata </p>");
out.println("</body></html>");
}
}
Altre
piccole modifiche
Fra le modifiche di minore importanza, si può
fare riferimento ai metodi della classe HttpUtils,
che sono stati spostati adesso in altri punti secondo
una logica più consona con l'intera API. I
metodi relativi alla creazione di URL a partire da
una request sono stati spostati nella HTTPServletRequest
stessa, che contemporaneamente è stata aggiunta
di altri metodi di utilità. Ad esempio
StringBuffer request.getRequestUrl() restituisce una
StringBuffer contenente l'url della richiesta.
Il metodo java.util.Map request.getParameterMap()
restituisce una mappa non modificabile con tutti i
parametri della invocazione. I nomi dei parametri
sono utilizzati come chiave della mappa, mentre i
valori sono memorizzati nei corrispondenti valori
della mappa. Per i valori multipli al momento non
è stato specificato quale convenzione verrà
utilizzata, probabilmente tutti i valori verranno
restituiti come array di stringhe.
Inoltre sono stati aggiunti all'oggetto ServletContext
i metodi getServletContextName(), che consentono di
ricavarne il nome, e getResourcePaths() che restituisce
un elenco non modificabile di stringhe corrispondenti
a tutti i path delle risorse disponibili all'interno
del contesto. In particolare ognuna di queste stringhe
inizia per "/" e deve essere considerato
un indirizzo relativo al contesto in esame.
L'oggetto HttpServletResponse invece è stato
aggiunto del metodo resetBuffer() il cui scopo è
quello di eliminare ogni contenuto all'interno del
buffer, senza pero' cancellare le intestazioni o il
codice di stato, prima che la risposta sia inviata
al client. In caso contrario viene generata una eccezione
di tipo IllegalStateException.
Infine dopo una lunga discussione del comitato che
segue l'evoluzione della API è stato chiarito,
rispetto alla versione precedente che lasciava in
sospeso questo aspetto, che il metodo HttpServletResponse.sendRedirect(String
uri) effettui un reindirizzamento verso l'indirizzo
specificato traducendo tale indirizzo relativo in
indirizzo assoluto ma relativamente al container e
non al context in uso. Più semplicemente in
una chiamata del tipo
response.sendRedirect("/index.html");
l'indirizzo
relativo "index.html" verrà trasformato
in indirizzo assoluto del tipo "http://server:port/index.html"
e non http://server:port/contextname/index.html. Chi
volesse ottenere il risultato opposto potrà
utilizzare il metodo getContextName() preponendolo
al nome dell'URI utilizzato.
Conclusioni
Questa nuova versione della servlet API appare piuttosto
interessante. Sicuramente le novità più
importanti sono quelle relative all'introduzione del
meccanismo dei filtri (di cui si è parlato
il mese scorso [1], e dei listener. Questi due innovativi
strumenti permetteranno un più agile e potente
meccanismo di gestione di applicazioni web basate
su tecnologia Servlet/JSP.
Per quanto riguarda invece le altre innovazioni, si
tratta di migliorie di minor conto, ma che nel complesso
mostrano come la API sia ormai giunta ad un buon livello
di maturità e stabilità; il tutto mostra
come Sun creda nella importanza di questo settore
della tecnologia Java (insieme a JSP, EJB, Java&XML),
e di fatto prosegua nella sua costante miglioramento
ed aggiornamento.
Una valutazione non di poco conto questa per chi volesse
investire in questa direzione o proseguire a farlo.
Bibliografia
[1] - "Servlet 2.3 API - I parte: i filtri"
di Giovanni Puliti, MokaByte 58, Dicembre 2001, www.mokabyte.it/2001/12/servlet23_1.htm
[2]
- Home page di Apache Tomcat: http://jakarta.apache.org
[3] "La gestione degli eventi con il JDK 1.1",
di Massimo Carli, MokaByte Numero 05 - Febbraio 1997,
www.mokabyte.it/05/jdk.htm
[4] "Pattern observer" di Lorenzo Bettini,
MokaByte Numero 26 - Gennaio 1999
http://www.mokabyte.it/1999/01/observer_teoria.htm
[5] "Una Fabbrica di oggetti" di Lorenzo
Bettini, MokaByte Numero 27 - Febbraio 1999
http://www.mokabyte.it/1999/02/factory_teoria.htm
|