ePrometeusCorsoJavaJava
testi articoli
Testi Articoli  Download
Home | Eshop | Java | Tools | Web | 
CorsoJava è ora Video! Free for all!
Clicca Qui!

FRAMEWORK SERVLET JAVA
Un framework per applicazioni Web in Java
Usare le Servlet
Interfaccia delle HttpServlet
Un compilatore di HTML
Semplificare le query
L'Agenda Web
Conclusioni
Bibliografia e Listati
L'autore

<<< Interfaccia delle HttpServlet >>>

Abbiamo visto come le servlet siano classi di due package che curiosamente hanno un nome che comincia con javax. Si tratta in effetti di una "standard extension": ovverlo le servlet non fanno parte della "core api", ovvero della piattaforma Java così come è definita da Sun, quindi non è richiesta l'implementazione delle classi delle servlet per fregiarsi del logo Java-compatibile. Sarebbe comico, infatti, che un Web Browser debba pure supportare funzioni di un Web Server per avere il logo! Comunque si tratta di classi in qualche modo standardizzate in maniera ufficiale. La standardizzazione è molto importante per le API, in modo da definire un comun denominatore per interfacciare molti prodotti diversi nello stesso modo. Il package javax.servlet specifica il framework generale, mentre il package più importante è javax.servlet.http, che implementa le servlet per i Web Server. Nel prosieguo tratteremo esplicitamente di servet http, per cui quando dico servlet intendo parlare di una HttpServlet.

Vediamo adesso le basi della tecnica di programmazione delle servlet. Una servlet è una classe che viene istanziata senza argomenti ma prima che diventi operativa, deve venir inizializzata: il Web Server chiama HttpServlet.init(ServletConfig) per configurarla. In particolare vengono passati dei parametri forniti con una procedura che varia da server a server (di solito esiste una sorta di pannello di controllo). È buona norma parametrizzare una servlet quanto possibile: per esempio il database utilizzato è meglio non "cablarlo" ma fare in modo che sia specificabile con dei parametri esterni. Ovviamente lo scopo è quello di riuscire a cambiare database senza dover ricompilare la servlet, magari da remoto.

Una servlet "serve" (appunto) richieste provenienti dal Web Server di cui è una estensione. Quindi il metodo principale è HttpServlet.service(HttpServletRequest req, HttpServletResponse res). Vedremo più avanti cosa può e deve fare la service. Completiamo il discorso con la "finalizzazione": il metodo destroy(), che viene chiamato quando si termina di eseguire le operazioni, tipicamente allo shutdown del servlet engine o quado l'utente volontariamante "scarica" una servlet. È buona norma al destory effettuare dei cleanup come la chiusura della connessioni al database. Non è cosa da trascurare: per esempio se la connessione non viene chiusa e il vostro server di database accetta un numero di connessioni limitato a causa della licenza, inesorabilmente arriverà il messagio: "numero di connessioni ammesso dalla licenza esaurito" dopo un paio di unload della servlet, unload che avvengono automaticamente quando si ricompila durante lo sviluppo. Per non parlare di peggiori effetti collaterali come file lockati irrimediabilmente che costringono ad un riavvio della macchina quando si usano Web Server "intelligenti" come il PWS.

Il Web Server indidua ogni servlet con un URL, in genere qualcosa come http://mio.web.server/servlet/NomeDellaServlet. Nell'URL, dopo il nome delll'host, /servlet indica al Web Server di utilizzare il servlet engine per esegure le richieste. NomeDellaServlet è generalmente il nome della classe che implementa la servlet; alcuni servlet engine consentono tuttavia di rinominare le servlet, in modo che la servlet mio.package.molto.annidato.MiaServlet possa essere chiamato semplicemente con /servlet/MiaServlet). La servlet viene carica in memoria una volta sola e rimane a servire le richieste: questo è già un notevole vantaggio in efficienza rispetto alle CGI. Inoltre essendo un unico programma a eseguire tutte le richieste ad un dato URL, può mantenere uno stato in maniera ben più semplice rispetto alle CGI: non occorre salvare nulla su disco, basta usare delle variabili in memoria: altro importante vantaggio in efficienza.

