MokaByte 59 - Gennaio 2002 
Servlet API 2.3
II parte: gli ascoltatori e le altre innovazioni
di
Giovanni
Puliti
Prosegue l'analisi delle novità introdotte con la versione 2.3 della servlet API. Dopo aver parlato il mese scorso di filtri, questa volta introduciamo il nuovo meccanismo di monitorazione del ciclo di vita tramite ascoltatori.

Introduzione
Il mese scorso [1] abbiamo introdotto la nuova Servlet API 2.3 affrontando quella che forse è la novità più interessante aggiunta dai progettisti di Sun, i filtri HTTP.
Questo mese parleremo delle rimanenti modifiche ed aggiunte alla API, delle quali l'introduzione del meccanismo dei listeners rappresenta la novità più interessante.
Ricordo che chi desiderasse provare ad implementare gli esempi che accenneremo in questa sede, dovrà utilizzare un servlet engine compatibile con la versione 2.3 della API. Fra tutti i prodotti attualmente disponibili, pochissimi implementano le recenti novità della API, per cui si consiglia di fare riferimento alla documentazione del server. Le prove fatte per questo articolo si riferiscono a Tomcat 4.0 [2], probabilmente la migliore implementazione attualmente disponibile sul mercato.

 

 

Ascoltatori e ciclo di vita
Il ciclo di vita di un Servlet segue precise regole: a partire dalla inizializzazione tramite il metodo init(), la gestione delle richieste dei client, metodo service(), e lo spegnimento del servlet tramite il metodo destroy().
Da quando Sun ha introdotto con la nascita della piattaforma Java2EE concetti come Container e ServletContext lo scenario si notevolmente complicato: il servlet context infatti permette a due servlet o ad una pagina JSP ed un Servlet di scambiarsi oggetti, oppure ad un servlet di ottenere maggiori informazioni circa l'oggetto chiamante ed i parametri di sessione. Anche l'oggetto Session, presente fin dalla prima release delle Servlet API, ha assunto un ruolo molto più importante, specie nel caso di web application basate su pagine JSP e Servlet.
Per questo motivo, al fine di permettere una maggiore e più semplice gestione di tutte queste funzionalità, è stato aggiunto un utilissimo meccanismo basato su ascoltatori sintonizzati sui vari cambiamenti che interessano sia il ServletContext che l'oggetto Session.
Implementando alcune semplici interfacce un ascoltatore adesso potrà essere informato tutte le volte che un oggetto viene aggiunto al contesto o alla sessione. Più precisamente, mentre la sessione viene associata ad ogni client collegato, il context nasce e muore insieme ad una web application. Visto quindi che l'ascoltatore può essere notificato sugli eventi di creazione e distruzione del ServletContext, non è improprio, anche se formalmente non corretto, dire che le modifiche apportate hanno dato vita ad un nuovo ciclo di vita del servlet: in realtà niente è cambiato nel ciclo di vita. Il meccanismo di notifica degli eventi è lo stesso degli eventi utilizzati in Swing o AWT, meccanismo che come noto è basato sul cosiddetto Delegation Model [3], o pattern Observer[4] .

 

 

Ascoltatori di contesto
L'interfaccia che deve essere implementata per creare un ascoltatore sincronizzato sul ciclo di vita del ServletContext è la ServletContextListener, che contiene i seguenti due metodi:

void contextInitialized(ServletContentEvent sce)
void contentDestroyed(ServletContentEvent sce)

Il primo viene richiamato quando la web application è pronta per eseguire le chiamate dei client HTTP. In particolare la Web Application non sarà pronta fino a che questo metodo non abbia terminato la sua esecuzione. Il secondo metodo invece viene invocato dal server nel momento in cui la web application viene spenta, o quando più genericamente il context viene rimosso (ad esempio nel caso di applicazioni mobili fra più application server). La gestione delle richieste viene sospesa un attimo prima della invocazione di tale metodo.
In tutti e due i casi l'oggetto ServletContextEvent, tramite il metodo getServletContext(), permette di ricavare un riferimento al contesto appena dopo di essere inizializzato o prima di essere rimosso.
Nel caso in cui un oggetto sia interessato ad osservare il movimento degli oggetti inseriti o rimossi dal contesto, potrà implementare l'interfaccia ServletContextAttributeListener. In questo caso i metodi della interfaccia da implementare saranno:

