Salta al contenuto
Note per Studenti Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni

Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni

In questa pagina 13

Con CGI e applicazioni web dinamicheLa Common Gateway Interface (CGI, RFC 3875) e' l'interfaccia fra un server web e un programma esterno che genera la risposta: per ogni richiesta il server fa fork ed exec del programma, gli passa i dati con le meta-variabili d'ambiente (REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH, CONTENT_TYPE, SCRIPT_NAME, PATH_INFO, SERVER_*, REMOTE_ADDR, HTTP_* per gli header) e con lo standard input (corpo della POST, CONTENT_LENGTH byte); il programma scrive sullo standard output header CGI (Content-Type obbligatorio, Status, Location), una riga vuota e il corpo, e il server li trasforma in una risposta HTTP completa; i punti delicati sono pipe e dup2, la chiusura delle estremita' inutilizzate, il timeout, i limiti di dimensione e la sicurezza (injection, path traversal, header Proxy, shellshock); il costo di un processo per richiesta ha portato a FastCGI, ai moduli del server e ai server applicativi.CGI e applicazioni web dinamiche → ogni richiesta avviava un nuovo processo. Una servlet (Java servlet) risolve lo stesso problema in modo più efficiente: è una classe Java che vive dentro un programma sempre in esecuzione, il servlet container, il quale riceve le richieste HTTP, le trasforma in oggetti Java, le consegna alla servlet e rimanda al client la risposta. Niente processo per richiesta: la servlet è già in memoria e ogni richiesta è servita da un thread. Lo standard è Jakarta Servlet (versione 6.0 per Tomcat 10.1, in precedenza Java Servlet con pacchetto javax.servlet; dalla versione 5 il pacchetto è jakarta.servlet).

Questa nota segue l'applicazione delle aule fino alla pagina HTML generata dal server (Web 1.0, Architettura delle applicazioni web - browser, server web e livelliUn'applicazione web è distribuita su più livelli (tier): presentazione (browser: HTML, CSS, JavaScript), logica applicativa (server web e application server) e dati (DBMS). Il client invia richieste HTTP, il server risponde; HTTP è senza stato, quindi lo stato dell'utente viaggia in cookie e sessioni. Il web server serve file statici, l'application server (container, per esempio Tomcat per le servlet Java) esegue codice che genera risposte dinamiche; spesso il web server fa da reverse proxy davanti all'application server. Il browser trasforma i byte della risposta in una pagina: decodifica, tokenizzazione e costruzione del DOM, CSS che diventa CSSOM, render tree, layout (reflow), paint e composizione; un script senza defer o async blocca il parsing, il CSS blocca il rendering. Origine = schema + host + porta; la same-origin policy limita ciò che uno script può leggere da altre origini. Web 1.0: pagine generate dal server, ogni azione ricarica la pagina; Web 2.0: AJAX, contenuti degli utenti, servizi REST; Web 3.0: dati con significato esplicito (RDF, linked data).Architettura delle applicazioni web - browser, server web e livelli →); la separazione tra logica e presentazione è in JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →. Gli strumenti per costruire e distribuire il progetto sono in Strumenti di sviluppo - git, Maven e DockerTre strumenti rendono un progetto web riproducibile e condivisibile. Git tiene la storia del codice: tre aree (cartella di lavoro, area di staging, repository), il commit è un'istantanea con un hash, il branch è un puntatore mobile a un commit, HEAD indica dove si è; il merge fonde due rami (fast-forward se non c'è divergenza, altrimenti un commit di merge) e può produrre conflitti, che si risolvono a mano nei marcatori di conflitto, poi add e commit; con un remoto si usano clone, fetch, pull (fetch + merge) e push, e un push rifiutato (fetch first) richiede prima un pull; in gruppo si lavora su rami di funzionalità con commit piccoli e main sempre funzionante. Maven descrive il progetto nel pom.xml (coordinate groupId:artifactId:version, packaging war, dipendenze con scope compile, provided, runtime, test), impone la struttura src/main/java, src/main/webapp, src/test, e il ciclo di vita validate, compile, test, package, verify, install, deploy: ogni fase esegue prima tutte le precedenti, mvn clean package produce target/aule.war. Docker confeziona l'applicazione: un'immagine (strati di sola lettura, costruita da un Dockerfile) è il modello, un container è un'istanza in esecuzione; per l'esempio un Dockerfile a due fasi compila il war con Maven e lo copia in Tomcat, e docker compose avvia insieme Tomcat e PostgreSQL con un volume per i dati.Strumenti di sviluppo - git, Maven e Docker →; richiami su classi, ereditarietà e eccezioni, in Python ma con gli stessi concetti, in Classi ed ereditarietà in PythonAttributi di istanza e di classe, metodi di istanza, di classe e statici; confronto e ordinamento con eq e lt; proprietà; ereditarietà, super(), override e polimorfismo; classi astratte come interfacce; eccezioni personalizzate; overloading e shadowing in Python.Classi ed ereditarietà in Python → e Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →.

Il container: Apache Tomcat

Apache Tomcat è il container (e web server) di riferimento per servlet e JSP; implementa Jakarta Servlet, Jakarta Pages (JSP) e altre specifiche. Ha tre componenti da conoscere:

Che cosa succede a una richiesta POST /prenotazioni:

browser ──HTTP──> Coyote ──> Catalina: sceglie l'applicazione dal context path
                                       sceglie la servlet dal mapping dell'URL
                              catena dei filtri (codifica, autenticazione...)
                              servlet.service(request, response)  ──> doPost(...)
                                       (in un thread del pool)
browser <──HTTP── Coyote <── risposta composta nell'oggetto response

Le applicazioni sono distribuite come war nella cartella webapps; ogni applicazione è caricata da un proprio class loader, per non mischiare le librerie di applicazioni diverse. Il context path è l'URL di base di un'applicazione: aule.war risponde sotto /aule, ROOT.war alla radice /.

Ciclo di vita di una servlet

Il container gestisce il ciclo di vita; l'oggetto servlet è creato dal container, mai con new.

Fase Metodo Quando Quante volte
caricamento e costruzione costruttore alla prima richiesta, oppure all'avvio se loadOnStartup ≥ 0 1
inizializzazione init() subito dopo la costruzione 1
servizio service(req, resp) per ogni richiesta, in un thread molte, anche in parallelo
distruzione destroy() allo spegnimento o al riavvio dell'applicazione 1
  • init() serve a preparare risorse costose (cercare il DAO, aprire file): se lancia un'eccezione la servlet non entra in servizio.
  • service() della classe base HttpServlet legge il metodo HTTP della richiesta e chiama doGet, doPost, doPut, doDelete, doHead, doOptions. Una servlet scritta bene ridefinisce (override) i soli doXxx che le servono; per un metodo non ridefinito HttpServlet risponde da solo 405 Method Not Allowed (400 Bad Request se il client parla HTTP/1.0).
  • destroy() rilascia ciò che init() ha acquisito.

Una servlet minima, con il mapping tramite annotazione:

java
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import java.io.IOException;

@WebServlet("/ciao")                              // risponde all'URL <contesto>/ciao
public class CiaoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");     // header Content-Type della risposta
        resp.getWriter().println("Ciao dal server");          // corpo
    }
}

Mapping: da URL a servlet

Il container sceglie la servlet confrontando il path (dopo il context path) con i pattern dichiarati, in quest'ordine di priorità:

