Servizi REST in Java - progettazione delle risorse e implementazione con servlet
In questa pagina 8
Fin qui il server rispondeva con pagine HTML complete (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 →): ogni azione dell'utente ricaricava l'intera pagina. Con il Web 2.0 il server diventa un servizio che offre dati, e la pagina, già caricata nel browser, li chiede in background e li mostra (AJAX e fetch - applicazioni Web 2.0 e MVC rivistoAJAX (Asynchronous JavaScript and XML, oggi quasi sempre con JSON) permette a JavaScript di chiedere dati al server in background e aggiornare solo una parte del DOM, senza ricaricare la pagina. L'oggetto storico è XMLHttpRequest (open, send, eventi onload e onerror); l'API moderna è fetch, che restituisce una Promise di Response (ok, status, json()). Una Promise rappresenta un risultato futuro (in attesa, mantenuta, rifiutata) e si concatena con then, catch e finally; async/await è la sintassi che la fa leggere come codice sequenziale, e dopo ogni await il resto della funzione riparte quando la Promise si risolve (il codice sincrono finisce prima, poi vengono le promise, poi i timer). Attenzione: fetch rifiuta la Promise solo per errori di rete; un 404 o un 500 sono risposte e si controllano con response.ok. Il client delle aule ha api.js (funzioni che chiamano l'API REST e lanciano ErroreApi se il codice non è 2xx) e pagina.js (legge il form, chiama api.js, aggiorna la tabella con textContent, un solo gestore di click per i bottoni di cancellazione). Il paradigma MVC viene rivisto: il server resta con model e controller REST ma restituisce solo dati, la view è nel browser (DOM costruito da JavaScript) e il controller della pagina sono i gestori di eventi; lo stato dell'interfaccia sta nel client e il server è senza stato. La same-origin policy (origine = schema, host, porta) impedisce a uno script di leggere risposte di altre origini; CORS permette al server di autorizzarle con Access-Control-Allow-Origin, e le richieste non semplici (JSON, DELETE) sono precedute da una richiesta OPTIONS di preflight.AJAX e fetch - applicazioni Web 2.0 e MVC rivisto →). L'architettura REST (REpresentational State Transfer, Fielding 2000) dà le regole per progettare questo servizio. I vincoli (client-server, senza stato, cache, interfaccia uniforme, livelli) e le definizioni di risorsa e rappresentazione sono in Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST → e qui non si ripetono; questa nota riguarda come si progetta l'API dell'applicazione delle aule e come si realizza con servlet. La semantica dei metodi e dei codici di stato è quella di RFC 9110 (HTTP Semantics); le basi dei messaggi sono in HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding → e 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 →.
Progettare le risorse
Il procedimento, applicato all'esempio (tabelle aula e prenotazione):
- Individuare i sostantivi del dominio. Aule e prenotazioni sono le cose che il client vuole leggere e modificare: ciascuna è un tipo di risorsa. Non tutto ciò che c'è nel database deve diventare una risorsa, e una risorsa non coincide per forza con una tabella.
- Collezione e elemento. Per ogni tipo, due URI: la collezione (
/api/aule) e il singolo elemento (/api/aule/3, dove 3 è l'id). Nomi al plurale, minuscoli; l'identificatore è parte del path perché identifica la risorsa. - Relazioni. Ogni prenotazione appartiene a un'aula (
aula_id). Due modi di esporlo: annidare (/api/aule/1/prenotazioni) oppure tenere le risorse piatte e filtrare (/api/prenotazioni?aula=1). L'esempio sceglie le risorse piatte: la prenotazione ha un URI proprio e breve (/api/prenotazioni/7) e i filtri (aula,giorno) si combinano liberamente (il giorno senza l'aula, per tutte le aule). L'annidamento ha senso per le risorse che non esistono senza il genitore. - Azioni. Si traducono in metodi HTTP sulle risorse, non in URI con verbi. "Prenotare" è creare un elemento della collezione (
POST /api/prenotazioni), "annullare" è eliminare l'elemento (DELETE /api/prenotazioni/7). Niente/api/prenotané/api/cancellaPrenotazione?id=7. Una modifica di orario non è offerta: si cancella e si ricrea (così il controllo delle sovrapposizioni resta nel soloPOST); unPUTcompleto o unPATCHsarebbero ammissibili con lo stesso controllo. - Rappresentazioni e codici. Si fissano il formato dei dati (JSON, sotto) e gli esiti di ciascuna operazione (tabella dei codici).
Regole per gli URI: sostantivi e non verbi; plurali; minuscoli con trattini se serve; il filtro, l'ordinamento e la paginazione nella query string (?giorno=...&ordina=...), l'identità nel path; un prefisso (/api) che separa le risorse dei dati dalle pagine; nessuna estensione (.json), perché il formato si negozia con Accept e Content-Type; il numero di versione, se serve, nel prefisso (/api/v1/...), omesso nell'esempio.
L'API dell'esempio
| Metodo | URI | Significato | Successo | Errori |
|---|---|---|---|---|
GET |
/api/aule |
elenco delle aule | 200 + array |
500 |
GET |
/api/aule/{id} |
una aula | 200 + oggetto |
404 |
GET |
/api/prenotazioni?aula=&giorno= |
elenco, filtrabile per aula e/o giorno | 200 + array (anche vuoto) |
400 filtro malformato |
GET |
/api/prenotazioni/{id} |
una prenotazione | 200 + oggetto |
404 |
POST |
/api/prenotazioni |
crea una prenotazione | 201 + Location + oggetto creato |
400 dati malformati, 415 non è JSON, 422 aula inesistente, 409 orario occupato |
DELETE |
/api/prenotazioni/{id} |
annulla una prenotazione | 204 senza corpo |
404 |
| altri | qualunque URI sopra | 405 con Allow |
I metodi HTTP
RFC 9110 definisce per ogni metodo due proprietà che il progettista deve rispettare:
- sicuro (safe): il client non chiede e non ottiene alcun cambiamento di stato sul server (letture). Un crawler o un'anteprima può chiamare liberamente i metodi sicuri;
- idempotente: ripetere la stessa richiesta più volte ha lo stesso effetto di farla una volta; un client può quindi ritentare dopo un errore di rete senza paura.
| Metodo | Uso | Sicuro | Idempotente |
|---|---|---|---|
GET |
leggere una risorsa o una collezione | sì | sì |
POST |
creare un elemento nella collezione (il server sceglie l'id), o azioni senza altro metodo adatto | no | no |
PUT |
sostituire per intero l'elemento, a un URI deciso dal client | no | sì |
PATCH |
modificare solo alcuni campi | no | non garantito |
DELETE |
eliminare | no | sì |
DELETE è idempotente nell'effetto (dopo la prima richiesta l'elemento non c'è più) anche se la seconda risposta è 404. POST non lo è: ripetere la richiesta può creare due elementi; nell'esempio il controllo delle sovrapposizioni risponde 409 al secondo tentativo, quindi un ritentativo non duplica la prenotazione.
I codici di stato
I codici sono parte del contratto: il client decide che cosa fare guardando il codice, e il corpo spiega. Classi: 2xx riuscito, 4xx errore del client (sbaglia lui, ripetere uguale non serve), 5xx errore del server.
| Codice | Quando nell'esempio |
|---|---|
200 OK |
lettura riuscita |
201 Created |
prenotazione creata; l'header Location contiene l'URI della nuova risorsa (/api/prenotazioni/5) e il corpo la sua rappresentazione |
204 No Content |
cancellazione riuscita, nessun corpo |
400 Bad Request |
richiesta malformata: JSON non valido, campo mancante, data o ora in formato sbagliato, ora_inizio non precedente a ora_fine, filtro non numerico |
404 Not Found |
l'URI non corrisponde a nessuna risorsa: id inesistente, path come /api/aule/abc |
405 Method Not Allowed |
URI valido ma metodo non previsto (DELETE /api/aule/1): la risposta deve contenere Allow con i metodi ammessi |
409 Conflict |
la richiesta è corretta ma contrasta con lo stato della risorsa: orario già occupato |
415 Unsupported Media Type |
il corpo non è application/json |
422 Unprocessable Content |
sintassi corretta ma contenuto non elaborabile: aula_id che non esiste |
500 Internal Server Error |
guasto del server (database): il dettaglio va nel log, non nella risposta |
Distinzioni che si chiedono all'esame: 400 o 422: 400 se la richiesta non si capisce (sintassi, tipi, campi), 422 se si capisce ma i dati hanno un significato impossibile; molte API usano solo 400, l'importante è la coerenza. 404 o 405: 404 se la risorsa non esiste, 405 se esiste ma non con quel metodo. 409: dipende dallo stato del server, e cambierebbe se qualcun altro cancellasse la prenotazione che blocca. Autenticazione e autorizzazione (401, 403) non sono nell'esempio: le credenziali viaggiano a ogni richiesta (header Authorization), mai in una sessione, perché il servizio è senza stato.
Le rappresentazioni JSON
{"id": 2, "nome": "Aula B2", "posti": 45}{"id": 1, "aula_id": 1, "giorno": "2026-10-19", "ora_inizio": "09:00", "ora_fine": "11:00", "studente": "Rossi"}- I nomi dei campi sono quelli delle colonne (
aula_id,ora_inizio,ora_fine), in snake_case. Date e ore sono stringhe nel formato ISO 8601 (aaaa-mm-gg,hh:mm): JSON non ha un tipo data. - Una collezione è un array, anche vuoto (
[]con200, non404). - Un errore ha sempre lo stesso corpo, leggibile dalle macchine e dalle persone:
{"errore": "l'aula è già prenotata in quell'orario"}. - In ingresso (
POST) il client manda la prenotazione senzaid: lo assegna il server. Se lo manda, viene ignorato. - L'header
Content-Type: application/jsondichiara il formato, in richiesta e in risposta; con JSON la codifica è sempre UTF-8.
L'implementazione con le servlet
Il model è lo stesso dell'applicazione MVC: Aula, Prenotazione, AulaDAO, PrenotazioneDAO (Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioniUna servlet è una classe Java che un container (Apache Tomcat: connettore HTTP Coyote, contenitore Catalina, compilatore JSP Jasper) istanzia una sola volta e chiama per ogni richiesta, ciascuna in un thread del pool. Ciclo di vita: caricamento e costruzione, init() una volta, service() per ogni richiesta (HttpServlet lo smista su doGet, doPost, doPut, doDelete), destroy() allo spegnimento. La richiesta (HttpServletRequest) dà metodo, parametri (query string e corpo del form), header, path (contextPath, servletPath, pathInfo) e corpo; la risposta (HttpServletResponse) si costruisce con stato, header e corpo (PrintWriter), con sendRedirect (302 + Location) o sendError. Il mapping si dichiara con @WebServlet o in WEB-INF/web.xml (che prevale). Poiché esiste una sola istanza e molti thread, i campi di istanza sono condivisi: lo stato della richiesta sta in variabili locali, quello condiviso in oggetti thread-safe (AtomicInteger, DataSource). HTTP è senza stato, quindi si usano le sessioni: la servlet crea un HttpSession, tiene i dati sul server e manda al browser solo l'identificatore nel cookie JSESSIONID (Set-Cookie con HttpOnly, Secure, SameSite, Path secondo RFC 6265); al login si cambia l'identificatore (changeSessionId) contro la session fixation. I filtri (Filter, FilterChain) elaborano richiesta e risposta prima e dopo le servlet: codifica, autenticazione, log. Il war ha la struttura WEB-INF/classes, WEB-INF/lib, WEB-INF/web.xml, META-INF/context.xml; l'accesso ai dati usa un DataSource con pool di connessioni dichiarato in context.xml e cercato con JNDI, e un DAO con PreparedStatement e try-with-resources.Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni →). Cambia solo la view: invece di una JSP c'è un serializzatore JSON. Per convertire oggetti Java e JSON si usa una libreria; qui Gson, dipendenza nel pom. Le servlet dell'API stanno nel package it.unipd.aule.rest; hanno gli stessi nomi, AuleServlet e PrenotazioniServlet, di quelle MVC ma URL diversi (/api/...), quindi possono coesistere.
Una classe di utilità raccoglie ciò che serve a entrambe:
package it.unipd.aule.rest;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/** Piccole utilità comuni alle servlet REST: scrivere JSON e leggere l'identificatore dal path. */
final class Json {
// aulaId diventa "aula_id", oraInizio "ora_inizio": nel JSON i nomi sono quelli delle colonne
static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.disableHtmlEscaping() // di serie Gson trasforma l'apostrofo e <, >, & in sequenze "uXXXX"
.create();
private Json() { }
static void invia(HttpServletResponse resp, int stato, Object corpo) throws IOException {
resp.setStatus(stato);
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write(GSON.toJson(corpo));
}
static void errore(HttpServletResponse resp, int stato, String messaggio) throws IOException {
invia(resp, stato, Map.of("errore", messaggio));
}
/** 405 con l'header Allow obbligatorio (RFC 9110, par. 15.5.6). */
static void nonConsentito(HttpServletResponse resp, String allow) throws IOException {
resp.setHeader("Allow", allow);
errore(resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED, "metodo non consentito su questa risorsa");
}
/** True se il path è la collezione: nessun path oppure solo "/". */
static boolean eCollezione(String pathInfo) {
return pathInfo == null || pathInfo.equals("/");
}
/** Estrae l'id da "/12". Lancia NumberFormatException per "/abc" o "/12/altro": la servlet risponde 404. */
static int id(String pathInfo) {
return Integer.parseInt(pathInfo.substring(1));
}
}FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORESfa sì cheaulaIddiventiaula_idnel JSON, e viceversa in lettura: il codice Java resta in camelCase, l'API in snake_case.inviaimposta stato,Content-Typee codifica prima di scrivere il corpo (altrimenti la risposta è già committed).nonConsentitoproduce il405con l'headerAllowrichiesto da RFC 9110.idestrae il numero da"/12"; con"/abc"o"/12/altro"Integer.parseIntlanciaNumberFormatException, che le servlet traducono in404.
Il dispatch sul path
L'URL /api/prenotazioni/12 e /api/prenotazioni devono arrivare alla stessa servlet, che capisce da sola di che risorsa si tratta. Con il mapping a prefisso /api/prenotazioni/*:
| URL | getPathInfo() |
Risorsa |
|---|---|---|
/api/prenotazioni |
null |
collezione |
/api/prenotazioni/ |
"/" |
collezione |
/api/prenotazioni/12 |
"/12" |
elemento 12 |
/api/prenotazioni/abc |
"/abc" |
non esiste: 404 |
Il container sceglie già il metodo (doGet, doPost, doDelete); dentro ciascuno si guarda getPathInfo(). Le aule, in sola lettura:
package it.unipd.aule.rest;
import it.unipd.aule.dao.AulaDAO;
import it.unipd.aule.model.Aula;
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.sql.SQLException;
/** Risorse /api/aule (collezione) e /api/aule/{id} (singola aula): solo lettura. */
@WebServlet("/api/aule/*")
public class AuleServlet extends HttpServlet {
private AulaDAO dao;
@Override
public void init() {
dao = (AulaDAO) getServletContext().getAttribute("aulaDAO");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
String path = req.getPathInfo(); // ciò che segue /api/aule: null, "/" oppure "/3"
if (Json.eCollezione(path)) {
Json.invia(resp, HttpServletResponse.SC_OK, dao.tutte());
return;
}
Aula aula = dao.perId(Json.id(path));
if (aula == null) {
Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "aula inesistente");
} else {
Json.invia(resp, HttpServletResponse.SC_OK, aula);
}
} catch (NumberFormatException e) {
Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "risorsa inesistente");
} catch (SQLException e) {
log("errore SQL", e);
Json.errore(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "errore interno");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Json.nonConsentito(resp, "GET");
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Json.nonConsentito(resp, "GET");
}
}Le prenotazioni, con lettura, creazione e cancellazione:
package it.unipd.aule.rest;
import com.google.gson.JsonSyntaxException;
import it.unipd.aule.dao.PrenotazioneDAO;
import it.unipd.aule.dao.PrenotazioneException;
import it.unipd.aule.model.Prenotazione;
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.sql.SQLException;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
/**
* API REST delle prenotazioni. Una sola servlet per due tipi di risorsa, scelti dal path:
* /api/prenotazioni collezione: GET (elenco, filtri ?aula= e ?giorno=), POST (crea)
* /api/prenotazioni/{id} singola prenotazione: GET, DELETE
*/
@WebServlet("/api/prenotazioni/*")
public class PrenotazioniServlet extends HttpServlet {
private PrenotazioneDAO dao;
@Override
public void init() {
dao = (PrenotazioneDAO) getServletContext().getAttribute("prenotazioneDAO");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
String path = req.getPathInfo();
if (Json.eCollezione(path)) {
String aula = req.getParameter("aula");
String giorno = req.getParameter("giorno");
if (giorno != null) {
LocalDate.parse(giorno); // solo per validarlo
}
Integer aulaId = aula == null ? null : Integer.valueOf(aula);
Json.invia(resp, HttpServletResponse.SC_OK, dao.lista(aulaId, giorno));
return;
}
Prenotazione p = dao.perId(Json.id(path));
if (p == null) {
Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "prenotazione inesistente");
} else {
Json.invia(resp, HttpServletResponse.SC_OK, p);
}
} catch (DateTimeParseException e) {
Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "giorno: formato aaaa-mm-gg");
} catch (NumberFormatException e) {
// "/abc" nel path è una risorsa che non esiste; "?aula=abc" è una richiesta malformata
boolean nelPath = !Json.eCollezione(req.getPathInfo());
Json.errore(resp, nelPath ? HttpServletResponse.SC_NOT_FOUND : HttpServletResponse.SC_BAD_REQUEST,
nelPath ? "risorsa inesistente" : "aula: numero intero");
} catch (SQLException e) {
erroreInterno(resp, e);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (!Json.eCollezione(req.getPathInfo())) {
Json.nonConsentito(resp, "GET, DELETE");
return;
}
String tipo = req.getContentType();
if (tipo == null || !tipo.startsWith("application/json")) {
Json.errore(resp, HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "serve Content-Type: application/json");
return;
}
req.setCharacterEncoding("UTF-8");
try {
Prenotazione p = Json.GSON.fromJson(req.getReader(), Prenotazione.class);
if (p == null) {
Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "corpo vuoto");
return;
}
p.setId(0); // l'id lo assegna il server, qualunque cosa mandi il client
p.valida();
dao.crea(p);
resp.setHeader("Location", req.getContextPath() + "/api/prenotazioni/" + p.getId());
Json.invia(resp, HttpServletResponse.SC_CREATED, p);
} catch (JsonSyntaxException e) {
Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, "il corpo non è un JSON valido");
} catch (IllegalArgumentException e) {
Json.errore(resp, HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); // dal metodo valida()
} catch (PrenotazioneException e) {
int stato = e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? HttpServletResponse.SC_CONFLICT : 422;
Json.errore(resp, stato, e.getMessage());
} catch (SQLException e) {
erroreInterno(resp, e);
}
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (Json.eCollezione(req.getPathInfo())) {
Json.nonConsentito(resp, "GET, POST");
return;
}
try {
if (dao.elimina(Json.id(req.getPathInfo()))) {
resp.setStatus(HttpServletResponse.SC_NO_CONTENT); // successo senza corpo
} else {
Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "prenotazione inesistente");
}
} catch (NumberFormatException e) {
Json.errore(resp, HttpServletResponse.SC_NOT_FOUND, "risorsa inesistente");
} catch (SQLException e) {
erroreInterno(resp, e);
}
}
private void erroreInterno(HttpServletResponse resp, SQLException e) throws IOException {
log("errore SQL", e); // il dettaglio resta nel log del server
Json.errore(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "errore interno");
}
}Punti da osservare:
- Nessun HTML, nessuna sessione: la servlet usa solo la richiesta; per questo l'API è senza stato.
doGetha due rami (collezione o elemento). I filtriaulaegiornosono convalidati prima di arrivare al DAO; unNumberFormatExceptionvuol dire 404 se l'errore sta nel path, 400 se sta nel filtro. Un elemento che non c'è (nulldal DAO) è404.doPostapplica nell'ordine: il path (unPOSTsu un elemento non ha senso:405), ilContent-Type(415), la lettura del JSON (getReader()e Gson; sintassi errata400), la validazione del modello (valida()400) e il DAO (crea:409o422). Conp.setId(0)si ignora l'idmandato dal client. In caso di successo:Locatione stato201con la prenotazione completa dell'id.doDelete:204senza corpo se la riga c'era,404se no.- Gli errori dell'utente e i guasti si distinguono:
SQLExceptionè registrata conloge al client arriva solo{"errore":"errore interno"}.
Uno scambio completo
Le risposte qui sotto sono quelle prodotte dalle due servlet, provate con richieste simulate e DAO in memoria sugli stessi dati dell'esempio (gli header Date e simili sono omessi). Prima una lettura:
GET /api/prenotazioni?aula=1&giorno=2026-10-19 HTTP/1.1
Host: localhost:8080
Accept: application/jsonHTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 207
[{"id":1,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"09:00","ora_fine":"11:00","studente":"Rossi"},{"id":2,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"14:00","ora_fine":"16:00","studente":"Bianchi"}]Una creazione riuscita, poi un tentativo sovrapposto alla prima:
POST /api/prenotazioni HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Content-Length: 93
{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}HTTP/1.1 201
Location: /api/prenotazioni/5
Content-Type: application/json;charset=UTF-8
Content-Length: 100
{"id":5,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}POST /api/prenotazioni HTTP/1.1
Content-Type: application/json
{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"10:00","ora_fine":"12:00","studente":"Neri"}HTTP/1.1 409
Content-Type: application/json;charset=UTF-8
{"errore":"l'aula è già prenotata in quell'orario"}(L'intervallo 10:00-12:00 si sovrappone sia a Rossi 09:00-11:00 sia alla nuova prenotazione di Neri 11:00-12:30.) Gli altri esiti:
| Richiesta | Risposta |
|---|---|
GET /api/aule/9 |
404 {"errore":"aula inesistente"} |
GET /api/aule/abc |
404 {"errore":"risorsa inesistente"} |
GET /api/prenotazioni?giorno=19-10-2026 |
400 {"errore":"giorno: formato aaaa-mm-gg"} |
POST /api/prenotazioni con aula_id 99 |
422 {"errore":"aula inesistente"} |
POST /api/prenotazioni con ora_inizio 13:00 e ora_fine 12:00 |
400 {"errore":"l'ora di inizio deve precedere quella di fine"} |
POST /api/prenotazioni con Content-Type: text/plain |
415 {"errore":"serve Content-Type: application/json"} |
POST /api/prenotazioni/1 |
405 con Allow: GET, DELETE |
DELETE /api/prenotazioni/5 |
204, senza corpo |
DELETE /api/prenotazioni/5 (di nuovo) |
404 {"errore":"prenotazione inesistente"} |
DELETE /api/prenotazioni |
405 con Allow: GET, POST |
Dalla riga di comando il servizio si prova con curl:
$ curl -i "http://localhost:8080/api/prenotazioni?giorno=2026-10-19"
$ curl -i -X POST http://localhost:8080/api/prenotazioni \
-H "Content-Type: application/json" \
-d '{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}'
$ curl -i -X DELETE http://localhost:8080/api/prenotazioni/5Quello che resta fuori
- HATEOAS: le risposte non contengono link alle azioni possibili (livello 3 di Richardson); con codici e URI corretti l'API è al livello 2.
- Autenticazione: un filtro potrebbe controllare
Authorization: Bearer ...su/api/*, rispondendo401; si dovrebbe anche limitare la cancellazione al proprietario (403). - CORS: se la pagina che usa l'API viene servita da un'altra origine servono gli header
Access-Control-*(AJAX e fetch - applicazioni Web 2.0 e MVC rivistoAJAX (Asynchronous JavaScript and XML, oggi quasi sempre con JSON) permette a JavaScript di chiedere dati al server in background e aggiornare solo una parte del DOM, senza ricaricare la pagina. L'oggetto storico è XMLHttpRequest (open, send, eventi onload e onerror); l'API moderna è fetch, che restituisce una Promise di Response (ok, status, json()). Una Promise rappresenta un risultato futuro (in attesa, mantenuta, rifiutata) e si concatena con then, catch e finally; async/await è la sintassi che la fa leggere come codice sequenziale, e dopo ogni await il resto della funzione riparte quando la Promise si risolve (il codice sincrono finisce prima, poi vengono le promise, poi i timer). Attenzione: fetch rifiuta la Promise solo per errori di rete; un 404 o un 500 sono risposte e si controllano con response.ok. Il client delle aule ha api.js (funzioni che chiamano l'API REST e lanciano ErroreApi se il codice non è 2xx) e pagina.js (legge il form, chiama api.js, aggiorna la tabella con textContent, un solo gestore di click per i bottoni di cancellazione). Il paradigma MVC viene rivisto: il server resta con model e controller REST ma restituisce solo dati, la view è nel browser (DOM costruito da JavaScript) e il controller della pagina sono i gestori di eventi; lo stato dell'interfaccia sta nel client e il server è senza stato. La same-origin policy (origine = schema, host, porta) impedisce a uno script di leggere risposte di altre origini; CORS permette al server di autorizzarle con Access-Control-Allow-Origin, e le richieste non semplici (JSON, DELETE) sono precedute da una richiesta OPTIONS di preflight.AJAX e fetch - applicazioni Web 2.0 e MVC rivisto →). - Paginazione e ordinamento per collezioni grandi (
?pagina=2&per_pagina=20); cache conETageCache-Controlsulle letture. - Test: oltre che con
curl, le servlet si provano con richieste simulate, come si è fatto per questa nota.
Errori tipici
- URI con verbi (
/api/prenotaAula) oGETche modifica lo stato. - Rispondere
200con{"errore": ...}nel corpo invece del codice giusto: il client non può distinguere. - Non rispondere
201eLocationalla creazione, o404per una collezione vuota (è200con[]). - Confondere
404e405; dimenticareAllownel405. - Fidarsi del JSON ricevuto: nessun controllo di tipo e di formato, oppure accettare l'
iddal client. - Mandare al client il testo dell'eccezione o la stack trace.
- Tenere uno stato di sessione nel servizio: non è più REST e non si può replicare.
- Non impostare
Content-Type: application/json, o scrivere il corpo prima dello stato.
Domande d'esame
1. Progettare l'API REST per le prenotazioni delle aule: risorse, URI, metodi e codici di stato.
Traccia. Risorse aule e prenotazioni; collezioni /api/aule e /api/prenotazioni con filtri in query, elementi /api/aule/{id}, /api/prenotazioni/{id}. GET per leggere (200, 404), POST sulla collezione per creare (201 + Location; 400, 409 per sovrapposizione, 422 per aula inesistente), DELETE sull'elemento (204, 404). Metodi non previsti: 405 con Allow. JSON con i nomi delle colonne.
2. Perché una POST che crea una risorsa non è idempotente e DELETE sì? Che cosa risponde il servizio alla seconda DELETE della stessa prenotazione?
Traccia. Due POST identiche possono creare due elementi; due DELETE portano allo stesso stato finale (l'elemento non c'è), anche se la seconda risposta è 404. Nell'esempio una seconda POST uguale dà 409.
3. Come fa una sola servlet mappata su /api/prenotazioni/* a gestire collezione ed elemento? Scrivere lo scheletro di doGet.
Traccia. getPathInfo() è null o "/" per la collezione, "/12" per l'elemento. Collezione: leggere i filtri, validarli, dao.lista(aula, giorno), 200 con l'array. Elemento: Integer.parseInt dell'id (NumberFormatException 404), dao.perId, 404 se null, altrimenti 200.
Versione ripasso
Idea. Il server offre risorse e dati (JSON), non pagine; la vista passa al client (AJAX e fetch - applicazioni Web 2.0 e MVC rivistoAJAX (Asynchronous JavaScript and XML, oggi quasi sempre con JSON) permette a JavaScript di chiedere dati al server in background e aggiornare solo una parte del DOM, senza ricaricare la pagina. L'oggetto storico è XMLHttpRequest (open, send, eventi onload e onerror); l'API moderna è fetch, che restituisce una Promise di Response (ok, status, json()). Una Promise rappresenta un risultato futuro (in attesa, mantenuta, rifiutata) e si concatena con then, catch e finally; async/await è la sintassi che la fa leggere come codice sequenziale, e dopo ogni await il resto della funzione riparte quando la Promise si risolve (il codice sincrono finisce prima, poi vengono le promise, poi i timer). Attenzione: fetch rifiuta la Promise solo per errori di rete; un 404 o un 500 sono risposte e si controllano con response.ok. Il client delle aule ha api.js (funzioni che chiamano l'API REST e lanciano ErroreApi se il codice non è 2xx) e pagina.js (legge il form, chiama api.js, aggiorna la tabella con textContent, un solo gestore di click per i bottoni di cancellazione). Il paradigma MVC viene rivisto: il server resta con model e controller REST ma restituisce solo dati, la view è nel browser (DOM costruito da JavaScript) e il controller della pagina sono i gestori di eventi; lo stato dell'interfaccia sta nel client e il server è senza stato. La same-origin policy (origine = schema, host, porta) impedisce a uno script di leggere risposte di altre origini; CORS permette al server di autorizzarle con Access-Control-Allow-Origin, e le richieste non semplici (JSON, DELETE) sono precedute da una richiesta OPTIONS di preflight.AJAX e fetch - applicazioni Web 2.0 e MVC rivisto →). Vincoli REST in Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →; semantica di metodi e codici da RFC 9110. L'API è senza stato: niente sessione, ogni richiesta porta tutto.
Progettare le risorse: 1 sostantivi del dominio (aula, prenotazione); 2 collezione ed elemento (/api/aule, /api/aule/3); 3 relazioni: risorse piatte con filtri (/api/prenotazioni?aula=1&giorno=2026-10-19) invece dell'annidamento; 4 azioni = metodi HTTP, mai verbi nell'URI (prenotare = POST, annullare = DELETE); 5 rappresentazioni e codici. URI: plurali, minuscoli, senza estensioni, filtri in query, identità nel path, prefisso /api.
| Metodo | URI | Successo | Errori |
|---|---|---|---|
GET |
/api/aule |
200 + array |
500 |
GET |
/api/aule/{id} |
200 |
404 |
GET |
/api/prenotazioni?aula=&giorno= |
200 + array (anche []) |
400 |
GET |
/api/prenotazioni/{id} |
200 |
404 |
POST |
/api/prenotazioni |
201 + Location + oggetto |
400, 415, 422 aula inesistente, 409 orario occupato |
DELETE |
/api/prenotazioni/{id} |
204 |
404 |
| altri | 405 + Allow |
Metodi: sicuro = nessun cambiamento di stato; idempotente = ripetere ha lo stesso effetto. GET sicuro e idempotente; PUT e DELETE idempotenti; POST no (due richieste possono creare due elementi; qui la seconda dà 409). DELETE ripetuta: effetto uguale, risposta 404. PATCH non garantito.
Codici: 200 lettura; 201 + Location creazione; 204 senza corpo; 400 richiesta malformata (JSON non valido, campo mancante, formato data, ora_inizio non prima di ora_fine, filtro non numerico); 404 risorsa inesistente; 405 metodo non previsto, con Allow obbligatorio; 409 conflitto con lo stato (orario occupato); 415 non è application/json; 422 sintassi giusta ma contenuto impossibile (aula_id inesistente); 500 guasto, dettaglio solo nel log. 4xx = errore del client, 5xx = del server. 404 se la risorsa non esiste, 405 se esiste ma non con quel metodo.
JSON: nomi delle colonne in snake_case, date e ore ISO 8601 come stringhe, collezione = array, errore {"errore": "..."}, id assegnato dal server (ignorato se il client lo manda), UTF-8.
{"id": 1, "aula_id": 1, "giorno": "2026-10-19", "ora_inizio": "09:00", "ora_fine": "11:00", "studente": "Rossi"}Implementazione. Model invariato (Aula, Prenotazione, DAO); la view è un serializzatore (Gson, dipendenza nel pom). Package it.unipd.aule.rest, servlet AuleServlet su /api/aule/* e PrenotazioniServlet su /api/prenotazioni/*. Json: Gson con FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES (aulaId aula_id) e disableHtmlEscaping(); invia(resp, stato, corpo) imposta stato, Content-Type e codifica prima del corpo; nonConsentito = 405 + Allow; id("/12") con Integer.parseInt, NumberFormatException 404.
Dispatch sul path: getPathInfo() = null o "/" collezione; "/12" elemento 12; "/abc" 404. Il container sceglie doGet/doPost/doDelete, dentro si guarda il path.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
String path = req.getPathInfo();
if (Json.eCollezione(path)) {
Integer aulaId = req.getParameter("aula") == null ? null : Integer.valueOf(req.getParameter("aula"));
Json.invia(resp, 200, dao.lista(aulaId, req.getParameter("giorno")));
return;
}
Prenotazione p = dao.perId(Json.id(path));
if (p == null) Json.errore(resp, 404, "prenotazione inesistente");
else Json.invia(resp, 200, p);
} catch (NumberFormatException e) {
Json.errore(resp, 404, "risorsa inesistente");
} catch (SQLException e) {
log("errore SQL", e);
Json.errore(resp, 500, "errore interno");
}
}doPost, in ordine: getPathInfo() non è la collezione 405 + Allow: GET, DELETE; Content-Type non JSON 415; Json.GSON.fromJson(req.getReader(), Prenotazione.class), null 400 "corpo vuoto", JsonSyntaxException 400; p.setId(0); p.valida() 400; dao.crea(p): PrenotazioneException SOVRAPPOSIZIONE 409, AULA_INESISTENTE 422; successo: header Location: /api/prenotazioni/{id} e 201 con l'oggetto. doDelete: path collezione 405 + Allow: GET, POST; dao.elimina(id) vero 204, falso 404.
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (!Json.eCollezione(req.getPathInfo())) { Json.nonConsentito(resp, "GET, DELETE"); return; }
String tipo = req.getContentType();
if (tipo == null || !tipo.startsWith("application/json")) {
Json.errore(resp, 415, "serve Content-Type: application/json");
return;
}
req.setCharacterEncoding("UTF-8");
try {
Prenotazione p = Json.GSON.fromJson(req.getReader(), Prenotazione.class);
if (p == null) { Json.errore(resp, 400, "corpo vuoto"); return; }
p.setId(0); // l'id lo assegna il server
p.valida();
dao.crea(p);
resp.setHeader("Location", req.getContextPath() + "/api/prenotazioni/" + p.getId());
Json.invia(resp, 201, p);
} catch (JsonSyntaxException e) {
Json.errore(resp, 400, "il corpo non è un JSON valido");
} catch (IllegalArgumentException e) {
Json.errore(resp, 400, e.getMessage());
} catch (PrenotazioneException e) {
Json.errore(resp, e.getTipo() == PrenotazioneException.Tipo.SOVRAPPOSIZIONE ? 409 : 422, e.getMessage());
} catch (SQLException e) {
log("errore SQL", e);
Json.errore(resp, 500, "errore interno");
}
}POST /api/prenotazioni HTTP/1.1
Content-Type: application/json
{"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}
HTTP/1.1 201
Location: /api/prenotazioni/5
{"id":5,"aula_id":1,"giorno":"2026-10-19","ora_inizio":"11:00","ora_fine":"12:30","studente":"Neri"}- Stessa richiesta con
ora_inizio10:00 eora_fine12:00409{"errore":"l'aula è già prenotata in quell'orario"}.GET /api/aule/9404;GET /api/prenotazioni?giorno=19-10-2026400;POSTconaula_id99422;Content-Type: text/plain415;POST /api/prenotazioni/1405conAllow: GET, DELETE. - Prova da riga di comando:
curl -i -X POST URL -H "Content-Type: application/json" -d '{...}'.
Fuori dall'esempio: HATEOAS (livello 3 di Richardson), autenticazione con Authorization e 401/403, CORS, paginazione, cache con ETag.
Errori tipici
- Verbi negli URI;
GETche modifica;200con l'errore nel corpo;[]sostituito da404. - Dimenticare
201+Location, oAllownel405; confondere404e405. - Fidarsi del JSON (tipi, formato,
iddal client); mostrare stack trace al client. - Stato di sessione nel servizio; corpo scritto prima dello stato.
Domande d'esame (tracce)
- API delle prenotazioni: risorse
auleeprenotazioni,GET,POST(201,409,422),DELETE(204,404),405+Allow, JSON con i nomi delle colonne. POSTnon idempotente (due elementi),DELETEidempotente (seconda risposta404, stesso stato finale).- Una servlet su
/api/prenotazioni/*:getPathInfo()nullo o"/"= collezione,"/12"= elemento.
Collegamenti: Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioniUna servlet è una classe Java che un container (Apache Tomcat: connettore HTTP Coyote, contenitore Catalina, compilatore JSP Jasper) istanzia una sola volta e chiama per ogni richiesta, ciascuna in un thread del pool. Ciclo di vita: caricamento e costruzione, init() una volta, service() per ogni richiesta (HttpServlet lo smista su doGet, doPost, doPut, doDelete), destroy() allo spegnimento. La richiesta (HttpServletRequest) dà metodo, parametri (query string e corpo del form), header, path (contextPath, servletPath, pathInfo) e corpo; la risposta (HttpServletResponse) si costruisce con stato, header e corpo (PrintWriter), con sendRedirect (302 + Location) o sendError. Il mapping si dichiara con @WebServlet o in WEB-INF/web.xml (che prevale). Poiché esiste una sola istanza e molti thread, i campi di istanza sono condivisi: lo stato della richiesta sta in variabili locali, quello condiviso in oggetti thread-safe (AtomicInteger, DataSource). HTTP è senza stato, quindi si usano le sessioni: la servlet crea un HttpSession, tiene i dati sul server e manda al browser solo l'identificatore nel cookie JSESSIONID (Set-Cookie con HttpOnly, Secure, SameSite, Path secondo RFC 6265); al login si cambia l'identificatore (changeSessionId) contro la session fixation. I filtri (Filter, FilterChain) elaborano richiesta e risposta prima e dopo le servlet: codifica, autenticazione, log. Il war ha la struttura WEB-INF/classes, WEB-INF/lib, WEB-INF/web.xml, META-INF/context.xml; l'accesso ai dati usa un DataSource con pool di connessioni dichiarato in context.xml e cercato con JNDI, e un DAO con PreparedStatement e try-with-resources.Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni →, 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 →, Esercizio - Progettazione dell'API REST delle prenotazioni (applicazione web).