Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Servlet 3.0

Breve panoramica che illustra le principali caratteristiche delle servet 3.0, con esempi
Breve panoramica che illustra le principali caratteristiche delle servet 3.0, con esempi
Link copiato negli appunti

Le nuove servlet 3.0 introducono una serie di importanti novitá sia per il loro sviluppo (diventando finalmente "annotation based"), sia per il consumo di risorse che, come vedremo, puó beneficiare della possibilitá di gestione asincrona di queste componenti.

La specifica JEE6, rilasciata ufficialmente alla fine del 2009 e via via concretamente implementata nel periodo a seguire, non introduce vere e proprie "nuove" tecnologie, se non l'adozionedi alcuni standard de facto, ma si concentra principalmente nel rendere meno complesso il processo di sviluppo per applicazioni web 2.0.

Tra le novitá è particolarmente interessante l'introduzione di API ed annotation per gestire nativamente servizi REST (senza cioè necessariamente dover ricorrere all'adozione di framework di terze parti), mentre tra i miglioramenti di rilievo vanno sicuramente citati gli aggiornamenti di alcune a librerie quali JPA 2.0 ed EJB 3.1, oltre naturalmente alla già citata nuova versione delle servlet, che analizzeremo brevemente in questo articolo.

Ovviamente per inziare a familiarizzare con le servlet 3 bisognerà controllare che la versione dell'IDE e del container che si utilizza sia conforme alle nuove specifiche, ed eventualmente aggiornare questi ambienti.

In linea di massima l'ultima versione di Eclipse o Netbeans (noi per gli esempi utilizzeremo Eclipse Indigo) dovrebbero andare più che bene, mentre di seguito forniamo una breve lista delle versioni dei più noti container (o application server) che introducono il supporto alle nuove specifiche.

Server compatibili

Annotazioni

Le servlet costituiscono fin dalle prime versioni delle foundation classes per applicazioni web scritte in java, nonché il fulcro fondamentale su cui si basano framework come Struts, Spring (web) o JSF.

La manutenzione dei vari necessari file di configurazione xml ha sempre costituito un elemento di problematicità per gli sviluppatori, ed un freno alla produttività: perchè allora non usare delle annotazioni?

Finalmente, anche le servlet (e quindi, filtri, listener, ...) possono utilizzarle, al tempo stesso mantenendo la possibilitá di usare (e quindi configurare dinamicamente) i relativi file di configurazione XML: in pratica gli sviluppatori hanno la possibilità di utilizzare il metodo che ritengono più utile al proprio stile di programmazione.

In concreto la nuova specifica ci consente di annotare le nostre classi con tre tipologie di nuove annotazioni:

  • @WebServlet propriamente per la configurazione di nuove servlet
  • @WebFilter per la configurazione di nuovi filtri
  • @WebListener per la configurazione dei listener (vedremo l'uso di un listener per ottimizzare le performance nei paragrafi seguenti)

Molto importante è sottolineare che tutto quanto detto per le servlet, vale anche indirettamente per le pagine JSP (che come sappiamo vengono tradotte di fatto in servlet esse stesse), tramite la definizione degli attributi (vecchi e nuovi) nella loro forma standard.

@WebServlet

L'uso di questa annotazione rimpiazza quello che veniva definito nel file di configurazione web.xml (il file, se presente sovrascriverá i valori dell'annotation), quindi, la url su cui viene mappato e i parametri di inizializzazione.

Inoltre, vengono introdotti nuovi campi da valorizzare, il piú importante dei quali è asyncSupported che segnala la possibilitá di definire una esecuzione asincrona della servlet. Questa è senz'altro la caratteristica piú importante della nuova specifica, di cui vedremo i vantaggi nel prossimo paragrafo.

package it.html.servlet3.sample;
...
/**
 * Servlet implementation class ServletSample
 */
@WebServlet(
		description = "A simple Sample",
		urlPatterns = { "/servletsample" },
		initParams = {
				@WebInitParam(name = "Param1", value = "Value1"),
				@WebInitParam(name = "Param2", value = "Value2")
		},
		asyncSupported= true)
public class ServletSample extends HttpServlet {
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		//...
	}
}

@WebFilter