Pattern Esempio Che cosa corrisponde
esatto /prenotazioni solo quel path
prefisso /api/prenotazioni/* quel path e tutto ciò che segue (/api/prenotazioni/7)
estensione *.jsp tutti i path che finiscono così
predefinito / tutto il resto (di solito la servlet che serve i file statici)

Vince la corrispondenza esatta, poi il prefisso più lungo, poi l'estensione, poi il predefinito. L'URL http://host:8080/aule/prenotazioni/annulla?id=3 si scompone per la servlet in:

Metodo Valore Significato
getContextPath() /aule applicazione
getServletPath() /prenotazioni/annulla parte che ha scelto la servlet (per un pattern a prefisso: solo il prefisso)
getPathInfo() null resto del path dopo servletPath, per i pattern a prefisso (/7 in /api/prenotazioni/7)
getQueryString() id=3 parte dopo ?

Lo stesso mapping si può dichiarare nel descrittore di distribuzione WEB-INF/web.xml (deployment descriptor), che prevale sulle annotazioni quando i due sono in conflitto:

xml
<servlet>
  <servlet-name>ciao</servlet-name>
  <servlet-class>CiaoServlet</servlet-class>
  <load-on-startup>1</load-on-startup>            <!-- creala all'avvio, non alla prima richiesta -->
</servlet>
<servlet-mapping>
  <servlet-name>ciao</servlet-name>
  <url-pattern>/ciao</url-pattern>
</servlet-mapping>

Le annotazioni tengono il mapping vicino al codice; il web.xml serve per ciò che non è una classe: pagina di benvenuto, sessioni, pagine di errore, ordine dei filtri. Quello dell'esempio:

xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
  <display-name>Prenotazione aule</display-name>

  <!-- pagina servita per l'URL della radice dell'applicazione -->
  <welcome-file-list>
    <welcome-file>login.html</welcome-file>
  </welcome-file-list>

  <!-- sessioni: 30 minuti, cookie JSESSIONID non leggibile da JavaScript -->
  <session-config>
    <session-timeout>30</session-timeout>
    <cookie-config>
      <http-only>true</http-only>
    </cookie-config>
  </session-config>

  <!-- pagine di errore al posto di quelle predefinite del container -->
  <error-page>
    <error-code>404</error-code>
    <location>/errore.html</location>
  </error-page>
  <error-page>
    <exception-type>java.lang.Throwable</exception-type>
    <location>/errore.html</location>
  </error-page>
</web-app>

La richiesta

HttpServletRequest espone tutto ciò che il client ha mandato (HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsingHTTP/0.9 (1991) ha una sola riga di richiesta "GET /percorso" e la risposta e' solo il documento HTML, senza codici di stato ne' header, e la connessione si chiude alla fine; HTTP/1.0 (RFC 1945) aggiunge Request-Line con versione, Status-Line con codice, header (generali, di richiesta, di risposta, di entita'), i metodi HEAD e POST, tipi di contenuto diversi dall'HTML; un messaggio e' riga iniziale, header "Nome: valore" a righe CRLF, riga vuota, corpo; in HTTP/1.0 il corpo della risposta finisce quando il server chiude la connessione, mentre per POST serve Content-Length; il parsing legge la prima riga, poi gli header fino alla riga vuota, poi il corpo; i nomi degli header non distinguono maiuscole e minuscole, e un client robusto accetta LF semplice e spazi variabili.HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsing →):

Metodo Restituisce
getMethod() GET, POST...
getParameter("nome") un parametro, null se manca; valori sia dalla query string sia dal corpo di un form application/x-www-form-urlencoded
getParameterValues("nome") tutti i valori di un parametro ripetuto (checkbox)
getHeader("Content-Type") un header; getCookies() i cookie
getReader() / getInputStream() il corpo grezzo (per JSON, Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →)
setAttribute / getAttribute oggetti che accompagnano questa richiesta, per passarli a un'altra risorsa (JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →)

Tre regole pratiche:

  1. Tutto ciò che arriva è testo e non fidato. Un parametro è sempre String (o null): si convertono con Integer.parseInt gestendo NumberFormatException e si validano formato e intervalli. Mancare il controllo significa errori 500 o, peggio, falle di sicurezza.
  2. req.setCharacterEncoding("UTF-8") va chiamato prima di leggere i parametri; altrimenti "è" diventa "è". Per non dimenticarlo si fa in un filtro (sotto).
  3. Un form POST ha nel corpo aula_id=1&giorno=2026-10-19&ora_inizio=09%3A00: la codifica percentuale è trattata in Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →; getParameter la decodifica.

GET e POST. GET è sicuro e idempotente (nessun effetto collaterale, ripetibile, memorizzabile in cache, parametri nell'URL); POST cambia lo stato del server, e il browser avvisa se si ricarica. Perciò le pagine che leggono usano doGet, quelle che modificano doPost (mai una modifica con GET: un crawler o un'anteprima la eseguirebbe).

La risposta

HttpServletResponse si compila in ordine: prima stato e header, poi il corpo. Appena il corpo comincia a partire (o il buffer, di solito 8 KB, si riempie) la risposta è committed e stato e header non si possono più cambiare.

Metodo Effetto
setStatus(int) codice di stato (SC_OK, SC_CREATED...)
setHeader(nome, valore) / addHeader header (il secondo ne aggiunge uno ripetuto)
setContentType("text/html;charset=UTF-8") Content-Type e codifica del testo
getWriter() PrintWriter per testo; getOutputStream() per dati binari (non entrambi)
sendRedirect(url) risposta 302 Found con Location: url (Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →); il browser rifà una GET su quell'URL
sendError(codice, messaggio) risposta di errore con la pagina di errore del container

Se una servlet non imposta nulla la risposta è 200 OK con corpo vuoto.

Un esempio completo: le prenotazioni

Il form HTML dell'applicazione delle aule fa POST verso la servlet; la servlet, passando dal DAO, controlla e salva, e risponde con un redirect. La GET mostra l'elenco del giorno e il form. Per ora l'HTML è scritto a mano nella servlet; con JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller → passerà a una pagina JSP.

java
package it.unipd.aule.web;

import it.unipd.aule.dao.AulaDAO;
import it.unipd.aule.dao.PrenotazioneDAO;
import it.unipd.aule.dao.PrenotazioneException;
import it.unipd.aule.model.Aula;
import it.unipd.aule.model.Prenotazione;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;

/** Versione "Web 1.0 senza MVC": la servlet fa tutto, anche scrivere l'HTML. */
@WebServlet("/prenotazioni")
public class PrenotazioniServlet extends HttpServlet {
    private AulaDAO aulaDAO;
    private PrenotazioneDAO prenotazioneDAO;

    @Override
    public void init() throws ServletException {                 // una volta sola, alla creazione dell'istanza
        aulaDAO = (AulaDAO) getServletContext().getAttribute("aulaDAO");
        prenotazioneDAO = (PrenotazioneDAO) getServletContext().getAttribute("prenotazioneDAO");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String giorno = req.getParameter("giorno");               // facoltativo: ?giorno=2026-10-20
        if (giorno == null || giorno.isBlank()) {
            giorno = LocalDate.now().toString();
        }
        try {
            LocalDate.parse(giorno);
        } catch (DateTimeParseException e) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "giorno non valido");
            return;
        }
        String studente = (String) req.getSession().getAttribute("studente");
        try {
            resp.setContentType("text/html;charset=UTF-8");
            PrintWriter out = resp.getWriter();
            out.println("<!DOCTYPE html><html lang=\"it\"><head><meta charset=\"utf-8\"><title>Prenotazioni</title></head><body>");
            out.println("<h1>Prenotazioni del " + Html.esc(giorno) + "</h1>");
            out.println("<p>Ciao, " + Html.esc(studente) + ".</p>");
            out.println("<table><tr><th>Aula</th><th>Dalle</th><th>Alle</th><th>Studente</th></tr>");
            for (Prenotazione p : prenotazioneDAO.lista(null, giorno)) {
                out.println("<tr><td>" + p.getAulaId() + "</td><td>" + p.getOraInizio() + "</td><td>"
                        + p.getOraFine() + "</td><td>" + Html.esc(p.getStudente()) + "</td></tr>");
            }
            out.println("</table>");
            out.println("<form method=\"post\" action=\"" + req.getContextPath() + "/prenotazioni\">");
            out.println("<select name=\"aula_id\">");
            for (Aula a : aulaDAO.tutte()) {
                out.println("<option value=\"" + a.getId() + "\">" + Html.esc(a.getNome()) + "</option>");
            }
            out.println("</select>");
            out.println("<input type=\"date\" name=\"giorno\" value=\"" + Html.esc(giorno) + "\" required>");
            out.println("<input type=\"time\" name=\"ora_inizio\" required>");
            out.println("<input type=\"time\" name=\"ora_fine\" required>");
            out.println("<button>Prenota</button></form></body></html>");
        } catch (SQLException e) {
            throw new ServletException("errore di accesso ai dati", e);   // il container risponde 500
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Prenotazione p = new Prenotazione();
        try {
            p.setAulaId(Integer.parseInt(req.getParameter("aula_id")));
        } catch (NumberFormatException e) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "aula non valida");
            return;
        }
        p.setGiorno(req.getParameter("giorno"));
        p.setOraInizio(req.getParameter("ora_inizio"));
        p.setOraFine(req.getParameter("ora_fine"));
        p.setStudente((String) req.getSession().getAttribute("studente"));   // dalla sessione, non dal form
        try {
            p.valida();
            prenotazioneDAO.crea(p);
            resp.sendRedirect(req.getContextPath() + "/prenotazioni?giorno=" + p.getGiorno());   // Post/Redirect/Get
        } catch (IllegalArgumentException e) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
        } catch (PrenotazioneException e) {
            int stato = e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? 409 : 422;
            resp.sendError(stato, e.getMessage());
        } catch (SQLException e) {
            throw new ServletException("errore di accesso ai dati", e);
        }
    }
}

