Nell’articolo Le Servlet Java (parte 1) abbiamo introdotto i concetti di servlet e servlet container ed abbiamo presentato un semplice esempio di implementazione. In questo post approfondiamo alcuni aspetti di tale tecnologia.
Ciclo di Vita
Abbiamo visto che per realizzare un servlet HTTP è sufficiente estendere la classe HttpServlet
, appartenente al package javax.servlet.http
, ridefinire il metodo init()
, per personalizzare l’inizializzazione della servlet, ed i metodi service()
, doPost()
e doGet()
per definirne invece il comportamento in funzione delle invocazioni del client.
Inizializzazione
In generale una servlet viene creata al momento della prima invocazione, ma è possibile anche istruire il container in modo che la istanzi non appena il server è avviato. Prima di essere resa disponibile per le chiamate da parte dei client il container invoca il metodo init()
, derivato direttamente dalla classe GenericServlet
, che ha la seguente firma:
1 |
public void init(ServletConfig config) throws ServletException |
Dalla versione 2.1 delle API è anche possibile utilizzare un metodo init()
senza parametri.
1 |
public void init() throws ServletException() |
In caso di problemi durante l’inizializzazione il metodo può sollevare l’eccezione ServletException
. In tal caso l’oggetto verrà immediatamente rilasciato ed il metodo destroy()
non sarà invocato, perché la servlet non si è inizializzata correttamente. Alla successiva richiesta il container provvederà a creare una nuova istanza.
In alternativa è possibile indicare un periodo di indisponibilità della servlet utilizzando l’eccezione UnavailableException
con la quale, oltre al messaggio, è possibile indicare tale periodo in secondi. Ad esempio forzando l’eccezione con l’init()
seguente:
1 2 3 4 |
public void init(ServletConfig config) throws ServletException { super.init(config); throw new UnavailableException( "Messaggio di test", 30 ); } |
osserveremo nel log un messaggio del tipo:
1 |
12:31:25,363 ERROR [io.undertow.servlet.request] (default task-18) UT015003: Stopping servlet HelloWorldServlet till Tue Oct 25 12:30:49 CEST 2016 due to temporary unavailability: javax.servlet.UnavailableException: Messaggio di test |
che indica appunto che la servlet è ferma fino alla data ed ora indicati. Si noti che le successive invocazioni della servlet entro i 30 secondi non genereranno alcuna eccezione, perché il container non proverà neppure ad istanziarla.
La servlet può ricevere i parametri di configurazione dall’oggetto ServletConfig
o in alternativa, nel caso si utilizzi l’init()
senza parametri, attraverso il metodo getInitParameter()
ereditato dalla classe GenericServlet
. Ad esempio configuriamo la servlet HelloWordServlet.java
presentata nel post Le Servlet Java (parte 1) nel modo seguente, dove abbiamo introdotto il parametro di inizializzazione site
:
1 2 3 4 5 6 7 8 9 10 |
<servlet> <description>My First Base Http Servlet</description> <display-name>HelloWorldServlet</display-name> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>it.javaboss.HelloWorldServlet</servlet-class> <init-param> <param-name>site</param-name> <param-value>www.javaboss.it</param-value> </init-param> </servlet> |
Il codice della servlet quindi recupera il parametro nell’init()
e lo utilizza nel doGet()
per stampare la string “Hello [nome] [cognome] from [site]”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class HelloWorldServlet extends HttpServlet { private String site; public void init(ServletConfig config) throws ServletException { this.site = config.getInitParameter( "site" ); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); String surname = request.getParameter("surname"); response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head><title>Hello World</title></head>"); out.println("<body>Hello " + name + " " + surname + " from " + site + "</body>"); out.println("</html>"); out.close(); } } |
Esecuzione
Una volta eseguita l’inizializzazione il servlet resta in attesa di una eventuale chiamata da un client. A tale scopo l’interfaccia Servlet
mette a disposizione il metodo service()
.
1 |
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; |
che viene invocato direttamente dal container in modalità multithread, ovvero il server, per ogni invocazione dei client, manda in esecuzione il metodo in un thread separato. Anche nel caso di servlet HTTP il metodo service()
è coinvolto, ma il suo comportamento è ridefinito in modo da eseguire i metodi doGet(), doPost() e tutti gli altri previsti dal protocollo HTTP in funzione della richiesta.
1 2 3 |
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
Distruzione
A completamento del ciclo di vita della servlet, si trova la fase di distruzione della stessa che è legata al metodo destroy()
. La ridefinizione di tale metodo, derivato dalla interfaccia Servlet
, permette di specificare tutte le operazioni simmetriche alla inizializzazione, oltre a sincronizzare e rendere persistente lo stato della memoria.
È importante notare che la chiamata al metodo destroy()
, così come quella all’init()
è a carico del container che ha in gestione la servlet. La distruzione della servlet e il relativo rilascio dalla memoria avviene solamente nel momento in cui tutte le chiamate al metodo service()
sono terminate. Nel caso in cui tale metodo effettui una computazione più lunga del previsto, il server può dare comunque atto alla distruzione della servlet.
Interagire con le Servlet
Negli esempi abbiamo visto come la comunicazione con le servlet avvenga in modo bidirezionale attraverso gli oggetti di tipo ServletRequest
, HttpServletRequest
, ServletResponse
e HttpServletResponse
.
La Request
Gli oggetti ServletRequest
e HttpServletRequest
oltre a permettere l’accesso tutte le informazioni relative agli header http, permette di ricavare i parametri passati insieme all’invocazione del client. Tali parametri sono inviati come di coppie nome-valore, sia che la richiesta sia di tipo GET che POST. L’interfaccia ServletRequest
dalla quale discende la HttpServletRequest
mette a disposizione alcuni metodi per ottenere i valori dei parametri passati alla servlet.
String getQueryString() |
Ritorna la query string completa (ovviamente solo per GET). |
Enumeration getParameterNames() |
Ritorna una enumerazione dei nomi dei parametri. |
String getParameter(String) |
Ritorna il valore del parametro a partire dal suo nome. Se il parametro prevede più valori deve essere utilizzato il metodo getParameterValues() . |
String) |
Ritorna una array di valori del parametro. |
Nel caso in cui si attendano dati non strutturati e in forma testuale, e l’invocazione sia una POST, una PUT o una DELETE si può utilizzare il metodo getReader()
, che restituisce un BufferedReader
. Se invece i dati inviati sono in formato binario, allora è indicato utilizzare il metodo getInputStream()
, il quale a sua volta restituisce un oggetto di tipo ServletInputStream
.
Gli Header nella Request
La codifica dei pacchetti HTTP prevede una parte di intestazione detta header dove si possono codificare le informazioni legate alla tipologia di trasmissione in atto. Un Servlet può accedere agli header della request HTTP per mezzo dei seguenti metodi della interfaccia HttpServletRequest
:
String getHeader(String name) | Ritorna il valore dell’header richiesto. |
Enumeration getHeaders(String name) | Ritorna un enumeration con tutti i valori assunti dall’header. |
Enumeration getHeaderNames() | Restituisce un enumeration con i nomi di tutti gli header. |
int getIntHeader(String name) | Restituisce il valore di un header che assume valori interi. |
long getDateHeader(String name) | Restituisce il valore di un header che assume valori di tipo data. |
Di seguito un esempio di servlet che restituisce tutti gli header della request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class HeaderServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head><title>Headers</title></head>"); out.println("<body>"); Enumeration<String> headers = request.getHeaderNames(); while ( headers.hasMoreElements() ) { String header = headers.nextElement(); out.println( header + ": " + request.getHeader( header ) + "<br/>"); } out.println("</body>"); out.println("</html>"); out.close(); } } |
La Response
La risposta del Servlet può essere inviata al client per mezzo di un oggetto di tipo ServletResponse
o HttpServletResponse
, i quali offrono due metodi per inviare dati al client.
getWriter() |
Restituisce un oggetto di tipo java.io.Writer che può essere utilizzato per inviare dati di tipo testuale al client (es. codice html). |
getOutputStream() |
Restituisce un oggetto di tipo javax.servlet.ServletOutputStream che può essere utilizzato per inviare dati in formato binario al client. |
Da notare che la chiusura di un Writer o di un ServletOutputStream dopo l’invio dei dati permette al server di conoscere quando la risposta è completa. Con i metodi setContentType()
e setHeader()
si può stabilire il tipo MIME della pagina di risposta e, nel caso sia HTML, avere un controllo fine su alcuni parametri.
Il container per di inviare la response al client utilizza tecniche di response buffering al fine di ottimizzare la comunicazione. L’interfaccia ServletResponse
fornisce metodi che consentono la gestione con tale buffer, indipendentemente dal tipo di writer utilizzato.
getBufferSize() |
Restituisce la dimensione del buffer attualmente utilizzata. Se è risultato un intero pari a zero, allora questo significa la mancanza di buffering. |
setBufferSize() |
Consente di impostare la dimensione del buffer associata ad una risposta. E’ sempre bene specificare una dimensione maggiore di quella utilizzata normalmente dal container, al fine di riutilizzare la memoria già allocata e velocizzare ulteriormente la modifica. Deve essere invocato prima di ogni operazione di scrittura. |
isCommitted() |
Restituisce un valore booleano ad indicare se siano stati inviati o meno dei dati verso il cliente. |
flushBuffer() |
Forza l’invio dei dati presenti nel buffer. |
reset() |
Provoca la cancellazione di tutti i dati presenti nel buffer |
Gli Header nella Response
Nell’inviare la risposta al client, la servlet può anche modificare l’header HTTP per mezzo dei metodi messi a disposizione dalla interfaccia HttpServletResponse.
void setHeader(String name, String value) void setDateHeader(String name, long date) void setIntHeader(String name, int value) |
Permettono di settare l’header con nome e valore per tipi specifici. Nel caso che uno o più valori fossero già presenti con tale nome, allora verrebbero tutti sostituiti con il nuovo valore. |
void addHeader(String name, String value) void addDateHeader(String name, long date) void addIntHeader(String name, int value) |
Consentono di aggiungere un valore a un header specifico che va quindi ad aggiungersi ai valori già assunti. |
Esistono poi metodi che consentono di impostare valori per header specifici come ad esempio il metodo sendRedirect()
che imposta l’header location.
La Sessione
Il fatto che più esecuzioni di una stessa servlet facciano capo allo stesso oggetto permette di condividere proprietà e metodi della classe e quindi l’informazione. In pratica ciò si traduce nell’istanziare nel metodo init()
delle variabili di classe, che saranno successivamente visibili nelle varie esecuzioni. L’utilizzo di tali variabili non è però adatto alla gestione di una sessione utente che per sua natura è unica su diverse chiamate e non condivisibile con gli altri utenti. Le Servlet API forniscono una serie di strumenti per fare tutto ciò in maniera efficace.
Quando una nuova sessione utente è creata il server predispone l’area della memoria che conterrà le informazione della sessione e farà in modo di tracciare il browser dell’utente, in modo da poter successivamente associare ogni richiesta di quel client con quella determinata sessione. In pratica viene registrato un cookie sul browser con un numero di identificazione della sessione, il Session Id.
Per gestire le sessioni le Servlet API mettono a disposizione tre classi: HttpSession
, HttpSessionBindingListener
e HttpSessionContext
. In pratica nei metodi doGet()
, doPost()
o service()
è possibile ottenere un riferimento all’oggetto HttpSession
tramite una chiamata al metodo getSession()
di HttpServletRequest
:
1 |
HttpSession session = request.getSession(); |
La servlet cerca di identificare il client (nel nostro caso il browser) che la chiama. Se esiste già un oggetto HttpSession
per quel client session, allora lo restituirà, altrimenti ne verrà creato uno nuovo.
Si noti che la classe HttpSession
è stata progettata per gestire eventuali situazioni in cui il browser non accetti i cookie. Essa infatti, oltre a eseguire le opportune verifiche, aggira il problema inserendo il Session Id negli URL.
L’utilizzo dei cookie presenta però il vantaggi, oltre dell’eleganza e della trasparenza per l’utente, di consentire il recupero della sessione anche se il browser viene chiuso, permettendo di fatto di poter recuperare il lavoro da dove è stato lasciato.
Di seguito si mostra una servlet che restituisce il numero di invocazione alla servlet CounterServlet
. Per un test completo si consiglia di provare ad invocare la servlet con browser diversi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class CounterServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); // Recupero il counter in sessione Integer counter = (Integer) session.getAttribute( "counter"); // Incremento il counter if ( counter==null ) { counter = new Integer(1); } else { counter = new Integer(counter.intValue() + 1); } // Memorizzo nella sessione il nuovo valore session.setAttribute("counter", counter); response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head><title>Hello World</title></head>"); out.println("<body>Counter: " + counter.toString() + "</body>"); out.println("</html>"); out.close(); } } |
Codice Sorgente
Il codice sorgente è scaricabile qui demo.