|
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);
}
|