Punti da osservare:

La pagina login.html e la servlet LoginServlet (sezione sulle sessioni) completano il giro.

Thread e concorrenza

Il container crea una sola istanza della servlet e la fa eseguire da molti thread insieme, uno per richiesta concorrente. Conseguenza: i campi di istanza sono condivisi da tutte le richieste (Thread e concorrenza - modelli, Pthreads e goroutineUn thread è un flusso di esecuzione dentro un processo: ha program counter, registri e stack propri ma condivide codice, dati, heap e file aperti del processo, quindi costa meno di un processo (creazione e commutazione) e comunica più facilmente, ma i dati condivisi producono race condition; concorrenza (più compiti che avanzano, anche su un solo core) non è parallelismo (più compiti eseguiti nello stesso istante, serve più di un core); si distinguono parallelismo sui dati e sui compiti e la legge di Amdahl dà lo speedup massimo 1/(S+(1-S)/N) con S frazione seriale; i thread utente sono gestiti da una libreria, quelli kernel dal sistema, e i modelli many-to-one, one-to-one (Linux, Windows) e many-to-many li collegano; in C si usa Pthreads (pthread_create, pthread_join, pthread_exit), in Go le goroutine (parola chiave go, sync.WaitGroup, canali); questioni aperte: fork/exec con i thread, segnali, cancellazione (asincrona o differita), thread-local storage, contention scope (PCS e SCS).Thread e concorrenza - modelli, Pthreads e goroutine →, Sincronizzazione - sezione critica, race condition e mutexQuando più processi o thread accedono agli stessi dati e il risultato dipende dall'ordine degli accessi si ha una race condition (esempio: counter++ e counter-- sono tre istruzioni macchina ciascuna, e interleaving diversi danno 4, 5 o 6 partendo da 5); il problema della sezione critica chiede un protocollo (entry section, sezione critica, exit section, remainder section) che garantisca mutua esclusione, progresso e attesa limitata; disabilitare gli interrupt non scala sui multiprocessori, l'alternanza stretta con turn viola il progresso, la soluzione di Peterson (flag[2] e turn) soddisfa i tre requisiti ma può fallire su hardware moderno per il riordino delle istruzioni, per cui servono barriere di memoria; l'hardware offre test_and_set e compare_and_swap (atomiche) e variabili atomiche; il mutex lock (acquire/release, in POSIX pthread_mutex_t) è lo strumento di alto livello, con attesa attiva se è uno spinlock oppure con sospensione del thread se è un mutex di sistema; bloccare due volte lo stesso mutex senza sbloccarlo causa deadlock.Sincronizzazione - sezione critica, race condition e mutex →).

java
public class SbagliataServlet extends HttpServlet {
    private String studente;                         // SBAGLIATO: un solo campo per tutti gli utenti
    private int visite = 0;                          // SBAGLIATO: visite++ non è atomico

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        studente = req.getParameter("nome");         // un altro thread può sovrascriverlo qui...
        visite++;
        resp.getWriter().println("Ciao " + studente);   // ...e questa richiesta saluta la persona sbagliata
    }
}

Due difetti: dato dell'utente sbagliato (la richiesta A scrive studente, la richiesta B lo riscrive, A risponde con il nome di B: oltre al guasto è una fuga di dati tra utenti) e aggiornamento perso (visite++ legge, somma e riscrive: due thread possono leggere lo stesso valore e perdere un incremento). Le regole:

  1. lo stato della richiesta sta in variabili locali (o negli attributi della richiesta), mai in campi;
  2. i campi servono solo per ciò che è condiviso per scelta, e devono essere immutabili o thread-safe (AtomicInteger, collezioni concorrenti, o accessi dentro synchronized);
  3. mai condividere tra richieste una Connection JDBC o un ResultSet: ogni richiesta prende dal pool la propria connessione e la restituisce.

La versione corretta:

java
package it.unipd.aule.web;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

/** Conta le visite alla pagina: l'unico stato condiviso tra le richieste è un contatore atomico. */
@WebServlet("/visite")
public class ContatoreServlet extends HttpServlet {
    private final AtomicInteger visite = new AtomicInteger();    // campo condiviso da TUTTI i thread: deve essere thread-safe

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        int questa = visite.incrementAndGet();                    // lettura + incremento in un solo passo indivisibile
        String nome = req.getParameter("nome");                   // dato della richiesta: variabile LOCALE, mai un campo
        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().println("Visita numero " + questa + (nome == null ? "" : " di " + nome));
    }
}

I DAO dell'esempio hanno solo un campo final, il DataSource, che è thread-safe, quindi un'unica istanza serve tutti i thread. Gli attributi di sessione e di applicazione sono anch'essi condivisi (due schede dello stesso browser lavorano nella stessa sessione): se un oggetto messo in sessione viene modificato, serve sincronizzazione.

Rimane la concorrenza nel database: due richieste che prenotano la stessa aula nello stesso orario passano entrambe il controllo e inseriscono due prenotazioni sovrapposte. Si risolve con una transazione e un blocco, come nel DAO più sotto.

HTTP è senza stato (HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsingHTTP/0.9 (1991) ha una sola riga di richiesta "GET /percorso" e la risposta e' solo il documento HTML, senza codici di stato ne' header, e la connessione si chiude alla fine; HTTP/1.0 (RFC 1945) aggiunge Request-Line con versione, Status-Line con codice, header (generali, di richiesta, di risposta, di entita'), i metodi HEAD e POST, tipi di contenuto diversi dall'HTML; un messaggio e' riga iniziale, header "Nome: valore" a righe CRLF, riga vuota, corpo; in HTTP/1.0 il corpo della risposta finisce quando il server chiude la connessione, mentre per POST serve Content-Length; il parsing legge la prima riga, poi gli header fino alla riga vuota, poi il corpo; i nomi degli header non distinguono maiuscole e minuscole, e un client robusto accetta LF semplice e spazi variabili.HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsing →): per ricordare l'utente fra una richiesta e l'altra servono le sessioni. L'idea:

  1. il server crea una sessione (un oggetto HttpSession nella sua memoria) con un identificatore casuale e lungo;
  2. lo manda al browser in un cookie;
  3. il browser rimanda il cookie a ogni richiesta; il server ritrova la sessione dall'identificatore.

I dati (nome, carrello) restano sul server; nel cookie c'è solo la chiave. In Tomcat il cookie si chiama JSESSIONID.

Un cookie è una coppia nome=valore che il server manda con l'header Set-Cookie e il browser restituisce con l'header Cookie nelle richieste successive allo stesso sito. Gli attributi (RFC 6265, con SameSite ora nei browser) ne regolano l'uso:

Attributo Significato
Path=/ il cookie è inviato solo per URL sotto questo percorso
Domain domini a cui inviarlo (senza, solo l'host che l'ha impostato)
Max-Age / Expires durata; senza, il cookie scompare alla chiusura del browser (cookie di sessione)
Secure solo su HTTPS
HttpOnly non leggibile da JavaScript (document.cookie): protegge l'identificatore da un XSS
SameSite=Lax / Strict non inviato nelle richieste provenienti da altri siti: contrasta il CSRF

Un login nell'esempio, a livello di messaggi HTTP (Tomcat omette la frase di motivazione, facoltativa in HTTP/1.1):

http
POST /login HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

studente=Rossi
http
HTTP/1.1 302
Set-Cookie: JSESSIONID=9D2F0C5A6B7E4D1F8A3C2B1E0F9D8C7B; Path=/; HttpOnly
Location: /prenotazioni
Content-Length: 0
http
GET /prenotazioni HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=9D2F0C5A6B7E4D1F8A3C2B1E0F9D8C7B

Il browser segue il 302 e, nella GET, restituisce il cookie: la servlet riconosce la sessione e sa che lo studente è Rossi.

HttpSession

java
package it.unipd.aule.web;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/** Riceve il form di login.html (POST) e ricorda lo studente nella sessione. */
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studente = req.getParameter("studente");
        if (studente == null || studente.isBlank() || studente.length() > 40) {
            resp.sendRedirect(req.getContextPath() + "/login.html?errore=1");
            return;
        }
        if (req.getSession(false) != null) {
            req.changeSessionId();                 // nuovo identificatore: evita la session fixation
        }
        HttpSession sessione = req.getSession();   // la crea se manca: ora parte il cookie JSESSIONID
        sessione.setAttribute("studente", studente.trim());
        sessione.setMaxInactiveInterval(30 * 60);  // scade dopo 30 minuti senza richieste
        resp.sendRedirect(req.getContextPath() + "/prenotazioni");
    }
}
Metodo Effetto
req.getSession() restituisce la sessione, creandola se non c'è (e quindi mandando Set-Cookie)
req.getSession(false) restituisce la sessione o null, senza crearla
setAttribute / getAttribute / removeAttribute dati della sessione (oggetti Java, lato server)
setMaxInactiveInterval(secondi) scadenza dopo l'inattività (in web.xml session-timeout in minuti)
invalidate() termina la sessione (logout)
req.changeSessionId() assegna un nuovo identificatore mantenendo i dati