I filtri sono delle classi di utilitá molto importanti in quanto consentono di creare un flusso di operazioni particolari senza modificare la struttura principale del nostro software.
Anch'essi possono beneficiare delle annotation, liberandoci dalla necessitá di configurare un apposito descrittore nel file web.xml. L'esempio che segue ci illustra ad esempio la creazione di un filtro associato alla servlet creata in precedenza:

package it.html.servlet3.sample;
...
@WebFilter(
dispatcherTypes = {DispatcherType.REQUEST },
initParams = {@WebInitParam(name = "param", value = "value")},
servletNames = { "ServletSample" })
public class FilterSample implements Filter {
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		//...
		chain.doFilter(request, response);
	}
	@Override
	public void init(FilterConfig fConfig) throws ServletException {
		//...
	}
	@Override
	public void destroy() {
		//...
	}
}

In questo caso stiamo associando l'esecuzione del filtro alla ricezione di una Request (annotando il parametro dispatcherTypes). Programmando opportunamente i metodi doFilter(...) (e init(...) e destroy(...), estesi dall'interfaccia) potremo implementare le funzionalitá desiderata; giacchè inoltre i filtri sono delle speciali servlet, sará possibile aggiungere le stesse annotazioni viste in precedenza (parametro asyncSupported incluso).

@WebListener

L'uso di classi di tipo listener consente una gestione di eventi legati alle azioni generate dal ciclo di vita della web application. È senza dubbio una maniera molto interessante di programmare delle applicazioni legando determinate azioni nel momento in cui qualcosa di nostro interesse accade (appunto, degli eventi). È possibile tracciare una serie di eventi legati a cambi che avvengono in sessione, o in generale nel ciclo di vita dell'intera applicazione e le sue componenti. L'esempio che segue è legato sempre al ciclo della request che abbiamo iniziato in precedenza:

package it.html.servlet3.sample;
...
@WebListener
public class ListenerSample implements ServletRequestListener {
    public ListenerSample() {}
    public void requestDestroyed(ServletRequestEvent arg0) {
        //...
    }
    public void requestInitialized(ServletRequestEvent arg0) {
        //...
    }
}

Qui, in realtá vediamo che l'annotation non ha particolari parametri da configurare in quanto i metodi che andremo a programmare sono quelli che guidano l'evento (richiesta inizializzata o terminata). Molto importante è infine ricordare di mantenere il costruttore di default che serve per l'inizializzazione della stessa classe (come bean) e la corretta gestione del ciclo di vita di questi componenti.

Performance

Fin dal principio la forza dello sviluppo in Java tramite servlet rispetto alle applicazioni native in CGI, era legata alla flessibilitá e “leggerezza” delle servlet e dei web server costruiti attorno a questa tecnologia: la gestione ottimizzata del ciclo di vita di questi componenti (persistenti in memoria, non dimentichiamolo) rendeva altamente reattivo e performante il server.

Senza voler entrare troppo nel dettaglio, ricordiamo brevemente che mentre una nuova applicazione CGI doveva essere creata ad ogni richiesta da parte di un utente, e quindi un pesante uso di memoria ed un “context switching” di tutto lo spazio di memoria allocato per quel servizio (nonchè una esplicitá gestione del multithreading), una servlet ha la possibilitá di un uso condiviso di risorse in memoria, quindi un “context switching” piú leggero e un sistema di pooling che rende il multithreading cucito sulle reali necessitá del servizio (e trasparente allo sviluppatore).

Per capire appieno i vantaggi introdotti dal nuovo modello asincrono delle servlet 3.0 è bene fare un rapido excursus del modello di comunicazione HTTP e la sua configurazione da parte dei web server.

Http 1.0 vs http 1.1

Inizialmente per il protocollo http 1.0, completamente stateless, per cui, ad ogni richiesta seguiva una risposta senza possibilitá di riutilizzare lo stesso canale per successive comunicazioni con lo stesso client.

Come ben sappiamo, una navigazione da parte di un browser apre decine e centinaia di richieste verso lo stesso server, quindi, un modello completamente stateless come quello appena citato è altamente inefficiente. Tale necessitá venne subito implementata nel protocollo http 1.1 che è quello ancora a tutt'oggi adottato da tutti i sistemi in internet, e che fa si che ogni richiesta venga mantenuta per un certo periodo di tempo limitando quindi i tempi di attesa dalla rinegoziazione della connessione TCP alle richieste successive alla prima.

Thread per connection model