Ad ogni richiesta viene dunque chiamato service(HttpServletRequest, HttpServletResponse). Tipicamente il service è gestito in multithreading, quindi ci si deve aspettare esplicitamente che più Thread invochino tale metodo sullo stesso oggetto. Per essere esatti, per ogni URL di servlet viene creata una istanza della classe, anche se ad URL disitinti possono corrispondere istanze diverse della stessa classe, tipicamente inizializzate con parametri diversi. Ogni istanza ha un metodo service che viene chiamato in maniera concorrente da più thread. Anzi, solitamente i servlet engine attivano un pool di thread che chiamano il service in maniera concorrente. Per cui bisogna stare molto attenti alle problematiche della concorrenza. Sconsigliatissimo dichiarare synchronized service. L'overhead è molto elevato per un metodo che viene essere chiamato continuamente. Se proprio si vuole una servlet che esegua una richiesta alla volta (presumibilmente una servlet che viene richiamata solo una volta ogni tanto e di cui si può trascurare l'efficienza) si può dichiarare implements SingleThreadModel. Si tratta di una tipica interfaccia dummy (senza metodi) usata come marcatore, per segnalare al servlet engine di non eseguire il service in maniera concorrente. In questo modo si evitano le pesanti strutture dati allocate dai metodi sincronizzati ottenendo di non doversi preoccupare dell'accesso concorrente.

Le informazioni della richiesta le troviamo nel parametro request, mentre le informazioni per la risposta le prendiamo dal parametro response. Principalmente la servlet deve produrre informazioni in formato HTML, per cui le prime operazioni da fare quasi sempore sono:


response.setContentType("text/html");

PrintWriter out = response.getWriter();


In questo modo informiamo che sarà un file HTML, e ricaviamo un PrintWriter dove scrivere il testo HTML che verrà generato. ServletRequest e ServletResponse in realtà contengono moltissime informazioni: i dati della form, i cookie, le varie informazioni sul richiedente, eccetera eccetera.

Da quanto visto dovrebbe essere abbastanza chiaro che le classi per le servlet offrono sì il necessario per interfacciarsi con il Web Server, ma la API è alquanto essenziale (offre ne più e ne meno che le stesse feature della CGI) e infatti scrivere servlet sfruttando solo la servlet API non è il massimo della comodità. Vediamo quindi come semplificarci la vita sviluppando il framework oggetto di questo articolo.

L'idea principale è questa: le applicazioni Web sono principalmente programmi che elaborano dati dalle form: li inseriscono in database oppure costruiscono pagine, che sono poi dei report in formato HTML di interrogazioni a database. I dati della form sono quindi l'oggetto principale da manipolare, e sono sostanzialmente delle associazioni chiave-valore, dove la chiave è una stringa mentre il valore è un array di stringhe. Questa informazione viene però passata dalle servlet in maniera un pochettino rudimentale: per esempio come stringa da decodificare oppure come Input Stream da leggere (dipende se si tratta di una POST o una GET).

Nel nostro framework, tutte le operazioni di una servlet sono eseguite sui dati della form, utilizzando una classe che li incapsula: HttpData, mostrata (quasi tutta - omessi solo metodi irrilevanti per brevità) nel listato 1. Questa classe è assolutamente centrale nel nostro framework, in quanto tutte le altre la utilizzano. Viene utilizzata al posto di HttpServletRequest e HttpServletResponse e offre una astrazione più comoda da usare (appunto la tabella chiave-valori) e vari metodi per accedere alle funzionalità più frequenti. In ogni caso, qualora servissero, request e response originari possono essere ricavati da essa.


//////////////////////////////////////////////////
// File agenda/HttpData.java
//////////////////////////////////////////////////
package agenda;

import javax.servlet.http.*;

import java.io.*;
import java.util.*;
import java.sql.*;

public class HttpData
{

  private Hashtable data = new Hashtable();
  private OutputStream os;

  /** Simple constructor for tests */
  public HttpData(String query) { this(query, null); }
  public HttpData(String query, ResultSet rs) {
      setResult(rs);
      os=System.out;
      out = new PrintWriter(os);
      if(query!=null && query.length() >0)
        add(HttpUtils.parseQueryString(query));
  }