Aspetti di sicurezza:

Filtri

Un filtro (filter) si inserisce prima (e dopo) una o più servlet: vede e può modificare richiesta e risposta, oppure bloccare la richiesta. Implementa Filter (o estende HttpFilter) e chiama chain.doFilter(req, resp) per passare al filtro successivo e, in fondo, alla servlet. Il codice dopo doFilter viene eseguito al ritorno, quando la servlet ha già risposto.

richiesta ──> filtro 1 ──> filtro 2 ──> servlet
risposta  <── filtro 1 <── filtro 2 <──

Usi tipici: impostare la codifica, controllare l'autenticazione, registrare nel log tempi e accessi, comprimere. Due filtri dell'esempio:

java
package it.unipd.aule.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/** Imposta UTF-8 sul corpo delle richieste, prima che una servlet legga i parametri (altrimenti "è" diventa "è"). */
@WebFilter("/*")
public class CodificaFilter extends HttpFilter {
    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain catena)
            throws IOException, ServletException {
        req.setCharacterEncoding("UTF-8");
        catena.doFilter(req, resp);          // passa la richiesta al filtro successivo, o alla servlet
    }
}
java
package it.unipd.aule.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

/** Lascia passare solo chi ha già indicato il proprio nome (salvato nella sessione); gli altri vanno al login. */
@WebFilter("/prenotazioni/*")
public class AutenticazioneFilter extends HttpFilter {
    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain catena)
            throws IOException, ServletException {
        HttpSession sessione = req.getSession(false);            // false: non creare la sessione se manca
        if (sessione == null || sessione.getAttribute("studente") == null) {
            resp.sendRedirect(req.getContextPath() + "/login.html");   // la catena si interrompe qui
            return;
        }
        catena.doFilter(req, resp);
    }
}

@WebFilter("/prenotazioni/*") lo applica a quel prefisso; il filtro di autenticazione non fa chain.doFilter se manca lo studente nella sessione, e risponde da solo con un redirect: la servlet non viene nemmeno chiamata. Con più filtri sullo stesso URL l'ordine delle annotazioni non è definito: se conta (la codifica prima di tutti) si dichiara l'ordine con gli elementi filter-mapping del web.xml.

Struttura del war e di Tomcat

Il war (Web ARchive) è uno zip con una struttura fissa, creato da Maven (Strumenti di sviluppo - git, Maven e DockerTre strumenti rendono un progetto web riproducibile e condivisibile. Git tiene la storia del codice: tre aree (cartella di lavoro, area di staging, repository), il commit è un'istantanea con un hash, il branch è un puntatore mobile a un commit, HEAD indica dove si è; il merge fonde due rami (fast-forward se non c'è divergenza, altrimenti un commit di merge) e può produrre conflitti, che si risolvono a mano nei marcatori di conflitto, poi add e commit; con un remoto si usano clone, fetch, pull (fetch + merge) e push, e un push rifiutato (fetch first) richiede prima un pull; in gruppo si lavora su rami di funzionalità con commit piccoli e main sempre funzionante. Maven descrive il progetto nel pom.xml (coordinate groupId:artifactId:version, packaging war, dipendenze con scope compile, provided, runtime, test), impone la struttura src/main/java, src/main/webapp, src/test, e il ciclo di vita validate, compile, test, package, verify, install, deploy: ogni fase esegue prima tutte le precedenti, mvn clean package produce target/aule.war. Docker confeziona l'applicazione: un'immagine (strati di sola lettura, costruita da un Dockerfile) è il modello, un container è un'istanza in esecuzione; per l'esempio un Dockerfile a due fasi compila il war con Maven e lo copia in Tomcat, e docker compose avvia insieme Tomcat e PostgreSQL con un volume per i dati.Strumenti di sviluppo - git, Maven e Docker →):

aule.war
├── login.html, prenota.html, js/...     file statici, serviti così come sono
├── META-INF/context.xml                 configurazione Tomcat: il DataSource
└── WEB-INF/                             MAI servita direttamente ai client
    ├── web.xml                          descrittore di distribuzione
    ├── classes/it/unipd/aule/...        le classi compilate (servlet, DAO, modello)
    ├── lib/gson-2.11.0.jar, ...         le librerie dell'applicazione
    └── jsp/aule.jsp, prenotazioni.jsp   le viste: raggiungibili solo con un forward

Tutto ciò che sta fuori da WEB-INF è pubblico; dentro WEB-INF il container non serve nulla ai client: ecco perché le JSP delle viste vanno là. Cartelle di Tomcat da riconoscere:

Cartella Contenuto
bin script di avvio (startup, catalina)
conf server.xml (connettori, porta 8080), web.xml predefinito, context.xml
lib librerie condivise da Tomcat e da tutte le applicazioni (qui il driver JDBC)
webapps le applicazioni (ROOT.war, aule.war); Tomcat decomprime il war in una cartella omonima
logs catalina.out, localhost_access_log
work le JSP tradotte e compilate

Un nuovo war in webapps viene distribuito (deployed) automaticamente.

Accesso al database

I concetti di JDBC (connessione, PreparedStatement, parametri, transazioni, SQL injection) sono in Organizzazione fisica dei dati e accesso programmatico alle basi di datiUn DBMS è formato da un gestore delle connessioni, un parser, un ottimizzatore che sceglie il piano di esecuzione usando le statistiche del catalogo, un esecutore, i metodi di accesso (heap, ordinato, hash, B-tree, indici), il buffer manager e il gestore delle transazioni (lock e log). Il costo si misura in accessi a blocchi del disco. L'ottimizzatore riscrive l'espressione algebrica (selezioni e proiezioni il prima possibile, prodotto con selezione = join, ordine dei join) e sceglie gli algoritmi: scansione o indice per le selezioni, nested loop, block nested loop, index nested loop, sort-merge o hash join per i join (con 1000 e 100 blocchi, 10 pagine di buffer: 11 mila accessi con block nested loop, 3300 con hash join). La progettazione fisica sceglie organizzazione dei file, indici ed eventuale denormalizzazione in base al carico. Le applicazioni accedono ai dati con SQL incorporato, interfacce a livello di chiamata (ODBC, JDBC, DB-API di Python), ORM o routine memorizzate: connessione, cursore, execute con parametri, fetch, commit o rollback. Mai concatenare l'input dell'utente nell'SQL (SQL injection): query parametrizzate. Un'applicazione web è a tre livelli: browser, server applicativo (HTTP senza stato, sessioni e cookie) e DBMS.Organizzazione fisica dei dati e accesso programmatico alle basi di dati →: qui si vede come stanno in un'applicazione web.