L'adozione del protocollo http 1.1 portó alla creazione del modello noto come “thread per connection”, ossia, associare ad ogni richiesta http un thread. Questo significa che un web server mantiene un numero di thread pari al numero di connessioni che gli vengono aperte: sicuramente si tratta di un modello non proprio efficiente, in quanto la navigazione di un sito web da parte di un utente non segue un costante flusso informativo tra le due macchine (l'utente apre una pagina e rimane “idle” anche qualche minuto prima di interagire nuovamente col server). Quindi, la limitazione di questo modello sta sicuramente nella scalabilitá ridotta. Piú utenti significa piú risorse hardware (e tutto quello che comporta un eventuale clustering, replica di sessioni, connessioni a db, ...).

Thread per request model

La limitazione vista nel parágrafo precedente era dovuta a un vincolo tecnológico che rendeva impossibile aprire un socket, “staccarne” il listener (un thread) per fare altro lavoro e “riattaccarlo” nel momento in cui ci fosse stato bisogno di nuova comunicazione bidirezionale: le classi java.io infatti non permettono una gestione “non-blocking” degli stream di dati.

La versione java 1.4 (2002), porta con sè un nuovo modello di uso degli stream con il package java.nio (New Input Output) non bloccante, che ha la capacitá di gestire stream di dati e thread in maniera totalmente nuova e senza la limitazione di dover associare ad ogni socket un thread fisso.

Questo nuovo modello porta all'implementazione del paradigma “thread per request”, dove, un pool prefissato di thread viene riutilizzato su un numero non predefinito di connessioni (di tipo http1.1). Se arriva una richiesta, il web server prende un thread dal pool, esegue tale richiesta, da il risultato al cliente finale e rimette il thread nel pool, pronto ad una successiva esecuzione con qualsiasi altra connessione che lo necessiti.

Questo modello è ottimale e ha la capacitá di scalare in maniera non direttamente proporzionale alle connessioni ma alle reali richieste che queste fanno. I web server sviluppati negli ultimi anni (Tomcat, Jetty, Grizzly) adottano questo modello.

Nuovo modello proposto: asincrono

Il modello noto come “thread per request” ha funzionato eccellentemente, ma negli ultimi 2-3 anni l'introduzione di tecniche AJAX ne ha costantemente diminuito il reale valore. Le applicazioni web che oggi utilizziamo hanno un costante scambio di informazioni tra client e server in maniera asincrona e con servizi third party (non sotto il nostro controllo, quindi, potenzialmente lenti o inefficienti).

Quello che puó succedere quando sviluppiamo questo tipo di applicazioni è vedere le performance del nostro server diminuire in forma drastica spesso perchè i thread che si occupano di eseguire una richiesta sono lenti nell'esecuzione della propria attivitá (dovuta soprattutto ad una moltiplicazione di richieste AJAX, che sono pur sempre richieste HTTP, e quindi un numero di thread maggiori consumati da ogni utente).

La nuova specifica delle servlet 3.0 ci viene incontro con l'introduzione del processamento di richieste asincrono. Ormai abbiamo imparato che ci sono una serie di situazioni che beneficiano in maniera eccellente del paradigma di esecuzione asincrona. Vediamo quindi in maniera semplice e con un esempio come esso possa essere applicato all'esecuzione di una richiesta http.

Paradigma asíncrono ed un esempio pratico

Cerchiamo di suddividere logicamente gli elementi che concorrono nell'esecuzione di una richiesta verso un server. Finora abbiamo identificato le connessioni http 1.1 e i thread, che eseguono una determinata attivitá. Le connessioni possono essere disaccoppiate dai thread, grazie alla tecnologia NIO. Perchè non disaccoppiare l'attivitá, l'esecuzione del processo dal processo stesso? In una situazione in cui il processo è immediato non avrebbe molto senso, nè tantomento in una situazione in cui dobbiamo avere una risposta appena possibile. Ma, in un contesto AJAX based, dove l'attesa ed il ritardo di una risposta sono contemplato dall'utente (almeno in parte, ovviamente) è possibile approfittarne.

