Introduzione
Per
lo sviluppo delle applicazioni per il Web sono disponibili molte di soluzioni:
si va dalle CGI (Common Gateway Interface), che possono essere realizzate
in vari linguaggi (C, Perl, …) per arrivare alle servlet, le JSP (JavaServer
Pages) e le ASP (Active Server Pages). Attualmente, volendo limitarci al
mondo Java, lo standard è rappresentato dalle servlet e dalle JSP.
Queste tecniche offrono Il grosso vantaggio di sfruttare le capacità
multi thread della JVM (Java Visual Machine): le request HTTP possono
essere gestite in thread separati senza la necessità di far partire
un processo ad ogni chiamata, come avviene per le CGI.
Un
aspetto piuttosto noioso nella creazione di siti dinamici ed interattivi
è rappresentato dalla gestione di una sessione dove memorizzare
gli stati fra una pagina e l’altra. Il protocollo HTTP non prevede la gestione
degli stati. Ogni coppia request-response rappresenta una singola transazione;
per utilizzare informazioni acquisite in momenti precedenti (come nel caso
di una login iniziale) bisogna ricorrere a trucchi che coinvolgono la programmazione
dal lato client. Con le servlet tutto questo è un ricordo; il lavoro
sporco lo fa il container (di solito utilizzando i cookies) mentre lo sviluppatore
trova a sua disposizione delle comode API per la gestione della sessione.
Le
servlet per funzionare necessitano di una libreria servlet.jar, liberamente
prelevabile presso la SUN, e di un servlet container. Esiste un’ampia offerta
di container sia commerciali che open source, alcuni dei quali completamente
realizzati in Java. Quelli commerciali sono piuttosto sofisticati e tendono
a essere un proprio e vero ambiente di sviluppo, quelli open source si
sono molto evoluti e sono ormai maturi ed affidabili.
Eppure
vi sono i presupposti per valutare la possibilità di sviluppare
un servlet container in casa.
Le
API della SUN sono ben documentate e dettagliate, ed i package java.servlet
e java.servlet.http forniscono, attraverso le interfacce e le classi astratte,
un disegno completo e vincolante. In ultima analisi si tratta di implementare
le interfacce sottoclassare le classi astratte e realizzare alcune classi
di supporto per realizzare tutte le funzionalità indicate.
E’
un’esperienza molto interessante, almeno lavorando con un progetto ben
disegnato come quello della SUN; si comprendono a fondo i meccanismi che
legano i vari oggetti e ci si sforza di lavorare assecondando ed interpretando
le scelte dei progettisti.
Alla
fine si ottiene un prodotto di cui si conosce tutto. La scatola nera sarà
anche bella cosa, ma poter comprendere a fondo il codice che si utilizza
da un maggior senso di sicurezza. Un conto è avere accesso al codice
di un prodotto open source, un conto essere partecipi delle varie scelte.
Conoscere un progetto che non sia proprio banale costa molta fatica; non
è sufficiente avere accesso al codice ed alla documentazione; servirebbe
un qualcosa che illustrasse i punti fondamentali, i perché delle
scelte e l’organizzazione interna del progetto, tali informazioni non sempre
sono disponibili.
Se
il progetto non è troppo complesso può quindi valere la pena
di tentare l’impresa.
Si
assiste alla tendenza verso il gigantismo del software: i prodotti sono
sempre più complessi ed ingordi di risorse. Probabilmente è
una conseguenza delle esigenze del Web e di una certa tendenza “server
centrica”. Sta di fatto che gli ambienti si fanno sempre più ingombranti
e complessi, e sempre più numerosi sono i prodotti coinvolti nello
sviluppo e nel funzionamento di un’applicazione.
Inoltre
la necessità di rendere le cose facili per gli utilizzatori è
paradossalmente un’ulteriore spinta verso il gigantismo. I tools visivi,
la produzione automatica del codice e tutte le altre diavolerie proposte
per rendere la vita facile ai programmatori si trasformano in tante funzionalità
aggiuntive da implementare e quindi portano a maggiore complessità
e consumo di risorse.
Se
per i manufatti tradizionali valeva la massima: quello che non c’è
non si può rompere. Per il software si potrebbe dire: quello che
non c’è non consuma risorse. E’ vero che come tutte le massime ad
effetto proclama una verità molto approssimata. Un buffer nella
gestione dell’IO, un pool nella gestione di una connessione sono senza
dubbio delle complicazioni; eppure permettono di migliorare l’utilizzazione
delle risorse. Però è vero, soprattutto parlando di risorse
in senso generale, che un prodotto snello ed agile offre migliori risultati.
Se fra le risorse consideriamo anche la capacità umana di controllo
ciò è ancora più vero.
Avendo
a disposizione delle specifiche da rispettare e volendo implementarle con
il minimo sforzo si può tentare d’individuare un sotto insieme delle
stesse che garantisca gli obbiettivi che si vogliono perseguire e permetta
di semplificare il progetto. In questo modo non si tradisce lo standard:
i prodotti sviluppati usando un insieme ridotto di risorse saranno senz’altro
compatibili con gli strumenti che implementano tutte le funzionalità.
In altri termini per non correre pericoli bisogna rispettare scrupolosamente
il progetto senza mai forzare il comportamento di una specifica e senza
aggiungere funzionalità non previste, per quanto si possa essere
sicuri della loro utilità.
Per
sobbarcarsi la fatica di realizzare un servlet container sono necessarie
anche delle motivazioni pratiche. Nel mio caso la motivazione decisiva
è venuta dalla necessità di realizzare un applicativo che
girasse anche in modalità standalone su portatili non particolarmente
dotati, che potesse fare a meno del Web Server, che fosse estremamente
semplice da installare, che non costasse nulla in termini di licenze, che
fosse compatibile con tute le JVM e infine che fosse affidabile.
Qualche rinuncia
e tutto si semplifica
Un
attento esame delle Servlet API 2.1 mi ha permesso d’individuare alcune
funzionalità, non necessaire alle mie esigenze, la cui eliminazione
ha permesso di semplificare molto il progetto.
Il
ciclo di vita delle servlet prevede:
-
Creazione
della servlet ed inizializzazione mediante il metodo init().
-
Gestione
della/e request-response con i metodi service(), doGet(), doPost().
-
Distruzione
della servlet mediante il metodo destroy().
Il ciclo
è abbastanza semplice ma presenta alcuni punti critici: istanziare
ogni volta un oggetto è un’operazione piuttosto onerosa. Tenerlo
in vita per una sola chiamata sembra uno spreco; ma implementare un meccanismo
per cercare di sfruttarlo per più chiamate significa introdurre
una complicazione i cui benefici sono difficilmente calcolabili.
La
soluzione più semplice è quella di crearlo ed inizializzarlo
al momento del lancio del container e di non distruggerlo mai. Le conseguenze
di tale scelta non sono drammatiche: il metodo init(ServletConfig config)
serve solo a rendere disponibili i parametri iniziali del servlet e il
contesto del container; tutte informazioni definite nel file di deployement
web.xml e quindi costanti.
L’unica
vera limitazione consiste nel dover rinunciare all’uso dell’interfaccia
SingleThreadMode. Utilizzare tale interfaccia equivale in pratica
ad dichiarare il metodo service() come synchronized.
Essendo
disponibile una sola istanza per ogni servlet utilizzare tale modalità
significa esporsi al rischio di gravi ritardi nel caso di un’elaborazione
complessa, come per esempio nel caso di una query particolarmente onerosa
da un DB. Non mi sembra una grossa rinuncia; per non correre rischi è
sufficiente fare attenzione all’uso delle variabili, tenendo conto che
le servlet lavorano in multi tasking. In altri termini le variabili, che
non siano quelle d’inizializzazione o delle costanti, devono essere dichiarate
all’interno del metodo service().
In
Sherpa la gestione delle servlet è affidata alla classe XservletMgr
che utilizza una Hastable per memorizzare le istanze di tutte le
servlet. E’ una piccola complicazione rispetto alla possibilità
di creare un’istanza per ogni chiamata HTTP, ma è uno di quei casi
in cui una piccola deviazione dall’indirizzo generale (perseguire la massima
semplicità) è giustificabile per i vantaggi che ne conseguono.
La
scelta più significativa ed importante ai fini di mantenere una
struttura semplice è stata la rinuncia alla possibilità di
aggiungere o modificare una servlet a runtime. In altri termini ogni volta
che si aggiunge, si modifica o si elimina una servlet o più semplicemente
si modifica il file web.xml bisogna rilanciare Sherpa.
Se
un servlet container deve supportare più applicativi a cui lavorano
molti sviluppatori, magari collegati in remoto, e deve garantire la continuità
di funzionamento doverlo rilanciare ad ogni modifica diventa una limitazione
inaccettabile. Ma in un piccolo progetto e con una JVM dedicata è
una situazione accettabile.
Un’altra
conseguenza fastidiosa si ha quando si testa e si sviluppa una servlet
utilizzata in uno stato del progetto che presuppone, per il suo funzionamento,
di essere passati attraverso stati precedenti. Ad esempio se è prevista
una procedura iniziale di autentificazione ed un controllo per ogni servlet,
per eseguire i test è necessario ogni volta ripassare attraverso
la fase iniziale. Questo fra l’altro complica l’implementazione di test
automatici.
Per
contro questa situazione invita a modulare il disegno e a ben definire
l’interfaccia di ogni servlet.
Permettere
di caricare o modificare una servlet senza rilanciare il container
comporta l’implementazione di un sofisticato meccanismo di caricamento
delle classi. In ogni caso resta aperto il problema delle classi di supporto:
variando una classe utilizzata da una o più servlet bisognerebbe
in qualche modo adeguare “al volo” i comportamenti delle servlet e le sessioni
attive; un’impresa davvero difficile.
Figura
1 - Schematizzazione concettuale del ciclo Request – Response.
In
realtà i filtri possono costituire una lista di n elementi, diversa
per ogni servlet.
La
chiamata del metodo service() avviene all’interno del metodo doChain()
dell’ultimo
filtro della lista.
Fare a meno del
Web Server
Vi
sono servlet container disponibili come estensioni di un Web Server (Tomcat,
iPlanet, Zeus, Caucho, Jigasw, ...)altri sono estensioni di Application
Server (WebLogic, Orion, WebSphera, ...) infine vi sono quelli da aggiungere
ad un Web Server (ServletExec, Jrun, Jserv, ...). In tutti i casi la request
HTTP viene inviata al server che poi in qualche modo la passa al gestore
delle servlet. In questo modo tutte le request di pagine statiche possono
normalmente essere eseguite dal WebServer mentre i servlet si limitano
a fornire le pagine dinamiche. Inoltre l’uso delle servlet risulta, in
modo trasparente, come un’estensione del server.
Implementando
delle servlet per l’invio di pagine statiche, d’immagini e applets si può
utilizzare in maniera completamente autonoma un servlet container. E’ un
ulteriore fattore di semplicità sia nell’implementazione che nell’uso
e sia ha una completa libertà nella scelta della JVM da utilizzare.
Infine su di una stesso computer si possono far girare più container,
ognuno nella sua JVM, senza rischi di conflitto. Negli esempi sono
proposti un paio di servlet adatti allo scopo.
Fare a meno delle
JSP
Le
JSP (JavaServer Pages) hanno avuto un grande successo e in alcuni articoli
sono state presentate come la soluzione ottimale per realizzare la divisione
fra logica dell’applicazione e la visualizzazione dei dati.
Non
tutti sono d’accordo, in ultima analisi sono un sistema alternativo per
scrivere delle servlet inserendo il codice direttamente nella pagina HTML
mediante degli scriptlet o richiamando,sempre da HTML, dei files jsp. I
ogni caso l’elaborazione avviene dal lato server (tanto è vero che
viene automaticamente prodotta una servlet al momento della prima compilazione)
e quindi rispetto alle servlet è solo un modo diverso per scrivere
del codice Java.
Utilizzare
libreria di tag (WebMacro, Velocity, ...) mi sembra una soluzione più
lineare ed in ultima analisi più portabile.
Ho
il sospetto che il successo delle JSP (così come delle ASP) derivi
dalla possibilità che offrono di inserire il codice dal lato client.
In altri termini un provider può facilmente mettere nella sua offerta
la disponibilità di JSP o ASP, sarebbe molto più problematico
offrire la possibilità di sviluppare delle servlet.
Per
la loro stessa natura le JSP invitano di fatto a progettare cominciando
da “ciò che si vede”. E’ la via migliore per restare impigliati
nei capricci del committente e per realizzare un progetto con il fascino
dei quartieri medioevali.
L’uso
del browser, pur con tutte le limitazione di HTML di JavaScript, favorisce
una nette separazione fra logica e presentazione, non vi è motivo
per rinunciarvi.
Organizzazione
dei package.
sherpa
sherpa/XCommands.java
sherpa/XConsole.java
sherpa/XEnv.java
sherpa/XMain.java
sherpa\config
sherpa\config\XContextLoader.java
sherpa\config\XFilterLoader.java
sherpa\config\XListenerLoader.java
sherpa\config\XLoaderMgr.java
sherpa\config\XServletLoader.java
sherpa\servlet
sherpa\servlet\XFilterChain.java
sherpa\servlet\XFilterConfig.java
sherpa\servlet\XRequestDispatcher.java
sherpa\servlet\XServletConfig.java
sherpa\servlet\XServletContext.java
sherpa\servlet\XServletInputStream.java
sherpa\servlet\XServletOutputStream.java
sherpa\servlet\http
sherpa\servlet\http\XHttpConstants.java
sherpa\servlet\http\XHttpServletRequest.java
sherpa\servlet\http\XHttpServletResponse.java
sherpa\servlet\http\XHttpSession.java
sherpa\support
sherpa\support\XCookieMgr.java
sherpa\support\XErrorThread.java
sherpa\support\XEventContainer.java
sherpa\support\XEventMgr.java
sherpa\support\XEventQueue.java
sherpa\support\XFilterDef.java
sherpa\support\XFilterMapServlet.java
sherpa\support\XFilterMgr.java
sherpa\support\XListenerMgr.java
sherpa\support\XQueryString.java
sherpa\support\XSecureSocketManager.java
sherpa\support\XServletDef.java
sherpa\support\XServletMgr.java
sherpa\support\XSessionControl.java
sherpa\support\XSessionMgr.java
sherpa\support\XSocketMgr.java
sherpa\support\XSocketThread.java
sherpa\support\XSocketThreads.java
sherpa\tools
sherpa\tools\XInfo.java
sherpa\tools\XSessionUtils.java
sherpa\tools\XUtils.java
sherpa\xml
sherpa\xml\XElement.java
sherpa\xml\XElementMgr.java
Nel
package sherpa vi è la classe (XMain) che lancia il server
e la classe (XEnv) che gestire le classi deputate alla configurazione ed
al lancio dei thread.
In
sherpa.config vi sono le classi che inizializzano le variabili di ServletContext,
istanziano le servlet, caricano le classi dei filtri e dei listener
e infine lanciano i thread delle connessioni.
In
sherpa.servlet vi sono le implementazioni delle interfacce e delle classi
astratte che nel servlet.jar della SUN sono in definite in javax.servlet
La
stessa cosa è in sherpa.servlet.http. Quando un’interfaccia
di javax.servlet.http è un’estensione di un’interfaccia di javax.servlet
(come il caso di javax.servlet.ServletRequest e javax.servlet.http.HttpServletRequest)
è stata implementata solamente l’interfaccia estensione (comprendendo
ovviamente i metodi di quella sottoclassata).
Sono
i package che rendono disponibili le API.
Figura
2 - Diagramma dell’implementazione delle principali interfacce.
Vi
sono rappresentate le classi di gran lunga più importanti
per
gli sviluppatori delle servlet.
In
sherpa.support vi sono tutte le classi che, lavorando dietro alle quinte,
forniscono alle classi che implementano le API di funzionare. Il loro servizi
sono disponibili esclusivamente alle classi di Sherpa.
In
sherpa.tools vi sono classi che forniscono vari servizi di utilità.
Le
classi di sherpa.xml servono soltanto a schermare il parser utilizzato
per il file di deployement web.xml. Attualmente è stato utilizzato
exml della Electric. Ma sono sufficienti poche modifiche per utilizzare
Jdom o Xerces. In ogni caso tutte le classi che utilizzano un servizio
di parser vedono soltanto le due classi del package.
Architettura per
la gestione delle connessioni
La
gestione delle connessioni è uno degli aspetti più delicati
nella realizzazione di un server. Coerentemente con la ricerca della massima
semplicità si è scelto di lanciare tutti i thread delle connessioni
al momento dello startup e di gestirle mediante un semplice pool . Il numero
delle connessioni è definito nel file web.xml e non è ampliabile.
Si tratta di una scelta che può forse indurre qualche disagio psicologico
per il fatto che il numero delle connessioni è predefinito. Ma si
tratta di un limite apparente. Configurando correttamente il numero massimo
delle connessioni in funzione del traffico previsto si arriva ai limiti
delle risorse di sistema prima di esaurire le connessioni disponibili.
E d’altra parte abbondare un poco sul numero massimo di connessioni in
fase di configurazione porta ad uno spreco di risorse limitato. Per contro
il guadagno in efficienza è veramente notevole.
Figura
3 - Diagramma delle classi che costituiscono il nucleo
di
Sherpa
e la cui organizzazione è indicativa delle scelte riguardo
al
ciclo di vita delle servlet e la gestione delle connessioni.
Figura
4- Diagramma di sequenza dell’implementazione del ciclo di vita
delle
Servlet in Sherpa. Non compare la chiamata al metodo init() di HttpServlet
in
quanto questo metodo viene chiamata una sola volta al momento della creazione
delle servelet da parte di XServletLoader.
Questa
scelta comporta una limitazione nell’implementazione del metodo destroy().
Non
devono essere cancellate o modificate le variabili inizializzate nel metodo
init().
Il
frammento di codice riportato fa il parser del file web.xml, legge gli
alias e i nomi delle classi delle servlet da creare. Sono letti anche i
parametri iniziali utilizzati nella chiamata del metodo init(). Tutte le
istanze delle servelet sono memorizzate in una Hastable che poi viene utilizzata
da XServletMgr che funge da semplice pool per le istanze delle servlet.
sherpa.xml.XElement .
public
Hashtable load(XElement root){
String servletName = null;
String className
= null;
XServletDef servletDef = null;
HttpServlet httpServlet = null;
Hashtable initParams = null;
root.buildChildrenList("servlet");
while(root.hashNextChild()){
XElement srvl = root.nextChild();
servletName = srvl.getChildText("servlet-name");
className = srvl.getChildText("servlet-class");
initParams = new Hashtable();
srvl.buildChildrenList("init-param");
while(srvl.hashNextChild()){
XElement par = srvl.nextChild();
String parName = par.getChildText("param-name");
String parValue = par.getChildText("param-value");
initParams.put(parName,parValue);
}
servletDef = new XServletDef(servletName,className);
//istanza di HttpServlet
httpServlet = instanceOfHttpServlet(
servletName,className,initParams);
servletDef.settHttpServlet(httpServlet);
servlets.put(servletName,servletDef);
}
return servlets;
}
private HttpServlet instanceOfHttpServlet(String aliasName,
String className,
Hashtable initParams){
HttpServlet servlet = null;
try{
Class clazz = Class.forName(className);
//Creazione istanza di HttpServlet
servlet = (HttpServlet)clazz.newInstance();
XServletConfig config = new XServletConfig(aliasName,
XEnv.getServletContext(),
initParams);
//Chiamata (unica) del init() di HttpServlet
servlet.init(config);
}
................
............
return servlet;
}
Al
momento del lancio vengono lanciati N thread (quante sono le connessioni
massime previste), in ognuno vi è un’istanza di XSocketThread. La
classe implementa il ciclo principale per la gestione delle request e le
invocazioni dei filtri e delle servlet.
private
void startConnectionCycle() {
while(XEnv.CONTINUE) {
//attesa dalla connessione
String[] dataRqs=socketMgr.receiveRequest();
...
//inizializza HttpRequest
try {
request.init(response, dataRqs, socketMgr);
}
....
...
//inizializzazione HttpResponse
response.init(request, socketMgr.getOutputStream());
//estrazione dalla request del nome della servlet
String servletName = request.getServletName();
.....
//Istanza della servlet
HttpServlet httpServlet;
httpServlet = XServletMgr.getServlet(servletName);
......
//Se sono mappati dei filtri invoca il primo della lista
//altrimenti invoca il metodo service del servlet
try{
if(!XFilterMgr.invokeFilter(httpServlet,
request,response)){
httpServlet.service(request,response);
}
}
....
//chiusura connessione
socketMgr.closeConnection();
.....
httpServlet.destroy();
}
}
Implementazione
della classe ServletOutputStream
Si
tratta di una classe particolarmente importante ai fini delle prestazioni
perché gestisce l’output della response.
ServletOutputStream,
estensione di OutputStream, è una classe astratta per la quale è
richiesta l’implementazione del metodo OutputStream.write(int c).
La
documentazione dice letteralmente:
This
is an abstract class that the servlet container implements.
Subclasses
of this class must implement the java.io.OutputStream.write(int c) method.
Sembrerebbe
tutto semplice, eppure è l’unico caso in cui ho dovuto faticare
per rispettare le API. Il problema nasce dalla gestione degli header.
Le
API prevedono, nella classe HttpServletResponse, il metodo setHeader(String
name, String value) per settare il valore degli header che poi vengono
messi in testa alla response da inviare al client.
Sono
disponibili due metodi di HttpServletResponse per ottenere la classe per
gestire l’output: getWriter(), che restituisce PrintWriter, e getOutputStream()
che restituisce ServletOutputStream.
Tutte
le chiamate del metodo setHeader() devono precedere la prima chiamata in
scrittura (write, println, ...) della classe usata per l’output.
Visto
che un vincolo, derivante dalla logica sottostante, bisogna imporlo; a
mio parere tanto valeva imporre d’inviare esplicitamente gli header.
In
altri termini invece di:
response.setContentType("text/html");
...
out.println(“hello”);
si
potrebbe scrivere:
out.println("text/html");
...
out.println(“hello”);
Ma
la scelta dei progettisti è stata quella di perseguire in ogni caso
il massimo di trasparenza possibile. In questo modo la classe che implementa
ServletOutputStream deve farsi carico di scrivere gli header settati.
Siccome
il metodo write(int c) è quello di più basso livello è
su quello che bisogna intervenire.
Prima
di scrivere ogni carattere bisogna controllare se vi sono degli header
e, nel caso, se sono stati scritti. Una volta inviati bisogna settare un
flag per non inviarli più volte. Ho sperimentato molte soluzioni,
l’ultima che ho implementato, in occasione del rilascio delle API 2.2 che
prevedono la gestione di un buffer, è quella che mi lascia meno
insoddisfatto.
Soffre
in ogni caso di un piccolo difetto: uno dei trucchi per migliorare
le prestazioni dell’output consiste nello scrivere un carattere di spazio
appena chiamato il metodo service(), prima dell’elaborazione della response,
in modo di anticipare l’invio dei dati verso il client.
Con
la classe XServletOutputStream utilizzare tale tecnica sarebbe del tutto
inutile, il carattere verrebbe inviato solo dopo che buff è stato
riempito ed è stato quindi chiamato il metodo flushBuff().
public
class XServletOutputStream extends ServletOutputStream {
private int BUFF_DEFAULT= 8 * 1024;
private byte buff[];
private int count;
..............
public XServletOutputStream(OutputStream outputstream, XHttpServletResponse
res) {
.....
this.buff = new byte[BUFF_DEFAULT];
......
}
......
......
public synchronized void write(int i) throws IOException {
if(count >= buff.length) flushBuff();
buff[count++] = (byte)i;
}
public synchronized void flushBuff() throws IOException
{
if(response.headersEnabled){
response.headersEnabled=false;
byte[] bs = response.getHeadersText().getBytes();
out.write(bs, 0, bs.length);
}
if(count > 0) {
out.write(buff, 0, count);
count = 0;
}
}
.....
....
}
I Listener
I
Listener sono stati aggiunti nella release 2.3. Sono particolarmente utili
e coprono una grave lacuna. Permettono di ascoltare eventi che riguardano
l’ambiente del server (ServletContext) e le sessioni. La loro implementazione
non ha rappresentato un grosso problema soprattutto grazie alla tecnica
di registrare i listener utilizzando la loro interfaccia e l’operatore
istanceof. L’ispirazione è venuta dal pattern marker.
Le
interfacce definite nelle API sono:
interface
ServletContextListener extends EventListener
public void contextInitialized(ServletContextEvent sce );
public void contextDestroyed (ServletContextEvent sce );
interface
ServletContextAttributesListener extends EventListener
public void attributeAdded(ServletContextAttributeEvent scab);
public void attributeRemoved(ServletContextAttributeEvent scab);
public void attributeReplaced(ServletContextAttributeEvent scab);
interface
HttpSessionListener
public void sessionCreated(HttpSessionEvent se );
public void sessionDestroyed(HttpSessionEvent se );
interface
HttpSessionActivationListener
public void sessionWillPassivate(HttpSessionEvent se);
public void sessionDidActivate(HttpSessionEvent se);
interface
HttpSessionAttributesListener extends EventListener
public void attributeAdded(HttpSessionBindingEvent se );
public void attributeRemoved(HttpSessionBindingEvent se );
public void attributeReplaced(HttpSessionBindingEvent se );
Non
è state implementata HttpSessionActivationListener per
mancanza di motivazioni: non ho mai incontrato né sono riuscito
ad immaginare uno scenario in cui utilizzarla.
Per
ogni interfaccia è stata implementata una coda di segnali per evitare
dei ritardi indesiderati. Infatti l’invio di un segnale può attivare
un’elaborazione onerosa per il listener, e se il collegamento fra la classe
che invia il segnale e la classe registrata come listener è diretto,
può accadere che la classe sorgente del segnale debba attendere
fino a che la classe di destinazione non termini la sua elaborazione. Naturalmente
questo non è vero nel caso che la classe listener sia un thread;
ma non sempre ci si trova in questa situazione.
Per
risolvere il problema si può “disacoppiare” la classe sorgente
dalla classe destinazione frapponendo una coda FIFO che sia essa stessa
un thread. La classe sorgente notifica l’evento alla coda e continua immediatamente
la sua elaborazione. La coda attende la disponibilità della classe
ascoltatrice per notificargli l’evento.
Per
ServletContextListener ho dovuto modificare il disegno d’inoltro degli
eventi creando un collegamento diretto. Al lancio del server la prima operazione
è l’inizializzazione di ServletContext. Tutte le altre elaborazioni
devono attendere che sia terminata tale operazione, in quanto le variabili
memorizzate in ServletContext sono necessarie per portare a termine la
fase d’avviamento.
Vi
è poi una tipologia di listener, definita fin dalla release 2.0,
nella quale sono definiti come listener gli oggetti che sono destinati
ad essere gestititi nella sessione. In altri termini un oggetto, implementando
l’interfaccia HttpSessionBindingListener diventa in un certo senso ascoltatore
di sé stesso.
Esempio
di un oggetto da gestire in sessione:
public
class ObjectOfSession implements HttpSessionBindingListener{
public void valueBound(HttpSessionBindingEvent event){
System.out.println("HttpSessionBindingListener Object1 valueBound");
}
public void valueUnbound(HttpSessionBindingEvent event){
System.out.println("HttpSessionBindingListener Object1 valueUnbound");
}
}
In
XHttpSession l’evento viene notificato direttamente agli oggetti
che implementano l’interfaccia:
public
void putValue(String name, Object value) {
. . .
Object oldObj= objs.put(name, value);
. . .
if(value instanceof HttpSessionBindingListener )
((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this,
name));
}
public
void removeValue(String name) {
. . .
Object value = objs.remove(name);
. . .
if (value instanceof HttpSessionBindingListener)
((HttpSessionBindingListener)value).valueUnbound(new HttpSessionBindingEvent(this,name));
}
Anche
in questo caso sarebbe stato corretto utilizzare una coda di segnali, ma
in verità i listener che implementano l’interfaccia HttpSessionAttributesListener
soddisfano all’esigenza di monitorare l’attività della sessione.
Inoltre istanziare una coda, e qundi un thread, per ogni istanza degli
oggetti da gestire nelle sessione, rappresenta un discreto impegno di risorse.
In ogni caso si tratta di una modifica che si può realizzare senza
troppa fatica.
Figura
5 - Il diagramma schematizza la sequenza utilizzata per notificare
gli eventi. Il ruolo centrale è svolto da XEventMgr, nella quale
sono implementati solo dei metodi statici, che fa da “punto di raccolta
“ delle notifiche che vengono dai vari oggetti.
In
XListenerMgr sono registrate tutti Listener e quindi “ridistribuisce” le
notifiche ai destinatari utilizzando delle code che sono tutte delle sottoclassi
di XEventQueue.
Filtri
Ritengo
che i filtri rappresentano la novità più interessante della
release 2.3. Sono abbastanza semplici concettualmente un pò meno
da utilizzare. Con i filtri è possibile manipolare una request
prima di passarla ad una servlet ed una response prima d’inviarla al client.
Permettono quindi di modificare il comportamento di una servlet senza intervenire
sul suo codice. Anche se alla lontana ricordano il comportamento dei trigger
di un DB.
I
filtri sono dichiarati e mappati nel file web.xml. Vengono invocati nello
stesso ordine nel quale sono stati mappati.
<!--
filtri
-->
<filter>
<filter-name>filter1</filter-name>
<filter-class>samples.Filter1</filter-class>
<init-param>
<param-name>par1</param-name>
<param-value>val1</param-value>
</init-param>
</filter>
<filter>
<filter-name>filter2</filter-name>
<filter-class>samples.Filter2</filter-class>
</filter>
<!—
mappaturadei
filtri
-->
<filter-mapping>
<filter-name>filter1</filter-name>
<servlet-name>testfilter</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>filter2</filter-name>
<servlet-name>testfilter</servlet-name>
</filter-mapping>
Per
la servlet testfilter viene chiamato prima filter1 e poi filter2.
Secondo
le API è possibile mappare i filtri utilizzando l’URL o parte di
esso. Per esempio
<filter-mapping>
<filter-name>filter1</filter-name>
<servlet-name>/*</servlet-name>
</filter-mapping>
indica
che filter1 va applicato a tutte le servlet.
Questa
modalità di mappatura non è stata ancora implementata in
sherpa.
Figura
6 - Diagramma delle classi per la gestione dei filtri. Ogni istanza
di
XFilterMapServlet contiene la lista dei filtri da invocare per ogni servlet.
XFilterMgr
contiene il metodo invokeFilter(), chiamato all’interno del ciclo in
XSocketThread,
che mette in moto tutto il meccanismo.
E’
abbastanza facile modificare una request mentre è piuttosto complicato
manipolare una response. Infatti bisogna utilizzare una sottoclasse di
HttpServletResponseWrapper che implementa il pattern Decorator applicato
alla classe HttpServletResponse; inoltre bisogna sottoclassare ServletOutputStream
per intercettare il flusso dei dati. Il codice riportato è utilizzato
nell’esempio samples.Filter2 applicato alla servlet samples.TestFilterSrv.
public
void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException,
ServletException{
System.out.println("inizio filter2");
HttpServletResponse resp =(HttpServletResponse)response;
PrintWriter writerResp = resp.getWriter();
ResponseWrapper wrapper = new ResponseWrapper(resp);
//il metodo chiama un filtro successivo, se esiste,
//o la servlet a cui è
//stato applicato il filtro
chain.doFilter(request,wrapper);
//vengono estratti i dati della response originale
//che viene dalla servlet o da un altro filtro.
String txt = new String(wrapper.getData());
int p = txt.indexOf("Pagina");
if(p > -1){
txt=txt.substring(0,p)+
"<br><h1> Testo inserito dal Filtro2 </h1><br>"+
txt.substring(p);
}
//la response modificata viene inviata all’output originale
writerResp.print(txt);
writerResp.close();
System.out.println("fine filter2");
}
class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream byteOut;
byteOut = new ByteArrayOutputStream();
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
public byte[] getData() {
return byteOut.toByteArray();
}
public PrintWriter getWriter() throws IOException{
return new PrintWriter(new FilterOutputStream(byteOut));
}
}
class FilterOutputStream extends ServletOutputStream {
private DataOutputStream dataOut = null;
// il parametro del costruttore è ByteArrayOutputStream;
// una sottoclasse di
// OutputStream
public FilterOutputStream(OutputStream output) {
dataOut=new DataOutputStream(output);
}
//l’output viene reindirizzato nell’array di byte del wrapper
public void write(int b) throws IOException {
dataOut.write(b);
}
}
}
Figura
7
Ancora problemi
con ServletOutputStream
Nell’esempio
sul filtro per la manipolazione delle response vi è una sottile
questione riguardante ServletOutputSTream:
La
class FilterServletOutputStream implementa il metodo write(int c) per poter
reindirizzare l’output e quindi renderlo disponibile alla classe ResponseWrapper
sotto forma di un array di byte. Tale classe non implementa alcun meccanismo
per la gestione degli header. Quindi, dopo l’istruzione String txt=wrapper.getData(),
txt contiene tutta la response tranne gli header. Questi vengono poi regolarmente
inviati utilizzando PrintWriter (o ServletOutputStream) nell’istruzione
writerResp.print(txt).
L’oggetto
writerResp è fornito dalla response passata come parametro al metodo
doChain(). Tale response contiene l’output originale, quello della servlet
da filtrare, e quindi restituisce il ServletOutputStream (o PrintWriter)
che implementa il meccanismo di gestione degli header.
Non
so se si tratta di una scelta dei progettisti delle API o se è una
conseguenza di un’implementazione errata. In linea di massima mi sembra
una conseguenza inevitabile del disegno, in ogni caso è un comportamento
che è bene evidenziare. Il problema può essere risolto solo
trovando un altro modo d’intercettare l’output della servlet.
Conclusioni
In
quest’articolo ho tentato di analizzare “dietro alle quinte” le problematiche
coinvolte nell’implementazione di un servlet container. Alcune scelte sono
state meditate altre sono solo conseguenza della pigrizia. Una volto risolto
il nucleo di problema non sempre si trovano le motivazioni per completare
al meglio il lavoro in tutti i dettagli.
Il
prossimo appuntamento potrebbe essere riguardare qualche piccolo framework
per aiutare “sul campo” gli sviluppatori di servlet.
Link utili
http://java.sun.com/products/servlet/index.html
http://java.sun.com/products/servlet/Filters.html
http://www.servlets.com/index.tea
http://theserverside.com/home/index.jsp
http://archive.coreservlets.com/
http://www.nikos.com/javatoys/deep/servlets/
http://www.servletsource.com/
http://www.coolservlets.com/
http://www.do.org/ssj/
http://www.servletguru.com/
http://opensource.go.com/
http://www.webmacro.org/
http://jakarta.apache.org/
http://www.w3.org/Jigsaw/
http://www.caucho.com/
http://www.locomotive.org/locolink/disp?home
http://www.orionserver.com/
Bibliografia
Jason
Hunter “Java Servlet Programming”, Hops Libri 2001 (In italiano)
Jason
Hunter “Java Servlet Programming”, O’REILLY 2001 www.oreilly.com
Marty
Hall “Core Servlets and JavaServer Pages” Perentice Hall PTR 1999 http://archive.coreservlets.com/
Todd
Courtois “JAVA networking & comunications”, Perentice Hall PTR 1999
www.prenhall.com
Allegati
Tutto
il software presentato, Sherpa e gli esempi, è contenuto nel file.zip.
Un file readme.txt contiene le istruzioni per l’istallazione. L’unico prerequisito
è la disponibilità delle JVM, preferibilmente versioni successive
alla 1.2.
Scarica
lo zip
|