Il pool di connessioni. Aprire una connessione al DBMS costa (autenticazione, rete): una per richiesta non regge. Il container tiene un pool di connessioni aperte e le presta; conn.close() non chiude davvero, restituisce la connessione al pool. L'oggetto che le consegna è un javax.sql.DataSource (in Jakarta EE 10 ancora nel pacchetto javax.sql, parte di Java SE). Lo si dichiara in META-INF/context.xml, con il driver nella cartella lib di Tomcat:

xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <!-- DataSource con pool di connessioni: l'applicazione lo cerca con java:comp/env/jdbc/aule -->
  <Resource name="jdbc/aule" auth="Container" type="javax.sql.DataSource"
            driverClassName="org.postgresql.Driver"
            url="jdbc:postgresql://db:5432/aule"
            username="aule" password="aule"
            maxTotal="10" maxIdle="5" maxWaitMillis="5000"
            validationQuery="SELECT 1" testOnBorrow="true"/>
</Context>

JNDI. L'applicazione non conosce URL e password: chiede al container la risorsa per nome con il Java Naming and Directory Interface (java:comp/env/jdbc/aule). Si fa una volta, all'avvio, in un ServletContextListener, che riceve due eventi (contextInitialized, contextDestroyed) e prepara i DAO condivisi:

java
package it.unipd.aule;

import it.unipd.aule.dao.AulaDAO;
import it.unipd.aule.dao.PrenotazioneDAO;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

/** Eseguito una volta sola all'avvio dell'applicazione: cerca il DataSource in JNDI e crea i DAO condivisi. */
@WebListener
public class AvvioListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent evento) {
        try {
            DataSource ds = (DataSource) new InitialContext().lookup("java:comp/env/jdbc/aule");
            ServletContext ctx = evento.getServletContext();
            ctx.setAttribute("aulaDAO", new AulaDAO(ds));
            ctx.setAttribute("prenotazioneDAO", new PrenotazioneDAO(ds));
        } catch (NamingException e) {
            throw new IllegalStateException("DataSource jdbc/aule non configurato in context.xml", e);
        }
    }
}

Il DAO. Il Data Access Object concentra in una classe tutto l'SQL di una tabella e restituisce oggetti Java (JavaBean): le servlet non vedono mai Connection, ResultSet o SQL. Cambiare database o ottimizzare una query tocca una sola classe. Il modello è fatto di JavaBean con costruttore vuoto, campi privati e getter/setter:

java
package it.unipd.aule.model;

/** Una riga della tabella aula(id, nome, posti): un JavaBean (costruttore vuoto, getter e setter). */
public class Aula {
    private int id;
    private String nome;
    private int posti;

    public Aula() { }

    public Aula(int id, String nome, int posti) {
        this.id = id;
        this.nome = nome;
        this.posti = posti;
    }

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getNome() { return nome; }
    public void setNome(String nome) { this.nome = nome; }
    public int getPosti() { return posti; }
    public void setPosti(int posti) { this.posti = posti; }
}
java
package it.unipd.aule.dao;

import it.unipd.aule.model.Aula;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/** Data Access Object della tabella aula: tutto l'SQL sulle aule sta qui e in nessun altro punto. */
public class AulaDAO {
    private final DataSource ds;

    public AulaDAO(DataSource ds) {
        this.ds = ds;
    }

    public List<Aula> tutte() throws SQLException {
        String sql = "SELECT id, nome, posti FROM aula ORDER BY nome";
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            List<Aula> aule = new ArrayList<>();
            while (rs.next()) {
                aule.add(new Aula(rs.getInt("id"), rs.getString("nome"), rs.getInt("posti")));
            }
            return aule;
        }
    }

    /** Restituisce l'aula, oppure null se l'id non esiste. */
    public Aula perId(int id) throws SQLException {
        String sql = "SELECT id, nome, posti FROM aula WHERE id = ?";
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? new Aula(rs.getInt("id"), rs.getString("nome"), rs.getInt("posti")) : null;
            }
        }
    }

    /** Aule con almeno "minimo" posti, in ordine di nome. */
    public List<Aula> conPostiAlmeno(int minimo) throws SQLException {
        String sql = "SELECT id, nome, posti FROM aula WHERE posti >= ? ORDER BY nome";
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setInt(1, minimo);
            try (ResultSet rs = ps.executeQuery()) {
                List<Aula> aule = new ArrayList<>();
                while (rs.next()) {
                    aule.add(new Aula(rs.getInt("id"), rs.getString("nome"), rs.getInt("posti")));
                }
                return aule;
            }
        }
    }
}
  • try (...) con più risorse (try-with-resources) chiude in ordine inverso ResultSet, PreparedStatement e Connection anche in caso di eccezione; per la Connection del pool vuol dire "restituisci".
  • La query è una stringa costante; i valori passano da ? e setInt: niente concatenazioni, niente SQL injection.
  • SQLException è un'eccezione verificata (checked): il DAO la dichiara, la servlet decide che cosa farne (di solito 500).
  • perId restituisce null se la riga non c'è: la servlet traduce in 404.

Il DAO delle prenotazioni ha in più il filtro per aula e giorno e le scritture:

java
package it.unipd.aule.dao;

import it.unipd.aule.model.Prenotazione;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

/** Data Access Object della tabella prenotazione. */
public class PrenotazioneDAO {
    private static final String COLONNE = "id, aula_id, giorno, ora_inizio, ora_fine, studente";

    private final DataSource ds;

    public PrenotazioneDAO(DataSource ds) {
        this.ds = ds;
    }