  /** Full constructor */
  public HttpData(HttpServletRequest req, HttpServletResponse res) throws IOException
  {
      this.req = req;
      this.res = res;
      os=res.getOutputStream();
      out = new PrintWriter(os);
      method = req.getMethod();
      if(method.toUpperCase().equals("GET")) {
        String s = req.getQueryString();
        if(s!=null && s.length()>0)
          add(HttpUtils.parseQueryString(s));
      } else if(method.toUpperCase().equals("POST")) {
        int n=req.getContentLength();
        if(n>0) {
          StringBuffer sb = new StringBuffer();
          InputStream in = req.getInputStream();
          while( n-- > 0)
            sb.append((char)in.read());
          add(HttpUtils.parseQueryString(sb.toString()));
        }
      }
  }


  public HttpServletRequest req;
  public HttpServletResponse res;
  public javax.servlet.http.HttpServletRequest getReq() { return req; }
  public javax.servlet.http.HttpServletResponse getRes() { return res; }

  public PrintWriter out;
  public void println(String s) { out.println(s);  }
  public void print(String s) { out.print(s); }
  public void flush() { out.flush(); }


  public String get(String k) { 
        return quote(getString(k)); 
  }
  public void set(String k, String v) {
    data.put(k.toUpperCase().intern(), new String[] { v });
  }
  public boolean isNull(String name) {
    return data.get(name.toUpperCase().intern())==null;
  }

  public String[] getStrings(String name) {
    return (String[])data.get(name.toUpperCase().intern());
  }
  public String getString(String name) {
        String[] as = getStrings(name);
        if(as==null)
          return "";
        StringBuffer sb = new StringBuffer(as[0]);
        for(int i=1; i<as.length; ++i)
          sb.append(" ").append(as[1]);
        return sb.toString();
  }
  public int getInt(String name) {
    try {
      return Integer.parseInt(getString(name));
    } catch(NumberFormatException ex) {
      return 0;
    }
  }
  public double getDouble(String name) {
    try {
      return Double.valueOf(getString(name)).doubleValue();
    } catch(NumberFormatException ex) {
      return 0.0;
    }
   }

  // Handling an embedded resultset
  private ResultSet rs = null;
  private String[] rskeys = null;
  private Hashtable rsfields = new Hashtable();
  private boolean rsnull = false;
  private boolean rsnull_onlyonce = false;
  public void setResult(ResultSet rs) { this.rs = rs; }
  public boolean initRSLoop()
  {
    if(rs == null) {
      rsnull = true;
      rsnull_onlyonce = true;
      return true;
    } else rsnull=false;
    try {
        ResultSetMetaData rsmd = rs.getMetaData();
        int rskeyslen;
        String s;
        rskeys = new String[rskeyslen = rsmd.getColumnCount()];
        for(int i=1; i<=rskeyslen; ++i) {
          s = rsmd.getColumnName(i).toUpperCase();
          rskeys[i-1] = s;
        }
    } catch(Exception ex) {
      System.err.println(ex.getMessage());
      return false;
    }
    return true;
  }

  public boolean RSLoop()
  {
    if(rsnull) {
      if(rsnull_onlyonce) {
        rsfields.clear();
        rsnull_onlyonce=false;
        return true;
      } else return false;
    }
    try {
      if(!rs.next())
        return false;
      for(int i=0; i<rskeys.length; ++i) {
        String key = rskeys[i];
        String val = rs.getString(key);
        rsfields.put(key.toUpperCase().intern(), (val==null) ? "" : val);
      }
      return true;
    } catch(Exception ex) {
      System.err.println(ex.getMessage());
      return false;
    }
  }

  public String fget(String s) {
    String val = (String)rsfields.get(s.toUpperCase().intern());
    if(val==null)
      return "";
    else
      return quote(val.trim());
  }

  public void set(ResultSet rs) {
    if(rs == null)
      return;
    String s,t;
    try {
        if(!rs.next())
          return;
        ResultSetMetaData rsmd = rs.getMetaData();
        int rskeyslen = rsmd.getColumnCount();
        for(int i=1; i<=rskeyslen; ++i) {
          s = rsmd.getColumnName(i).toUpperCase();
          t = rs.getString(s).trim();
          System.out.println(s+"="+t);
          set(s, t==null ? "" : t);
        }
    } catch(Exception ex) {
      System.err.println(ex.getMessage());
    }
  }