void attributeAdded(ServletContextAttributeEvent sceae)
void attributeRemoved(ServletContextAttributeEvent sceae)
void attributeReplaced(ServletContextAttributeEvent sceae)

Il significato e funzionamento di tali metodi dovrebbe apparire piuttosto intuitivo. L'oggetto ServletContextAttributeEvent, che deriva direttamente dal ServletContextEvent estende il padre aggiungendo i metodi getName() e getValue() i quali permettono di avere maggiori informazioni sugli oggetti rimossi, aggiunti o modificati.

 

 

Ascoltatori di sessione
Per quanto riguarda la creazione di ascoltatori sincronizzati sugli eventi delle sessioni, il meccanismo è piuttosto simile. Anche in questo caso è stata aggiunta una interfaccia, la HTTPSessionListener, che mette a disposizione i seguenti metodi

void sessionCreated(HttpSessionEvent hse)
void sessionDestroyed(HttpSessionEvent hse)

Il primo viene invocato al momento della creazione della sessione, mentre l'altro alla sua distruzione. Anche in questo caso gli oggetti passati come parametri permettono di ottenere maggiori informazioni circa la sessione in esame. In particolare il metodo getSession() consente di ottenere un oggetto Session.
Anche in questo caso è possibile implementare un controllo più fine concentrando l'attenzione di un ascoltatore solo sugli oggetti aggiunti o rimossi dalla sessione: infatti implementando l'interfaccia HttpSessionAttributeListener ed i metodi

void attributeAdded(HttpSessionBindingEvent hsbe)
void attributeRemoved(HttpSessionBindingEvent hsbe)
void attributeReplaced(HttpSessionBindingEvent hsbe)

L'oggetto HttpSessionBindingEvent estende la più generica HttpSessionEvent, ed aggiunge I metodi getName() e getValue per conoscere nome e valore dell'oggetto inserito, rimosso o modificato nella sessione. La scelta del nome al posto di HttpSessionAttibuteEvent è stata fatta per mantenere la compatibilità con le versioni precedenti della API, visto che era già presente una classe con questo nome. E' possibile che nelle versioni successive, Sun decida di apportare modifiche in tal senso.

 

 

Implementazione degli ascoltatori
Dopo aver brevemente accennato i concetti di base relativamente agli ascoltatori, passiamo adesso a vedere un possibile utilizzo pratico di questi oggetti. Per prima cosa vedremo come implementare un ascoltatore di contesto ed uno di sessione, cercando successivamente di dare un senso ed una motivazione sull'utilizzo di questi strumenti.
Per creare un ascoltatore la prima cosa da fare è creare una classe che implementi una delle interfacce appena viste. Vediamo prima il caso di un ascoltatore di contesto: la classe potrebbe avere questa struttura

public final class MyContextListener
implements ServletContextAttributeListener, ServletContextListener {
...
}

In questo caso l'ascoltatore che andremo a realizzare sarà in grado di "ascoltare" sia gli eventi relativi al contesto vero e proprio che ai relativi attributi.
I vari metodi della interfaccia potrebbero in una versione molto semplice di questo ascoltatore effettuare un log su file tutte le volte che si verifica un evento. Ad esempio potremmo scrivere

public void attributeAdded(ServletContextAttributeEvent event) {
   fileLog("Aggiunto attributo al contesto " +
      
event.getName() +
      ", " + event.getValue());
}

dove il metodo fileLog() potrebbe utilizzare le funzionalità messe a disposizione dal contesto per effettuare log su file; ad esempio

private void fileLog(String message) {
   if (context != null)
      context.log("MyContextListener: " + message);
   else
      System.out.println("MyContextListener: " + message);
}

Gli altri metodi dell'ascoltatore potrebbero effettuare operazioni analoghe o del tutto differenti. In questo caso tralasciamo questi aspetti rimandando alla fantasia del programmatore.
Per la realizzazione di un ascoltatore di sessione invece potremmo creare un oggetto che, come nel caso precedente, possa funzionare come ascoltatore di tutti i tipi di eventi riguardanti una sessione. Ad esempio