Le servlet 3.0 consentono proprio di aggiungere un livello ulteriore di granularitá agli elementi di sviluppo dell'applicazione, rendendo asincrona, l'esecuzione di un dato task (o attivitá).
I campi di applicazione per beneficiare di questa nuova funzionalitá sono veramente tanti, soprattutto nell'era delle applicazioni web 2.0: ad esempio nel caso della lettura di informazioni da un web service di terze parti, spesso intrinsecamente lento e consumato tramite AJAX: immaginando ritardi nell'esecuzione di richieste di questo tipo è facile comprendere come non ci sia più la necessità di thread "appesi" ad aspettare il risultato, prima di ritornare a far parte del pool a disposizione di altre richieste.

Utilizzando il parametro asincrono (asyncSupported=true) possiamo dire al container di lanciare l'esecuzione del thread e, prima ancora di restituire il risultato, restituire il thread al pool per nuove richieste. Quando il risultato sará disponibile, tramite una opportuna gestione degli eventi, potremo riutilizzare un nuovo thread per restituire la risposta. In pratica guadagneremo un livello ulteriore di disponibilità nella gestione multithread per eseguire altre richieste. L'esempio che segue illustra concretamente questa situazione:

package it.html.servlet3;
...
/**
 * Servlet implementation class AsyncServlet
 */
@WebServlet(urlPatterns={"/asyncservlet"}, asyncSupported=true)
public class AsyncServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	public static final String QUEUENAME = "slowProcess";
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		/*
		 * asyncSupported=true, quindi la coppia request/response
		 * viene eseguita (asincronamente) e il relativo oggetto (AsyncContext)
		 * messo in una coda. Il doGet restituisce quindi il flusso di
		 * esecuzione al browser
		 */
		AsyncContext actx = request.startAsync(request, response);
		ServletContext appScope = request.getServletContext();
		//aggiungiamo la richiesta alla coda di esecuzione globale di cui conosciamo il nome
		Object x = appScope.getAttribute(QUEUENAME);
		((Queue<AsyncContext>)x).add(actx);
	}
}

La coda request/response

Per poter sfruttare la logica asincrona faremo uso di una coda che recuperiamo dallo scope application con un attributo fisso (poi vedremo chi la istanzia). La nostra servlet, in questo semplice caso si deve occupare di creare un AsyncContext, un oggetto in cui vengono memorizzati request e response della richiesta originale e salvarlo in tale struttura dati (una coda in questo esempio per una logica FIFO, ma potrebbe essere qualsiasi struttura, in memoria o persistente).

AsyncContext, attraverso il nuovo metodo sull'oggetto ServletRequest (startAsync(...)), consente l'uscita dal metodo senza necessitá di fare il “commit” della risposta (che verrá fatto in un tempo futuro non noto a priori): tutto questo è possibile perchè abbiamo dato ordine al container di garantire un'esecuzione asincrona (attraverso la solita annotazione asyncSupported).

La risposta?

Per avere un flusso ottimo quello che facciamo è di creare una serie di consumer (o uno solo) di quella coda che si occuperá di evadere le richieste (nel momento in cui arrivano, con dei ritardi programmati, diciamo che la decisione puó dipendere da come volete implementare il servizio). Per fare ció, abbiamo bisogno di rimanere in qualche modo dentro il contesto del container e salvare nell'application scope una struttura dati che viene utilizzata dalla servlet. Una buona maniera per farlo potrebbe essere ad esempio usare un listener:

package it.html.servlet3;
...
/**
 * Application Lifecycle Listener implementation class SlowProcess
 *
 */
@WebListener
public class SlowProcess implements ServletContextListener {
	public SlowProcess(){}
    public void contextInitialized(ServletContextEvent sce) {
		//Inizializziamo una nuova coda di processi
		Queue<AsyncContext> jobQueue = new ConcurrentLinkedQueue<AsyncContext>();
		//e la definiamo come coda generale (a livello di servlet context)
		sce.getServletContext().setAttribute(AsyncServlet.QUEUENAME, jobQueue);
		//Avvio il processo di consumo e lascio il metodo
		new SlowProcessConsumer(jobQueue).start();
	}
    public void contextDestroyed(ServletContextEvent sce) {}
}

Chiamiamo allusivamente tale listener SlowProcess ed estendiamo l'interfaccia ServletContextListener, che è quella che gestisce gli eventi del ciclo di vita delle servlet. Quello che vogliamo fare è creare in fase di startup un consumer su tale coda che si occuperá di evadare le richieste e restituire un risultato al cliente finale.