  public String quote(String s) { /*omissis*/}
  private add(Hashtable ht) { /*omissis*/}
}

//////////////////////////////////////////////////
// File agenda/HttpServlet.java
//////////////////////////////////////////////////

package agenda;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HttpServlet
    extends javax.servlet.http.HttpServlet
{

  public void
  service(HttpServletRequest req,
          HttpServletResponse res)
            throws ServletException, IOException
  {
    HttpData data = null;
    try {
      data = new HttpData(req, res);
      String method = req.getMethod();
      String op = data.getString("op");
      select(data, op);
    } catch(Exception ex) {
      PrintWriter out = res.getWriter();
      out.println("<pre>");
      ex.printStackTrace(out);
      out.println("</pre>");
    }
    if(data!=null)
      data.flush();
   }

   public void select(HttpData data, String op) throws Exception { }
   public void doException(HttpData data){/*omissis*/}
}



Costruiamo HttpData data = new HttpData(request,response) all'ingresso della nostra servlet e poi ci serviamo di questo oggetto per leggere i dati provenienti dalla form o come contenitore di informazioni da passare tra le varie classi che gestiscono una servlet. Notare che possiamo estrarre i dati della form come array di stringhe (data.getStrings("name")) ma anche come stringa singola (solo la prima - che poi in pratica è il caso più frequente), o come intero o double. Inoltre il nostro data può anche contenere un ResultSet, il risultato di una interrogazione a database, e fornisce i metodi per navigare semplicemente tra le righe e i campi del result set (initRSLoop, RSLoop, fget). Il nostro HttpData contiene anche due metodi print e println per generare l'output. Comunque sia, all'atto pratico questo design si dimostra comodo ed efficace e risparmia codice e quindi errori. Dopo aver scritto decine di volte il codice che estraeva lo stream di output dall'oggetto data, mi sono deciso a dare all' HttpData la capacità di stampare,e non me ne sono ancora pentito…

La classe in questione si preoccupa anche di aspetti come il quoting, e infatti le stringhe estratte possono normalmente essere stampate in un testo HTML senza preoccuparsi se contengono ">" o "&": di default infatti sono quoted, ma se serve si può evitare.

Adesso costruiamo le altre classi, che assumono che i dati da manipolare si trovino in un HttpData. La classe agenda.HttpServlet (sempre nel listato 1), estende e ridefinisce la javax.servlet.http.HttpServlet in maniera da introdurre alcune convenzioni, anch'esse molto utili in pratica. La prima convenzione è che il campo op contiene sempre la prossima azione da eseguire. Ogni volta che si produce una pagina, questa rappresenta uno stato, che deve memorizzare anche la prossima azione da eseguire. Mi spiego subito con un esempio. L'agenda ha due bottoni: NEW e EDIT. Con NEW arrivo ad una form vuota per inserire i dati di una persona da mettere nell'agenda; l'azione successiva da eseguire sarà una INSERT nel database. Con EDIT arrivo alla stessa form però riepita con i dati presi dal database, e l'azione successiva da eseguire sarà una UPDATE. In pratica le pagine devono spesso memorizzare la prossima azione da eseguire: per questo uso la convenzione della op. Questa convenzione è rinforzata in agenda.HttpServlet dal fatto che il metodo da ridefinire per implementare le proprie azioni è select(String op, HttpData data), dove op è il campo op preso dalla form in input. Un altro aspetto importante è che usando queste due classi (HttpData e HttpServlet) viene notevolmente semplificato il debugging. Infatti è possibile testare ogni azione implementata simulando l'input che proviene dalla form usando qualcosa come:


// Test Servlet
public static void main(String[]args) {
// il parametro
è l'encoding di una query string

HttpData data = new HttpData("op=add&a=1&b=2);

new MiaServlet().select(data.get("op"), data);

}


ePrometeus s.r.l. - Web Software House & Open Source System Integrator
MILANO - SAN BENEDETTO DEL TRONTO(AP)
Contatti: info@eprometeus.com