MokaByte 55 - 7mbre  2001
Foto dell'autore non disponibile
di
Giuseppe Materni
Sherpa
Un servlet container fatto in casa
La tecnologia servlet si è ormai affermata come lo standard delle applicazioni Java per il Web, ed esiste un’ampia offerta di servlet container commerciali ed open source. Presentare un’implementazione completa di un servlet container può aiutare comprenderne più a fondo la logica e quindi a sfruttare al meglio la tecnologia servlet. 
Limitandosi ad affrontare l’argomento solamente dal punto di vista dello sviluppo delle servlet alcuni aspetti sono probabilmente destinati a restare in ombra. Ho chiamato il prodotto Sherpa, perché e parco nell’uso delle risorse e, almeno nelle speranze, affidabile. 


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

 

Biografia
laureato in matematica nel 1975 all’Università’ di Roma. Per molti anni ha insegnato informatica nelle scuole superiori interessandosi soprattutto di questioni teorico didattiche. 
Nel 1992 si è  completamente dedicato alla libera professione. Appasionato di  Smaltalk  ed ha  lavorato in C (soprattutto per fare CGI agli albori di internet). 
Ha iniziata ad utilizzare Java sviluppando programmi di “contorno” a gestionali che giravano in ambiente UNIX ed AS/400 (gestione ordini, interrogazione varie, ..) utilizzando un’architettura client-server. 
Da circa tre anni si dedica completamente allo sviluppo di applicativi per internet.

Può essere contattato all'indirizzo materni@isa.it
 

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java®, Jini®  e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
info@mokabyte.it