    /** Elenco filtrato: aulaId e giorno sono facoltativi (null = nessun filtro). */
    public List<Prenotazione> lista(Integer aulaId, String giorno) throws SQLException {
        StringBuilder sql = new StringBuilder("SELECT " + COLONNE + " FROM prenotazione WHERE 1 = 1");
        List<Object> parametri = new ArrayList<>();
        if (aulaId != null) {
            sql.append(" AND aula_id = ?");
            parametri.add(aulaId);
        }
        if (giorno != null) {
            sql.append(" AND giorno = ?");
            parametri.add(Date.valueOf(LocalDate.parse(giorno)));
        }
        sql.append(" ORDER BY giorno, aula_id, ora_inizio");   // solo costanti nel testo SQL, i valori sono parametri
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement(sql.toString())) {
            for (int i = 0; i < parametri.size(); i++) {
                ps.setObject(i + 1, parametri.get(i));
            }
            try (ResultSet rs = ps.executeQuery()) {
                List<Prenotazione> risultato = new ArrayList<>();
                while (rs.next()) {
                    risultato.add(daRiga(rs));
                }
                return risultato;
            }
        }
    }

    /** Tutte le prenotazioni di uno studente, dalla più recente. */
    public List<Prenotazione> perStudente(String studente) throws SQLException {
        String sql = "SELECT " + COLONNE + " FROM prenotazione WHERE studente = ? ORDER BY giorno DESC, ora_inizio";
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, studente);
            try (ResultSet rs = ps.executeQuery()) {
                List<Prenotazione> risultato = new ArrayList<>();
                while (rs.next()) {
                    risultato.add(daRiga(rs));
                }
                return risultato;
            }
        }
    }

    /** Restituisce la prenotazione, oppure null se l'id non esiste. */
    public Prenotazione perId(int id) throws SQLException {
        String sql = "SELECT " + COLONNE + " FROM prenotazione WHERE id = ?";
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setInt(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? daRiga(rs) : null;
            }
        }
    }

    /** Elimina la prenotazione. Restituisce false se non esisteva. */
    public boolean elimina(int id) throws SQLException {
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement("DELETE FROM prenotazione WHERE id = ?")) {
            ps.setInt(1, id);
            return ps.executeUpdate() == 1;
        }
    }

    /**
     * Inserisce la prenotazione se l'aula esiste ed è libera in quell'orario. Controllo e inserimento stanno
     * nella stessa transazione, e la riga dell'aula è bloccata (FOR UPDATE) perché due richieste
     * contemporanee sulla stessa aula non passino entrambe il controllo.
     * Imposta l'id generato sull'oggetto ricevuto e lo restituisce.
     */
    public Prenotazione crea(Prenotazione p) throws SQLException, PrenotazioneException {
        try (Connection c = ds.getConnection()) {
            c.setAutoCommit(false);
            try {
                if (!bloccaAula(c, p.getAulaId())) {
                    throw new PrenotazioneException(PrenotazioneException.Tipo.AULA_INESISTENTE);
                }
                if (siSovrappone(c, p)) {
                    throw new PrenotazioneException(PrenotazioneException.Tipo.SOVRAPPOSIZIONE);
                }
                p.setId(inserisci(c, p));
                c.commit();
                return p;
            } catch (SQLException | PrenotazioneException | RuntimeException e) {
                c.rollback();
                throw e;
            }
        }
    }

    private boolean bloccaAula(Connection c, int aulaId) throws SQLException {
        try (PreparedStatement ps = c.prepareStatement("SELECT id FROM aula WHERE id = ? FOR UPDATE")) {
            ps.setInt(1, aulaId);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next();
            }
        }
    }

    /** Due intervalli [a,b) e [c,d) si sovrappongono se e solo se a < d e c < b. */
    private boolean siSovrappone(Connection c, Prenotazione p) throws SQLException {
        String sql = "SELECT COUNT(*) FROM prenotazione "
                   + "WHERE aula_id = ? AND giorno = ? AND ora_inizio < ? AND ora_fine > ?";
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setInt(1, p.getAulaId());
            ps.setDate(2, Date.valueOf(LocalDate.parse(p.getGiorno())));
            ps.setTime(3, Time.valueOf(LocalTime.parse(p.getOraFine())));     // ora_inizio esistente < nuova fine
            ps.setTime(4, Time.valueOf(LocalTime.parse(p.getOraInizio())));   // ora_fine esistente > nuovo inizio
            try (ResultSet rs = ps.executeQuery()) {
                rs.next();
                return rs.getInt(1) > 0;
            }
        }
    }

    private int inserisci(Connection c, Prenotazione p) throws SQLException {
        String sql = "INSERT INTO prenotazione (aula_id, giorno, ora_inizio, ora_fine, studente) VALUES (?, ?, ?, ?, ?)";
        try (PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            ps.setInt(1, p.getAulaId());
            ps.setDate(2, Date.valueOf(LocalDate.parse(p.getGiorno())));
            ps.setTime(3, Time.valueOf(LocalTime.parse(p.getOraInizio())));
            ps.setTime(4, Time.valueOf(LocalTime.parse(p.getOraFine())));
            ps.setString(5, p.getStudente());
            ps.executeUpdate();
            try (ResultSet chiavi = ps.getGeneratedKeys()) {
                chiavi.next();
                return chiavi.getInt(1);
            }
        }
    }

    private static Prenotazione daRiga(ResultSet rs) throws SQLException {
        Prenotazione p = new Prenotazione();
        p.setId(rs.getInt("id"));
        p.setAulaId(rs.getInt("aula_id"));
        p.setGiorno(rs.getDate("giorno").toLocalDate().toString());
        p.setOraInizio(rs.getTime("ora_inizio").toLocalTime().toString());
        p.setOraFine(rs.getTime("ora_fine").toLocalTime().toString());
        p.setStudente(rs.getString("studente"));
        return p;
    }
}

Il metodo crea è il più delicato:

  1. apre una transazione (setAutoCommit(false)): controllo e inserimento sono un'unica operazione indivisibile (SQL avanzato - indici, transazioni e triggerUn file di record può essere heap (inserimento in ordine, ricerca per uguaglianza in media n/2 accessi), ordinato (log2 n), hash (costante, solo uguaglianza) o B-tree (log_F n, anche intervalli). Un indice è una struttura ausiliaria di coppie (chiave K, puntatore P) ordinate, molto più piccola della tabella: l'indice primario è sul campo di ordinamento del file ordinato (una chiave per blocco, non denso, log2(m)+1 accessi), l'indice secondario è su un campo qualsiasi (denso). CREATE [UNIQUE] INDEX, anche su espressioni; PostgreSQL usa tabelle heap con indici secondari B-tree (predefinito) o hash, e PRIMARY KEY e UNIQUE creano un indice implicito. Gli indici velocizzano le letture ma costano nelle scritture e in spazio. Una transazione è una sequenza atomica di operazioni con proprietà ACID (atomicità, consistenza, isolamento, durabilità): START TRANSACTION, COMMIT, SAVEPOINT, ROLLBACK [TO SAVEPOINT]. Un trigger è una regola evento-condizione-azione (ECA): BEFORE/AFTER, ROW/STATEMENT, WHEN, con funzione PL/pgSQL che usa NEW e OLD; serve per vincoli complessi e attributi derivati.SQL avanzato - indici, transazioni e trigger →);
  2. blocca la riga dell'aula (SELECT ... FOR UPDATE): una seconda richiesta sulla stessa aula aspetta la fine della prima, quindi non può inserire tra il controllo e l'INSERT;
  3. controlla la sovrapposizione con una COUNT(*): due intervalli [a,b)[a,b) e [c,d)[c,d) si sovrappongono se e solo se a<da<d e c<bc<b, quindi basta ora_inizio < nuova_fine AND ora_fine > nuovo_inizio (due prenotazioni una dopo l'altra, 11:00-13:00 dopo 09:00-11:00, non si sovrappongono). La derivazione e una tabella di casi provati sono nell'Esercizio - Servlet delle prenotazioni con controllo delle sovrapposizioni (applicazione web);
  4. se tutto va bene commit, altrimenti rollback e l'eccezione risale.

PrenotazioneException distingue gli errori dell'utente (aula inesistente, sovrapposizione) dai guasti (SQLException), perché le servlet rispondono in modo diverso: 409 o 422 contro 500.

Errori tipici

  • Dati della richiesta in campi di istanza della servlet (e visite++ non atomico).
  • Chiamare getParameter prima di setCharacterEncoding; scrivere header dopo aver iniziato il corpo (risposta già committed).
  • Concatenare i parametri nell'SQL (SQL injection) o scriverli nell'HTML senza escape (XSS).
  • Non chiudere le connessioni: con try-with-resources non succede; senza, il pool si esaurisce e l'applicazione si blocca.
  • Modificare lo stato con GET.
  • Dimenticare changeSessionId() al login; usare getSession() in un filtro di controllo (crea la sessione che dovrebbe cercare: serve getSession(false)).
  • Mettere le JSP fuori da WEB-INF, raggiungibili senza passare dal controller.
  • Annotazioni e web.xml con lo stesso nome o URL: il web.xml vince e il comportamento sorprende.
  • Credere che la validazione nel browser basti: il server controlla sempre.

Domande d'esame

1. Descrivere il ciclo di vita di una servlet e dire perché i dati di una richiesta non vanno messi nei campi. Traccia. Il container crea l'istanza una volta, chiama init(), poi service() (che smista su doGet/doPost) per ogni richiesta in un thread del pool, infine destroy(). C'è una sola istanza condivisa da tutti i thread: un campo scritto da una richiesta è visto dalle altre (dati scambiati, aggiornamenti persi). Variabili locali per la richiesta, oggetti thread-safe per lo stato condiviso.

2. Come fa il server a riconoscere lo stesso utente fra richieste HTTP diverse? Che cosa si mette nel cookie e che cosa sul server? Quali attributi proteggono il cookie? Traccia. Sessione: oggetto HttpSession sul server, identificatore casuale nel cookie JSESSIONID (Set-Cookie alla risposta, Cookie nelle richieste). HttpOnly (non leggibile da JavaScript), Secure (solo HTTPS), SameSite (contro CSRF), Path. changeSessionId() al login contro la session fixation.

3. Scrivere un filtro che respinge con un redirect a login.html le richieste di utenti non autenticati. Perché getSession(false)? Traccia. Vedere AutenticazioneFilter: se la sessione manca o non ha l'attributo, sendRedirect e return senza chain.doFilter; altrimenti chain.doFilter. getSession(false) non crea una sessione nuova, che renderebbe il controllo inutile.

Versione ripasso

Servlet. Classe Java eseguita da un container (Tomcat: Coyote = connettore HTTP, Catalina = servlet container, Jasper = compilatore JSP). Un thread per richiesta, nessun processo per richiesta (a differenza di CGI e applicazioni web dinamicheLa Common Gateway Interface (CGI, RFC 3875) e' l'interfaccia fra un server web e un programma esterno che genera la risposta: per ogni richiesta il server fa fork ed exec del programma, gli passa i dati con le meta-variabili d'ambiente (REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH, CONTENT_TYPE, SCRIPT_NAME, PATH_INFO, SERVER_*, REMOTE_ADDR, HTTP_* per gli header) e con lo standard input (corpo della POST, CONTENT_LENGTH byte); il programma scrive sullo standard output header CGI (Content-Type obbligatorio, Status, Location), una riga vuota e il corpo, e il server li trasforma in una risposta HTTP completa; i punti delicati sono pipe e dup2, la chiusura delle estremita' inutilizzate, il timeout, i limiti di dimensione e la sicurezza (injection, path traversal, header Proxy, shellshock); il costo di un processo per richiesta ha portato a FastCGI, ai moduli del server e ai server applicativi.CGI e applicazioni web dinamiche →). Standard Jakarta Servlet 6.0 (Tomcat 10.1), pacchetto jakarta.servlet.

Ciclo di vita: costruzione (1 volta) →\to init() (1 volta) →\to service() per ogni richiesta, smista su doGet/doPost/doPut/doDelete →\to destroy(). loadOnStartup per crearla all'avvio. Un metodo non ridefinito risponde 405.

java
@WebServlet("/ciao")
public class CiaoServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().println("Ciao dal server");
    }
}