L'esecuzione del metodo contextInitialized(...) avviene in fase di avvio dell'applicazione ed è semplicemente la creazione della coda e l'inserimento della stessa in un attributo noto (quello che avevamo recuperato nella servlet). Anche il metodo contextDestroyed(...) va implementato, ma nel nostro caso non ha nulla di interessante (in genere si usa per fare pulizia di risorse, nel nostro caso dovremmo fermare il thread che si avvia nell'inizializzazione - lo lasciamo come esercizio).

Il consumer

Ora vediamo cosa effettivamente fa il consumer (per semplicitá di gestione messo in un thread separato):

class SlowProcessConsumer extends Thread{
	//Il pool di thread che si occuperá dell'evasione delle richieste asincrone
	Executor executor = Executors.newFixedThreadPool(100);
	Queue<AsyncContext> jobQueue;
	public SlowProcessConsumer(Queue<AsyncContext> jobQueue) {
		this.jobQueue=jobQueue;
	}
	public void run(){
		//Il thread è incaricato di fare il dispatcher delle richieste in arrivo
		while(true){
			if(!jobQueue.isEmpty()){
				final AsyncContext actx = jobQueue.poll();
				System.out.println("Request incoming "+actx);
				executor.execute(new Runnable(){
					public void run(){
						/*
						 * Parametri di richiesta/risposta
						 * */
						System.out.println("Evaluating incoming request");
						ServletRequest request = actx.getRequest();
						ServletResponse response = actx.getResponse();
						try {
							//Simuliamo un ritardo di 5 secondi
							System.out.println("Sleeping 5 seconds...");
							Thread.sleep(5000);
							response.getWriter().println("Hello");
							response.getWriter().flush();
							actx.complete();
							System.out.println("Done!");
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				});
			}
		}
	}
}

Il nostro interesse sta nel metodo run(). Possiamo vedere che si tratta della simulazione di un "demone" (c'è un while(true) (che sarebbe bene rimpiazzare con un ritardo controllato!) che, all'atto di intercettare una richiesta recupera il corrispettivo AsyncContext, e lo passa ad un thread di un pool gestito per nostro conto. Tale thread (una classe anonima che implementa Runnable) simula il ritardo e subito dopo effettua un'operazione di scrittura in maniera classica sul flusso di output della pagina. Prima di terminare chiude in maniera definitiva quello che la servlet asincrona aveva aperto tramite il metodo complete() (in pratica: il commit della richiesta).

L'esempio che abbiamo presentato è servito per dare una idea dei componenti utilizzabili in una implementazione reale: una situazione molto piú complessa potrebbe ad esempio prevedere l'uso di tecnologia JMS, magari accoppiata con MessageDriven Beans o altre tecnologie.

Altri esempi di uso comune sono legati al modello publisher/subscriber per diversi utenti (e quindi diversi AsyncContext) che partecipano ad una stessa chat: il beneficio, in tali casi di diffusione in multicast di una informazione comune anche a livello di disegno dell'applicazione è evidente.

Applicare il paradigma Comet

Provando ad eseguire l'esempio dopo circa 5 secondi dalla richiesta il server risponde con il semplice saluto. Se peró immaginiamo di eseguire tali richieste da javascript (o jquery, ext-js, ...) per gestire una logica di interfaccia totalmente AJAX oriented, tutto cambia.

Quello che in gergo viene chiamato reverse Ajax o Comet è il processo di interazione che trae beneficio dal processamento asincrono sulle servlet 3.0.

Fino ad oggi, infatti, i diversi vendor di web application avevano sopperito all'assenza di un comune modello asincrono con la creazione di classi specifiche (come la CometProcessor di Tomcat), tutto ció riducendo la portabilitá del codice da un server ad un altro e non dando una reale soluzione architetturale al problema di un nuovo paradigma che si è andato definendo negli ultimi anni.

Ora, grazie alle servlet 3.0 tutto ciò diventa possibile, facendo ovviamente attenzione a non convertire l'intera applicazione in forma asincrona: se pure ha senso che alcune richieste (come quelle tipiche Ajax in stile Comet) vengano trattate con la nuova logica, la struttura portante dell'applicazione deve rimanere sincrona (ad esempio la fase di presentazione classica: richiesta pagina html – risposta), e vale un principio di buonsenso generale che ci invita a non abusare di questo nuovo paradigma forzandone l'applicazione anche dove non introduce valore aggiunto.

Approfondimenti


Ti consigliamo anche