public final class MySessionListener implements
   HttpSessionAttributeListener, HttpSessionListener {
   ...
   // uno dei metodi per la gestione degli eventi
   public void attributeAdded(HttpSessionBindingEvent event) {

      fileLog("Attributo aggiunto alla sessione " +               event.getSession().getId() +
              ": " + event.getName() + ", " + event.getValue() );
   }
}

Riconsiderando per un momento il meccanismo implementato in Swing o AWT circa la gestione degli eventi, si ricorderà che per rendere operativo un ascoltatore lo si deve registrare come listener di una determinata sorgente. In questo caso il servlet container effettua una operazione del tutto analoga: l'unica differenza è che essendo la sorgente unica per tutti i listener, il servlet container stesso, così come i tipi di eventi. in questo caso la registrazione si limita a comunicare al server il nome della classe associata ad un listener. Per fare questo al solito si ricorre ad un apposito tag XML, da inserire nel file web.xml della web application che contiene l'oggetto ascoltatore.
Ad esempio si potrebbe pensare di scrivere:

<!-un semplice ascoltatore di eventi di contesto -->
<listener>
<listener-class>
com.mokabyte.listeners.MyContextListener
</listener-class>
</listener>

 

 

Come e quando utilizzare gli ascoltatori
Il significato di questa importante aggiunta alla API potrebbe non essere immediatamente chiaro a tutti, ma in realtà rappresenta un ulteriore passo nella direzione della semplificazione della gestione delle applicazioni Enterprise. Per potersi convincere di questo si potrebbero elencare moltissimi casi in cui l'utilizzo degli ascoltatori possa essere utile. Per brevità si analizzeranno solo due semplici casi.
Il primo riguarda la gestione della connessione verso un database, e la sua ottimizzazione tramite l'utilizzo di ascoltatori del contesto. In questo caso si potrebbe utilizzare un connection pool al fine di ottimizzare le risorse e ridurre i tempi di attesa nella apertura e chiusura delle connessioni. Tale connection pool potrebbe essere inizializzato (lettura dei parametri di configurazione, apertura delle connessioni, e così via) prima del suo utilizzo, tramite un metodo startup(), e viceversa disattivato quando non più necessario, metodo shutdown().
In questo scenario si dovrebbe consentire ad ogni Servlet di ricavare una propria istanza di connection pool, ma anche di effettuare l'operazione di inizializzazione una sola volta, alla prima richiesta da parte di un Servlet.
Tipicamente questo tipo di operazioni viene svolto tramite l'utilizzo combinato del pattern Factory [5] (per permettere ad ogni servlet di ricavare una propria istanza del connection pool), e del Singleton (per effettuare l'inizializzazione una volta soltanto). Sebbene questa soluzione non sia particolarmente complessa, obbliga ad inserire nei metodo init() e destroy() di ogni Servlet una serie di operazioni concettualmente non banali.
Si potrebbe pensare di sfruttare il ciclo di vita del contesto tramite opportuni ascoltatori per effettuare una inizializzazione universale per tutti i servlet.
Ad esempio

public void contextInitialized(ServletContextEvent event) {
   // effettua la creazione del connection pool
   // invocando il costruttore con gli opportuni parametri
   ConnectionPool cp= new ...
   cp.startup();
   event.getServletContext().setAttribute("connectionpool",cp);
}

a questo punto dopo la creazione del contesto, tutti i servlet potranno ricavare il connection pool che sarà disponibile come attributo di sistema. Ad esempio:

public void doGet(HttpServletRequest request,
                  HttpServletResponse response)
                  throws ServletException, IOException{

   // ricava il connectio pool come attributo di contesto
   ServletContext context = getServletContext();
   ConnectionPool cp;    cp=(ConnectionPool)context.getAttribute("connectionpool");
   con=cp.getConnection();
}

Anche la gestione dello spegnimento del connection pool in modo corretto, ovvero quando tutti i servlet verranno rimossi dalla memoria, potrà essere eseguita da un ascoltatore opportunamente sincronizzato:

public void contextDestroyed(ServletContextEvent event) {
   // ricava il connectio pool come attributo di contesto
   ServletContext context = event.getServletContext();
   ConnectionPool cp;    cp=(ConnectionPool)context.getAttribute("connectionpool");
   cp.shutdown() ;
}

