MokaByte 58 - Dicembre 2001 
Servlet API 2.3
I Parte: i filtri
di
 
Giovanni
Puliti
 
Con il rilascio della specifica 2.3 della Serlvet API sono state aggiunte alcune importanti caratteristiche oltre alla messa a punto di quanto già introdotto con la 2.2 In questo articolo parleremo di Filtri

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").


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