Introduzione
Da quando Sun nel lontano 1998 iniziò a parlare
di piattaforma Java 2, tutto quello che ruota intorno
a questo linguaggio ha preso una strada nuova rispetto
al passato. Una delle novità più interessanti
è sicuramente quella legata alla diversificazione
fra JDK core e Standard Extensions: in quest'ottica
se da un lato il nocciolo della JVM e delle API di
base si sono evolute in modo pressoché indolore
per gli sviluppatori (evitando modifiche o stravolgimenti
tali da compromettere la compatibilità all'indietro),
parallelamente si è potuto assistere alla evoluzione
di una serie di tecnologie parallele che pur essendo
compatibili con il nocciolo del JDK, ne estendendono
in modo molto potente le funzionalità di base.
Questa mossa ha reso di fatto tutto il mondo Java
più modulare, semplice da aggiornare e molto
più stabile.
Uno degli esempi più lampanti di questa organizzazione
è offerto dalla Servlet API, giunta ormai alla
versione 2.3: se da un lato è necessario e
sufficiente un motore di servlet (servlet engine)
compatibile con lo standard Java 2 (JDK 1.2 o superiori),
si può utilizzare la versione più recente
della API semplicemente rimpiazzando un file .jar
e passare così da una servlet API reference
implementation ad un'altra.
Questo meccanismo se risulta essere molto utile per
quegli ambienti di sviluppo come JBuilder o Visual
Age che consentono di sviluppare e debuggare Web Application
al loro interno, è ancora più importante
nell'ambito degli application server (o più
propriamente Servlet/JSP engine).
Per
migliorare ulteriormente questa funzionalità,
con la versione 2.3 delle API sono state introdotte
alcune soluzioni volte a migliorare ulteriormente
la modularità sia dell'application server che
delle Web Application installate. Ad esempio adesso
ogni Web Application utilizza un classloader ed uno
spazio di indirizzamento separati rispetto a quelli
del container. In questo modo ad esempio si può
evitare conflitti fra il parser XML utilizzato da
Tomcat per interfacciarsi con i vari file di configurazione
XML e quelli eventualmente utilizzati dalle Web Application.
Questa tecnica, pur non essendo ancora perfettamente
affinata, e di tanto in tanto si rischia di incorrere
in qualche intoppo, sicuramente la strada intrapresa
è quella giusta, e non dovrebbe essere necessario
più di qualche semestre o anno per rendere
l'architettura complessiva perfettamente funzionante.
Anche se ancora alcuni piccoli dettagli sono da chiarire,
il tutto mette in luce il buon livello di maturità
raggiunto dalla Servlet API.
Modifiche
alla servlet API 2.3
Per quanto riguarda il mondo delle Web Application
(Servlet e JSP), ogni volta che Sun rilascia una nuova
specifica è necessario un po' di tempo affinché
i produttori dei vari application server possano mettere
a punto un ambiente di esecuzione che supporti le
nuove funzionalità. Da questo punto di vista
una dei prodotti più interessanti, meglio riusciti
ed aggiornati è sicuramente Tomcat, giunto
ormai alla versione 4.0.1, scaturito dalla collaborazione
fra Sun e Apache Org. Questo application server (che
funziona come HTTP server, Servlet/JSP Engine) è
in grado di supportare al momento la versione 2.3
della Servlet API e la 1.2 di JSP.
Tanto per comprendere la bontà di questo progetto,
basti pensare che da un lato è adottato da
Sun ormai come standard di riferimento, ma al contempo
è utilizzato all'interno di application server
commerciali e tool di sviluppo, come base per permettere
l'esecuzione di web application Java.
Tomcat al momento è stato il primo (ed anche
se ancora per poco l'unico) application server in
grado di supportare la versione 2.3 delle servlet
API, oltre ad offrire la compatibilità per
la versione 1.2 di JSP. Il suo rilascio qualche settimana
fa ci ha permesso infine di iniziare a fare un po'
di prove con le nuove caratteristiche introdotte di
recente da Sun con la nuova specifica. Ecco un breve
elenco delle innovazioni apportate alla servlet API
2.3
-
E' stato ufficialmente definito il JDK 1.2 o successivi
come la piattaforma di base per l'esecuzione di
servlet
- Introduzione
del concetto di filtro
- Nuovo
ciclo di vita dei servlet (più in linea con
quanto avviene ad esempio con EJB) e contemporanea
introduzione di nuovi eventi legati ai vari passaggi
si stato o alterazioni varie.
- Nuovo
supporto per l'internazionalizzazione
- Formalizzazione
del meccanismo di dipendenza fra i vari file jar
- Formalizzazione
del meccanismo di class loading
- Nuovi
attributi per la gestione degli errori e della sicurezza
- E'
stata sostituita la classe HTTPUtils
- Aggiunta
di numerosi metodi alle varie classi
- Migliore
definizione dell'interfacciamento coni
vari DTD XML
In
questo articolo concentreremo l'attenzione sul nuovo
meccanismo dei filtri, mentre il mese prossimo parleremo
del nuovo ciclo di vita e gestione degli eventi annessi,
cose queste che sicuramente rappresentano le novità
più interessanti.
Pre-postprocessamento
di HTTP
Da sempre una delle caratteristiche più interessanti
della tecnologia delle web application è quella
che consente di mettere in comunicazioni risorse differenti:
si pensi ad esempio alla possibilità di effettuare
forward da una risorsa dinamica ad un'altra, alla
capacità di condividere oggetti di contesto
fra una pagina JSP ed un Servlet (e viceversa) o alla
creazione di catene di servlet in esecuzione in cascata.
Il panorama è quanto mai variegato ed in continua
evoluzione: le possibilità sono praticamente
illimitate, se si pensa alla possibilità di
mescolare le normali funzionalità che un web
server mette a disposizione (alias, url dinamici,
reindirizzamenti) con le varie tecniche utilizzabili
all'interno di una web application.
Con la nuova Servlet API 2.3, con l'introduzione del
concetto di filtro, un nuovo strumento si aggiunge
alle già note tecniche.
Tramite l'implementazione della interfaccia javax.servlet.Filter
ed effettuando una operazione di deploy nel modo corretto
il programmatore adesso è in grado di creare
oggetti che filtrino le invocazioni da parte del client
HTTP, verso una risorsa dinamica (Servlet o JSP se
si considera il mondo Java), ma anche statica (pagine
HTML o altro ancora).
Un filtro inoltre potrà modificare la risposta
inviata al client prima che questa sia effettivamente
inviata.
Limitando per semplicità lo scenario al solo
caso della interazione browser+servlet, un filtro
potrà ad esempio andare in esecuzione prima
che il metodo service() del servlet sia invocato (potendo
intervenire sull' HTTPServletRequest), o viceversa
effettuando opportune modifiche sugli header o sui
dati una volta che il servlet abbia terminato l'esecuzione
di tale metodo (agendo in questo caso su HTTPServletResponse).
Prima di vedere come questo sia possibile vediamo
qualche semplice esempio che faccia comprendere in
quali casi tutto questo possa essere utile.
Formalmente un filtro si definisce come un preprocessore
di una request http prima che questa raggiunga il
servlet, e post processore della risposta prima che
questa venga inviata al client.
Più precisamente un filtro può effettuare
le seguenti operazioni
- Intercettare
una chiamata ad un servlet prima che questo sia
effettivamente eseguito
- Esminare
una richiesta prima del suo inoltro al servlet
- Modificare
la sezione degli header dei vari pacchetti HTTP,
prima del loro inoltro al servlet
- Modificare
la sezione degli header dei vari pacchetti HTTP,
prima della spedizione al client
- Intercettare
una chiamata di un servlet dopo che tale servlet
sia stato invocato
La
prima cosa che viene in mente è realizzare
un filtro che controlli tutto il traffico diretto
verso i vari servlet installati su un server ed effettui
un trace del flusso dati (magari con relativo log
su file).
Parallelamente si potrebbe immaginare il caso in cui
tali dati siano effettivamente modificati prima di
essere inviati al servlet o al client. Fra gli esempi
presenti nella distribuzione 4.0.1 di Tomcat, vi è
un filtro che permette effettuare una compressione
zip/gzip dei dati se il flusso supera una determinata
soglia.
Un altro caso interessante potrebbe essere quello
di introdurre sistemi di reindirizzamento in modo
dinamico da e verso tutte le chiamate verso determinati
servlet. Si pensi ad esempio al caso del MokaBook,
il libro su Java che ormai da qualche tempo è
rilasciato gratuitamente su registrazione sul nostro
sito. Un filtro potrebbe controllare la presenza di
un ipotetico oggetto LoginUser (la cui presenza corrisponderebbe
ad una corretta procedura di login) nel contesto di
esecuzione del servlet che poi effettuerà il
download del file vero e proprio.
Fra i molti esempi presenti nella Java Developer Community
(JDC) di Sun vi è un esempio interessante di
utilizzo pratico di un filtro. Si immagini che molte
delle applicazioni in funzione sul nostro server debbano
collegarsi più o meno saltuariamente verso
un altro sito, del quale non si abbia nessun controllo,
ad esempio per effettuare delle interrogazioni di
un database. Nel caso in cui il secondo sita sia irraggiungibile
per motivi non dipendenti dalla nostra volontà,
si dovrebbe procedere alla modifica di un gran numero
di link, di Servlet o pagine JSP per poter avvisare
gli utenti finali della momentanea inutilizzabilità
del servizio.
Un semplice filtro potrebbe effettuare questo lavoro
per noi, inviando un messaggio all'utente (o effettuando
un redirect su una pagina di errore appositamente
creata), ma anche evitando che i vari servlet tentino
di collegarsi al sito bloccato.
Questi sono semplicemente due dei possibili utilizzi
che si possono mettere in piedi grazie ai filtri,
e come ho avuto modo di accennare in precedenza sicuramente
le possibilità sono praticamente illimitate.
L'obiezione che potrebbe sorgere spontanea in questo
caso è che tutto ciò poteva essere realizzato
anche prima dell'introduzione dei servlet grazie a
cose del tipo "servlet chaining" o reindirizzamenti
vari. In effetti questo è vero anche se l'utilizzo
dei filtri rende le cose molto più semplici,
eleganti e soprattutto simili con il resto della logica
delle Web Applications di J2EE.
I
filtri
Per poter realizzare un filtro la prima cosa che è
necessario fare è implementare l'interfaccia
javax.servlet.Filter che definisce i seguenti tre
metodi
void
init(FilterConfig config)
FilterConfig getFilterConfig ()
void doFilter(ServletRequest request, ServletResponse
response,
FilterChain chain)
Il
primo esegue la inizializzazione del filtro, mentre
il metodo getFilterConfig() invece serve per poter
ricavare dall'esterno l'oggetto di configurazione
del filtro. All'interno di doFilter() verranno infine
eseguite le operazioni di filtro. L'interfaccia FilterConfig
mette a disposizione alcuni metodi che consentono
fra l'altro di ottenere il nome del filtro, i parametri
di inizializzazione, prelevando tal valori direttamente
dal file XML di configurazione, in modo analogo al
caso dei servlet standard. In modo analogo ai metodi
doGet() doPost() e service() dei Servlet, il metodo
doFilter() riceve i parametri corrispondenti alla
invocazione da parte del client ed alla risposta da
inviare nel canale http. In particolare gli oggetti
ServletRequest e ServletResponse hanno lo stesso significato
che nel caso dei Servlet.
E' possibile definire una catena di filtri in grado
di effettuare le loro modifiche in sequenza per la
stessa invocazione/risposta: l'oggetto FilterChain
rappresenta proprio tale catena. Per fare questo dentro
il metodo doFilter() si potrebbe scrivere
chain.doFilter(request,
response)
Se
invece non si passasse il controllo al filtro successivo,
la chiamata verrebbe interrotta e non innoltrata al
servlet o al client (cosa utile questa ad esempio
per implementare tecniche di isolamento delle comunicazioni).
Ecco un semplice esempio che mostra come calcolare
il tempo di esecuzione di una ipotetica risorsa sul
server per ogni invocazione da parte di un client.
public
class TimeFilter implements Filter{
FilterConfig config;
ServletContext context;
//
esegue l'inizializzazione del filtro
public void init(FilterConfig config){
this.config = config;
context=config.getServletContext()
;
}
public
FilterConfig getFilterConfig(){
return this.config;
}
public
void doFilter(ServletRequest request, ServletResponse
response,FilterChain
chain)
throws
IOException, ServletException {
long
InvocationTime=System.currentTimeMillis();
chain.doFilter(request, response);
long EndExecutionTime=System.currentTimeMillis();
String Message;
Message="Tempo di esecuzione
della richiesta: ";
Message+=""+(EndExecutionTime-InvocationTime);
context.log(Message);
}
}
In
questo semplice esempio il metodo doFilter()
verrà invocato in occasione di chiamate da
parte di un client, ed andrà a scrivere su
file di log il tempo necessario per effettuare l'invocazione.
Come si può notare la cosa interessante è
che il passaggio all'elemento successivo nella catena
è effettuato in mezzo alla chiamata di doFilter():
in questo modo il controllo ritornerà al filtro
chiamante, dando vita ad un vero e proprio annidamento
delle chiamate.
Wrapping delle comunicazioni
Un'altra interessante caratteristica offerta con la
nuova API è la possibilità di modificare
le risposte e le invocazioni inglobando gli oggetti
ServletRequest
e ServletResponse
in appositi wrapper al fine di personalizzarne il
contenuto. Gli oggetti
WrapperServletRequest
e WrapperServletResponse
sono stati introdotti proprio a questo scopo. Per
comprenderne il funzionamento si potrebbe prendere
spunto da uno degli esempi presenti in Tomcat 4. In
questo caso un filtro intercetta le chiamate e le
risposte ed effettua una compressione dei dati se
il pacchetto da trasferire complessivamente supera
una determinata dimensione. Ecco un breve estratto
del metodo doFilter()
public
void doFilter(ServletRequest req, ServletResponse
res,
FilterChain
chain)
throws
IOException, ServletException {
// esegue una serie di controlli
per verificare se
// il
client supporta la compressione dei dati
. . .
. . .
//
adesso nel caso in cui il traffico sia in risposta
// effettua la compressione
dei dati
if (res instanceof HttpServletResponse)
{
//
si utilizza un Wrapper di risposta personalizzato
//
che
permette la compressione dei dati
CompressionServletResponseWrapper
wrRes;
wrRes
= new CompressionServletResponseWrapper((HttpServletResponse)res);
//
imposta la soglia oltre la quale i pacchetti verranno
//
compressi
wrRes.setCompressionThreshold(compressionThreshold);
try
{
//
inoltra la chiamata utilizzando il
//
wrapper
di risposta
chain.doFilter(request,
wrappedResponse);
}
finally
{
wrappedResponse.finishResponse();
}
return;
}// fine metodo doFilter()
}//
fine classe
Si
noti come la risposta venga intercettata e successivamente
reindirizzata verso gli altri filtri della catena,
provvedendo ad inglobare la risposta in un wrapper
apposito: in questo caso si tratta di un oggetto CompressionServletResponseWrapper
che provvede a fornire i metodi necessari per la compressione
dei dati. Non potendo riportare completamente il codice
di tale classe (sia per motivi di spazio che per imposizioni
contrattuali) se ne può brevemente analizzare
i punti più interessanti. Per prima cosa la
definizione della classe che deve estendere HttpServletResponseWrapper
public
class CompressionServletResponseWrapper extends HttpServletResponseWrapper
{
Il
costruttore riceve dall'esterno una generica HttpServletResponse
di cui effettuerà poi la funzione di wrapping
public
CompressionServletResponseWrapper(HttpServletResponse
response)
{
super(response);
origResponse
= response;
}
Il
metodo createOutputStream()
che crea uno stream di compressione, classe CompressionResponseStream
, sulla base della risposta originaria
public
ServletOutputStream createOutputStream() throws IOException
{
return
(new CompressionResponseStream(origResponse));
}
infine
uno dei metodi deputati alla scrittura delle informazioni
nel canale HTTP, si appoggia ai metodi di compressione
offerti dalla CompressionResponseStream
public
void flushBuffer() throws IOException {
((CompressionResponseStream)stream).flush();
}
Le
operazioni di compressione vere e proprie verranno
effettuate all'interno della classe CompressionResponseStream
che utilizzando alcune classi e metodi del package
java.util.zip Trasforma lo stream dei dati in pacchetti
compressi (si veda la bibliografia).
Installazione
e deploy dei filtri
Resta da vedere adesso come procedere nella fase di
installazione e deploy di un filtro. L'operazione
è molto semplice e segue fedelmente la procedura
da utilizzare per il deploy di una comune Web Application
basata su JSP o Servlet.
Per prima cosa si deve ricreare una struttura a directory
tipica delle Web Application (basata sulle directory
WEB-INF, classes, META-INF etc..) e spostare i vari
file .class nella posizione opportuna, tipicamente
sotto webapproot\WEB-INF\classes.
A questo punto del file web.xml (presente in WEB-INF)
si dovrà procedere alla definizione del filtro,
dei suoi parametri di inizializzazione e degli eventuali
alias URL. Ecco un esempio
<?xml
version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems,
Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>filteredservlet</servlet-name>
<servlet-class>com.mokabyte.servlet.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>filteredservlet</servlet-name>
<url-pattern>/filteredservlet</url-pattern>
</servlet-mapping>
<filter>
<filter-name>
logtime
</filter-name>
<filter-class>
ServletFilter
</filter-class>
<init-param>
<param-name>maxtime</param-name>
<param-value>212</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>logtime</filter-name>
<url-pattern>/servlet/FilteredSerlvet</url-pattern>
</filter-mapping>
</web-app>
Dopo aver definito un servlet ed averlo mappato con
nome opportuno, si procede alla installazione di un
filtro prima, e successivamente alla definizione del
cosiddetto filter-mapping: in questo modo si dice
al server http quando tale filtro dovrà essere
eseguito. In questo caso il controllo verrà
intercettato solo in occasione dell'invocazione del
servlet di prova. Se invece avessimo scritto
<filter-mapping>
<filter-name>logtime</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Il
filtro avrebbe filtrato le chiamate per tutte le risorse
mappate sotto la web root, ovvero anche file html
statici, pagine JSP, programmi CGI-BIN.
Sempre nel file XML appena visto si noti la sezione
<init-param>
<param-name>maxtime</param-name>
<param-value>212</param-value>
</init-param>
che
permette di passare dei parametri di inizializzazione
al filtro: tali parametri saranno recuperati all'interno
del metodo init() ad esempio
public
void init(FilterConfig filterConfig)throws ServletException
{
this.filterConfig = filterConfig;
this.attribute = filterConfig.getInitParameter("maxtime
");
}
Si noti infine nel file web.xml la presenza della
prima riga
<
. . . "http://java.sun.com/dtd/web-app_2_3.dtd">
necessaria
per permettere l'utilizzo dei nuovi tag <filter>
<filter-mapping> e simili non presenti nella
versione 2.2.
Mentre scrivo quest'articolo non ho ancora trovato
nessun riferimento ufficiale relativamente all'ordine
con cui i vari filtri sono eseguiti, anche se dalle
prove da me effettuate pare che sia seguito l'ordine
riportato nel file web.xml.
Conclusione
Con il rilascio della nuova versione della servlet
API Sun ha realizzato un importante passo avanti nel
processo di semplificazione e standardizzazione della
piattaforma J2EE.
Sicuramente l'aspetto più interessante è
la possibilità di utilizzare i filtri come
strumento di personalizzazione o modifica di intere
web application semplicemente tramite la modifica
di poche righe di codice XML. Chiunque abbia dovuto
manutere siti web di commercio elettronico di dimensioni
medio-elevate comprenderà la potenza di questo
sistema. Parallelamente l'introduzione di un sistema
di chiamate in callback relativamente al ciclo di
vita di un servlet (di cui parleremo nel mese prossimo)
offre grosse potenzialità relativamente all'inserimento
di web application nella sfera di applicazioni distribuite,
magari basate su EJB, ed utilizzando sistemi di messaggistica
asincrona come JTA/JTS.
Personalmente credo che la vera potenza di tutto questo
non risieda tanto nella presenza di questi nuovi strumenti,
che fino ad oggi potevano essere sostituiti con tramite
tecniche tradizionali, ma piuttosto nella grande semplicità
con cui adesso sia possibile realizzare lo stesso
tipo di servizio.
E' bene infatti tenere presente che a causa della
notevole crescita della piattaforma Java, il successo
di una tecnologia a discapito di altre sta nella semplicità
con cui questa potrà inserirsi nello scenario
già presente magari aumentandone le prestazioni,
potenzialità ma anche e soprattutto riducendone
la complessità.
I pezzi di codice riportati in questo articolo sono
stati in parte presi dagli esempi presenti in Tomcat
4.0 ("The Apache Software License, Version 1.1
Copyright (c) 1999 The Apache Software Foundation.
All rights reserved").
|