Un altro interessante utilizzo dei listener, sia di sessione che di contesto, potrebbe essere quello di realizzare un sistema monitor che controlli in ogni momento il numero di web application in funzione e di sessioni attive, ovvero di client connessi. Non entreremo nei dettagli di una applicazione di questo tipo, visto che lo spazio a disposizione non lo permette. Invece per comprendere meglio quello che potrebbe essere il risultato finale, si può vedere in figura 1, l'output prodotto dalla web application di gestione remota di Tomcat 4.0.


Figura 1 - La web application "Manager" di esempio fornita in
Tomcat 4.0 consente di monitorare ogni singola web application
in funzione ed il numero di sessioni attive

 

 


Selezione della codifica dei caratteri
Con la versione 2.3 della API è possibile adesso specificare il set di caratteri con cui il servlet deve interpretare la request del client. Il metodo

request.setCharacterEncoding(String encoding)

consente di effettuare tale impostazione e di permettere in modo corretto ad esempio una successiva lettura dei parametri di invocazione. Ad esempio se il client utilizza il set di caratteri Shift_JIS, uno dei set di caratteri utilizzati per la lingua giapponese, per poter correttamente interpretare un parametro passato all'invocazione, si potrebbe pensare di scrivere

request.setCharacterEncoding("Shift_JIS");
String param=req.getParameter("param");

Questo genere di operazioni è necessario in tutti quei casi in cui il set di caratteri di default, il Latin-1 (ISO 8859-1) può portare ad errori di interpretazione, come nel caso della lingua giapponese appunto. Normalmente infatti il set di caratteri se diverso da quello di default dovrebbe essere specificato come intestazione nell'header della richiesta, ma tale assunzione non è detto che sia sempre rispettata dai vari browser o client HTTP.
Per questo motivo il set di caratteri deve essere specificato prima di ogni operazione di tipo getParameter() o getReader(); la chiamata al metodo setCharacterEncoding() può generare una eccezione di tipo java.io.UnsupportedEncodingException se la codifica non è supportata.

 

 

Gestione separata dei vari classloaders
Una modifica piuttosto importante è stata apportata al classloader delle web applications. Adesso ogni web application esegue il caricamento delle classi in uno spazio di memoria indipendente dalle altre, ma anche e soprattutto indipendente da quello del server. Questo secondo aspetto, apparentemente non rilevante, ha invece importanti ripercussioni in fase di deploy di una applicazione. Infatti prima non era infrequente che una qualche libreria caricata dalla JVM del server andasse in conflitto con quelle necessarie alle varie web application, a discapito di queste ultime (dato che il server carica in memoria le classi prima delle singole web application). Il caso più eclatante e frequente era quello del conflitto di parser XML: dato che il server spesso utilizza file XML per la configurazione complessiva, utilizza strumenti come Xerces o simili, che spesso però sono utilizzati anche all'interno della web application (magari con una versione differente).
E' per questo motivo che infatti dalla versione 4.0 Tomcat non fornisce più nessun parser XML di default alle varie web application le quali devono provvedere a contenerne uno in modo autonomamente, ad esempio tramite un file .jar all'interno della directory lib della applicazione.

 

 

Nuovi attributi di protezione
Nell'ambito delle connessioni in modalità protetta tramite protocollo HTTPS, la nuova api fornisce due nuovi attributi in grado di determinare il livello di sicurezza relativamente della chiamata in atto. I due attributi sono

javax.servlet.request.chiper_suite
javax.servlet.request.key_size

il primo permette di ricavare la chiper suite utilizzata da HTTPS se presente, mentre il secondo indica il numero di bit utilizzati dall'algoritmo di crittografia. Utilizzando tali informazioni un servlet adesso può rifiutare una connessione se il livello di sicurezza o l'algoritmo utilizzato non risultano idonei. Ad esempio