Mapping: esatto (/prenotazioni) >> prefisso più lungo (/api/prenotazioni/*) >> estensione (*.jsp) >> predefinito (/). In /aule/prenotazioni/annulla?id=3: getContextPath() = /aule, getServletPath() = /prenotazioni/annulla, getQueryString() = id=3; con un pattern a prefisso getPathInfo() è il resto (/7). Dichiarabile con @WebServlet o in WEB-INF/web.xml (servlet, servlet-mapping), che prevale. Nel web.xml anche welcome-file-list, session-config, error-page, ordine dei filtri.

Richiesta: getMethod(), getParameter() (query string e corpo del form; sempre String o null: convertire e validare), getParameterValues(), getHeader(), getCookies(), getReader() per il corpo (JSON), setAttribute/getAttribute per portare oggetti alla risorsa successiva. setCharacterEncoding("UTF-8") prima dei parametri (meglio in un filtro). GET sicuro e idempotente, POST modifica lo stato: mai modificare con GET.

Risposta: prima stato e header, poi il corpo; dopo il primo byte è committed. setStatus, setHeader, setContentType, getWriter() (testo) o getOutputStream() (binario), sendRedirect (302 + Location, il browser rifà una GET), sendError(codice, messaggio). Senza istruzioni: 200 e corpo vuoto.

Escape e codici: i dati dell'utente nell'HTML passano da un escape di < > & " ' (altrimenti XSS). Errori in doPost: formato sbagliato 400, aula inesistente 422, sovrapposizione 409, guasto del database 500 (ServletException o sendError).

java
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Prenotazione p = new Prenotazione();
    try {
        p.setAulaId(Integer.parseInt(req.getParameter("aula_id")));
    } catch (NumberFormatException e) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "aula non valida");
        return;
    }
    p.setGiorno(req.getParameter("giorno"));
    p.setOraInizio(req.getParameter("ora_inizio"));
    p.setOraFine(req.getParameter("ora_fine"));
    p.setStudente((String) req.getSession().getAttribute("studente"));   // dalla sessione, non dal form
    try {
        p.valida();
        prenotazioneDAO.crea(p);
        resp.sendRedirect(req.getContextPath() + "/prenotazioni?giorno=" + p.getGiorno());   // Post/Redirect/Get
    } catch (IllegalArgumentException e) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
    } catch (PrenotazioneException e) {
        resp.sendError(e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? 409 : 422, e.getMessage());
    } catch (SQLException e) {
        throw new ServletException("errore di accesso ai dati", e);
    }
}
java
static String esc(String s) {          // escape per l'HTML: ogni dato dell'utente ci passa
    if (s == null) return "";
    return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
            .replace("\"", "&quot;").replace("'", "&#39;");
}

public void valida() {                // controlli formali del modello, senza database
    if (studente == null || studente.isBlank() || studente.length() > 40)
        throw new IllegalArgumentException("studente obbligatorio, al massimo 40 caratteri");
    try {
        LocalDate.parse(giorno);
        if (!LocalTime.parse(oraInizio).isBefore(LocalTime.parse(oraFine)))
            throw new IllegalArgumentException("l'ora di inizio deve precedere quella di fine");
    } catch (DateTimeParseException e) {
        throw new IllegalArgumentException("formato richiesto: giorno aaaa-mm-gg, ore hh:mm");
    }
}

La GET fa String studente = (String) req.getSession().getAttribute("studente"), legge giorno (oggi se manca, 400 se non è una data) e scrive tabella e form con Html.esc su ogni dato dell'utente.

Concorrenza: una sola istanza, molti thread: i campi di istanza sono condivisi.

java
private String studente;      // SBAGLIATO: un campo per tutti gli utenti
private int visite = 0;       // SBAGLIATO: visite++ non è atomico
private final AtomicInteger visite = new AtomicInteger();   // GIUSTO per lo stato condiviso
  • Stato della richiesta in variabili locali o attributi della richiesta; stato condiviso immutabile o thread-safe; mai condividere una Connection.
  • I DAO hanno solo il DataSource (thread-safe), quindi un'istanza basta. Gli attributi di sessione sono condivisi tra le schede del browser.
  • Sovrapposizioni nel database: transazione e SELECT ... FOR UPDATE sulla riga dell'aula.

Sessioni: l'oggetto HttpSession sta sul server, nel cookie JSESSIONID c'è solo l'identificatore (Set-Cookie →\to Cookie, RFC 6265).

http
HTTP/1.1 302
Set-Cookie: JSESSIONID=9D2F0C5A6B7E4D1F8A3C2B1E0F9D8C7B; Path=/; HttpOnly
Location: /prenotazioni
Attributo Effetto
Path, Domain a quali URL si invia
Max-Age / Expires durata (senza: cookie di sessione)
Secure solo HTTPS
HttpOnly non leggibile da JavaScript
SameSite non inviato da altri siti (contro CSRF)
  • getSession() crea, getSession(false) no (usarlo nei controlli), setAttribute/getAttribute, setMaxInactiveInterval, invalidate() (logout), changeSessionId() al login (contro la session fixation).
java
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String studente = req.getParameter("studente");
    if (studente == null || studente.isBlank() || studente.length() > 40) {
        resp.sendRedirect(req.getContextPath() + "/login.html?errore=1");
        return;
    }
    if (req.getSession(false) != null) {
        req.changeSessionId();                 // nuovo identificatore: evita la session fixation
    }
    HttpSession sessione = req.getSession();
    sessione.setAttribute("studente", studente.trim());
    sessione.setMaxInactiveInterval(30 * 60);  // 30 minuti di inattività
    resp.sendRedirect(req.getContextPath() + "/prenotazioni");
}
  • Sessioni sul server legano a quel server: per questo le API REST sono senza stato. Nei cookie personalizzati niente dati riservati.

Filtri: Filter/HttpFilter, chain.doFilter(req, resp) passa avanti; il codice dopo viene eseguito al ritorno. Non chiamare doFilter blocca la richiesta (esempio: redirect a login.html se manca lo studente nella sessione). Usi: codifica, autenticazione, log, compressione. Ordine con più filtri: elementi filter-mapping del web.xml.

java
@WebFilter("/prenotazioni/*")
public class AutenticazioneFilter extends HttpFilter {
    protected void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain catena)
            throws IOException, ServletException {
        HttpSession sessione = req.getSession(false);            // false: non creare la sessione
        if (sessione == null || sessione.getAttribute("studente") == null) {
            resp.sendRedirect(req.getContextPath() + "/login.html");   // la catena si interrompe
            return;
        }
        catena.doFilter(req, resp);
    }
}

CodificaFilter (mappato su /*) fa solo req.setCharacterEncoding("UTF-8") e catena.doFilter(req, resp).

War: login.html, js/ (pubblici); META-INF/context.xml; WEB-INF/web.xml, WEB-INF/classes, WEB-INF/lib, WEB-INF/jsp (mai servita direttamente). Tomcat: bin, conf (server.xml, porta 8080), lib (driver JDBC), webapps, logs, work (JSP compilate). Un war in webapps si distribuisce da solo; ROOT.war sta alla radice.

Database: pool di connessioni in context.xml (Resource name="jdbc/aule" di tipo javax.sql.DataSource), cercato con JNDI (java:comp/env/jdbc/aule) in un ServletContextListener che crea i DAO e li mette nel ServletContext. conn.close() restituisce la connessione al pool.

xml
<Context>
  <Resource name="jdbc/aule" auth="Container" type="javax.sql.DataSource"
            driverClassName="org.postgresql.Driver" url="jdbc:postgresql://db:5432/aule"
            username="aule" password="aule" maxTotal="10" maxIdle="5" maxWaitMillis="5000"/>
</Context>
java
@WebListener
public class AvvioListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent evento) {
        try {
            DataSource ds = (DataSource) new InitialContext().lookup("java:comp/env/jdbc/aule");
            evento.getServletContext().setAttribute("aulaDAO", new AulaDAO(ds));
            evento.getServletContext().setAttribute("prenotazioneDAO", new PrenotazioneDAO(ds));
        } catch (NamingException e) {
            throw new IllegalStateException("DataSource jdbc/aule non configurato", e);
        }
    }
}
java
String sql = "SELECT id, nome, posti FROM aula WHERE id = ?";
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
    ps.setInt(1, id);
    try (ResultSet rs = ps.executeQuery()) {
        return rs.next() ? new Aula(rs.getInt("id"), rs.getString("nome"), rs.getInt("posti")) : null;
    }
}
java
public Prenotazione crea(Prenotazione p) throws SQLException, PrenotazioneException {
    try (Connection c = ds.getConnection()) {
        c.setAutoCommit(false);
        try {
            if (!bloccaAula(c, p.getAulaId())) throw new PrenotazioneException(Tipo.AULA_INESISTENTE);
            if (siSovrappone(c, p))            throw new PrenotazioneException(Tipo.SOVRAPPOSIZIONE);
            p.setId(inserisci(c, p));
            c.commit();
            return p;
        } catch (SQLException | PrenotazioneException | RuntimeException e) {
            c.rollback();
            throw e;
        }
    }
}
sql
SELECT id FROM aula WHERE id = ? FOR UPDATE;
SELECT COUNT(*) FROM prenotazione
WHERE aula_id = ? AND giorno = ? AND ora_inizio < ? AND ora_fine > ?;
  • Sovrapposizione di [a,b)[a,b) e [c,d)[c,d) se e solo se a<da<d e c<bc<b: SQL ora_inizio < nuova_fine AND ora_fine > nuovo_inizio; 11:00-13:00 dopo 09:00-11:00 non si sovrappone. PrenotazioneException separa errori dell'utente (409, 422) dai guasti (500).

Errori tipici

  • Dati della richiesta nei campi; header scritti dopo il corpo; getParameter prima di setCharacterEncoding.
  • SQL concatenato (injection), HTML senza escape (XSS), connessioni non chiuse.
  • Modificare con GET; niente changeSessionId() al login; getSession() al posto di getSession(false) nel filtro.
  • JSP fuori da WEB-INF; fidarsi solo della validazione nel browser.

Domande d'esame (tracce)

  • Ciclo di vita: costruzione, init(), service() per richiesta (un thread ciascuna, una sola istanza), destroy(). Perciò dati della richiesta in variabili locali.
  • Sessione: stato sul server, identificatore nel cookie JSESSIONID; HttpOnly, Secure, SameSite; changeSessionId() al login.
  • Filtro di autenticazione: getSession(false), se manca l'attributo sendRedirect e return, altrimenti chain.doFilter.

Collegamenti: JSP e pattern Model-View-ControllerScrivere l'HTML con println dentro la servlet mescola logica e presentazione. Una JSP (Jakarta Pages) è una pagina HTML con parti dinamiche che Jasper traduce in una servlet alla prima richiesta (quindi vale la stessa concorrenza). Si usano la direttiva page, le espressioni EL (Expression Language) per leggere attributi e proprietà dei JavaBean (la proprietà nome chiama getNome), gli oggetti impliciti (param, requestScope, sessionScope, pageContext) e le librerie JSTL core (c:out, c:if, c:choose, c:forEach); l'EL non fa escape, quindi i dati dell'utente passano da c:out. Il pattern Model-View-Controller separa il model (JavaBean e DAO: dati e regole), la view (JSP, che mostra) e il controller (servlet: legge la richiesta, chiama il model, mette i risultati negli attributi della richiesta e fa il forward alla view). Le JSP stanno in WEB-INF così non si raggiungono direttamente. Forward: stessa richiesta, stesso URL, attributi condivisi, lato server; redirect: risposta 302 con Location, nuova richiesta GET, URL cambiato, attributi persi. Il pattern Post/Redirect/Get risponde alla POST con un redirect a una GET, per evitare che il ricaricamento rinvii il form; il messaggio di conferma passa in sessione (flash) e si mostra una volta sola; in caso di errore invece si fa il forward alla stessa pagina con il form compilato e lo stato 4xx.JSP e pattern Model-View-Controller →, Servizi REST in Java - progettazione delle risorse e implementazione con servletUn servizio REST espone risorse, non pagine: il server risponde con dati (JSON) e codici di stato, e la presentazione passa al client. Per progettare l'API si individuano i sostantivi del dominio (aula, prenotazione), si dà a collezioni ed elementi un URI (/api/aule, /api/aule/3, /api/prenotazioni, /api/prenotazioni/7: nomi al plurale, mai verbi), si usano i metodi HTTP con la semantica di RFC 9110 (GET sicuro e idempotente, POST crea e non è idempotente, PUT e DELETE idempotenti) e si filtrano le collezioni con la query (?aula=1&giorno=2026-10-19). I codici fanno parte del contratto: 200 lettura, 201 creazione con Location, 204 cancellazione, 400 richiesta malformata, 404 risorsa inesistente, 405 metodo non consentito con Allow, 409 conflitto (orario già occupato), 415 tipo di contenuto, 422 richiesta ben formata ma non elaborabile (aula inesistente), 500 guasto. Il JSON usa i nomi delle colonne (aula_id, ora_inizio, ora_fine) e gli errori hanno un corpo {"errore": "..."}. In Java una servlet mappata su /api/prenotazioni/* fa il dispatch sul path: getPathInfo() nullo o "/" indica la collezione, "/12" l'elemento; doGet, doPost e doDelete implementano i metodi, Gson converte oggetti e JSON, i metodi non previsti rispondono 405 con l'header Allow. Il servizio è senza stato: niente sessione, ogni richiesta porta tutto ciò che serve.Servizi REST in Java - progettazione delle risorse e implementazione con servlet →, Strumenti di sviluppo - git, Maven e DockerTre strumenti rendono un progetto web riproducibile e condivisibile. Git tiene la storia del codice: tre aree (cartella di lavoro, area di staging, repository), il commit è un'istantanea con un hash, il branch è un puntatore mobile a un commit, HEAD indica dove si è; il merge fonde due rami (fast-forward se non c'è divergenza, altrimenti un commit di merge) e può produrre conflitti, che si risolvono a mano nei marcatori di conflitto, poi add e commit; con un remoto si usano clone, fetch, pull (fetch + merge) e push, e un push rifiutato (fetch first) richiede prima un pull; in gruppo si lavora su rami di funzionalità con commit piccoli e main sempre funzionante. Maven descrive il progetto nel pom.xml (coordinate groupId:artifactId:version, packaging war, dipendenze con scope compile, provided, runtime, test), impone la struttura src/main/java, src/main/webapp, src/test, e il ciclo di vita validate, compile, test, package, verify, install, deploy: ogni fase esegue prima tutte le precedenti, mvn clean package produce target/aule.war. Docker confeziona l'applicazione: un'immagine (strati di sola lettura, costruita da un Dockerfile) è il modello, un container è un'istanza in esecuzione; per l'esempio un Dockerfile a due fasi compila il war con Maven e lo copia in Tomcat, e docker compose avvia insieme Tomcat e PostgreSQL con un volume per i dati.Strumenti di sviluppo - git, Maven e Docker →, Esercizio - Servlet delle prenotazioni con controllo delle sovrapposizioni (applicazione web).

Esercizi su questo argomento

Teoria collegata