public void service(HttpServletRequest req,
                    HttpServletResponse res)
                    throws ServletException, IOException {
   Integer CriptSize =(Integer)
   req.getAttribute("javax.servlet.request.key_size");

   if(CriptSize == null || CriptSize.intValue() < 128){
      response.setContentType("text/html");
      PrintWriter out = response.getWriter();
      out.println("<html>");
      out.println("<head><title>HTTPS Servlet </title></head>");
      out.println("<body>");
      out.println("<p> La chiamata non è sufficientemente
      sicura e verrà ignorata </p>");
      out.println("</body></html>");
   }
}

 

Altre piccole modifiche
Fra le modifiche di minore importanza, si può fare riferimento ai metodi della classe HttpUtils, che sono stati spostati adesso in altri punti secondo una logica più consona con l'intera API. I metodi relativi alla creazione di URL a partire da una request sono stati spostati nella HTTPServletRequest stessa, che contemporaneamente è stata aggiunta di altri metodi di utilità. Ad esempio
StringBuffer request.getRequestUrl() restituisce una StringBuffer contenente l'url della richiesta.
Il metodo java.util.Map request.getParameterMap() restituisce una mappa non modificabile con tutti i parametri della invocazione. I nomi dei parametri sono utilizzati come chiave della mappa, mentre i valori sono memorizzati nei corrispondenti valori della mappa. Per i valori multipli al momento non è stato specificato quale convenzione verrà utilizzata, probabilmente tutti i valori verranno restituiti come array di stringhe.
Inoltre sono stati aggiunti all'oggetto ServletContext i metodi getServletContextName(), che consentono di ricavarne il nome, e getResourcePaths() che restituisce un elenco non modificabile di stringhe corrispondenti a tutti i path delle risorse disponibili all'interno del contesto. In particolare ognuna di queste stringhe inizia per "/" e deve essere considerato un indirizzo relativo al contesto in esame.
L'oggetto HttpServletResponse invece è stato aggiunto del metodo resetBuffer() il cui scopo è quello di eliminare ogni contenuto all'interno del buffer, senza pero' cancellare le intestazioni o il codice di stato, prima che la risposta sia inviata al client. In caso contrario viene generata una eccezione di tipo IllegalStateException.
Infine dopo una lunga discussione del comitato che segue l'evoluzione della API è stato chiarito, rispetto alla versione precedente che lasciava in sospeso questo aspetto, che il metodo HttpServletResponse.sendRedirect(String uri) effettui un reindirizzamento verso l'indirizzo specificato traducendo tale indirizzo relativo in indirizzo assoluto ma relativamente al container e non al context in uso. Più semplicemente in una chiamata del tipo

response.sendRedirect("/index.html");

l'indirizzo relativo "index.html" verrà trasformato in indirizzo assoluto del tipo "http://server:port/index.html" e non http://server:port/contextname/index.html. Chi volesse ottenere il risultato opposto potrà utilizzare il metodo getContextName() preponendolo al nome dell'URI utilizzato.

 

 

Conclusioni
Questa nuova versione della servlet API appare piuttosto interessante. Sicuramente le novità più importanti sono quelle relative all'introduzione del meccanismo dei filtri (di cui si è parlato il mese scorso [1], e dei listener. Questi due innovativi strumenti permetteranno un più agile e potente meccanismo di gestione di applicazioni web basate su tecnologia Servlet/JSP.
Per quanto riguarda invece le altre innovazioni, si tratta di migliorie di minor conto, ma che nel complesso mostrano come la API sia ormai giunta ad un buon livello di maturità e stabilità; il tutto mostra come Sun creda nella importanza di questo settore della tecnologia Java (insieme a JSP, EJB, Java&XML), e di fatto prosegua nella sua costante miglioramento ed aggiornamento.
Una valutazione non di poco conto questa per chi volesse investire in questa direzione o proseguire a farlo.

Bibliografia
[1] - "Servlet 2.3 API - I parte: i filtri" di Giovanni Puliti, MokaByte 58, Dicembre 2001, www.mokabyte.it/2001/12/servlet23_1.htm

[2] - Home page di Apache Tomcat: http://jakarta.apache.org
[3] "La gestione degli eventi con il JDK 1.1", di Massimo Carli, MokaByte Numero 05 - Febbraio 1997, www.mokabyte.it/05/jdk.htm
[4] "Pattern observer" di Lorenzo Bettini, MokaByte Numero 26 - Gennaio 1999
http://www.mokabyte.it/1999/01/observer_teoria.htm
[5] "Una Fabbrica di oggetti" di Lorenzo Bettini, MokaByte Numero 27 - Febbraio 1999
http://www.mokabyte.it/1999/02/factory_teoria.htm

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