Les contrôleurs gérent la communication avec les clients.
La communication se fait en utilisant le protocole HTTP.
Nous allons brièvement rappeler le fonctionnement du protocole HTTP.
Ensuite nous verrons comment les Servlet
permettent d'écrire des serveurs Web en Java.
Il ne sont pas directement manipulés en Spring mais ils seront présents sous le capot.
Le protocole HTTP fonctionne sur un principe requête-réponse.
Le protocole HTTP est stateless. C'est à dire qu'il ne conserve aucune information sur les requêtes précédentes.
GET /toto.html HTTP/1.1 Host: localhost:8989 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:93.0) Gecko/20100101 Firefox/93.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Cookie: username-localhost-8888="2|1:0|10:1635022841... Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1
Structure de la requête:
Le header contient les informations nécessaires pour décoder le body.
Plusieurs méthodes possibles : GET, POST, PUT, PATCH, DELETE ...
$curl -i -X GET http://www.google.com/
HTTP/1.1 200 OK Date: Mon, 25 Oct 2021 22:36:26 GMT Expires: -1 Cache-Control: private, max-age=0 Content-Type: text/html; charset=ISO-8859-1 P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info." Server: gws X-XSS-Protection: 0 X-Frame-Options: SAMEORIGIN Set-Cookie: NID=511=As...ODLU; path=/; domain=.google.com; HttpOnly Accept-Ranges: none Vary: Accept-Encoding Transfer-Encoding: chunked <!doctype html><html itemscope="" itemtype="http://schema.org/WebPage"...
Structure de la réponse:
Exemples:
GET /universite/qui-sommes-nous/ HTTP/1.1 Host: www.univ-gustave-eiffel.fr User-Agent: ...
Quand on clique sur le bouton Compute, le navigateur envoie la requête POST suivante:
POST /App/rectangle HTTP/1.1 ... content-length : 17 content-type : application/x-www-form-urlencoded height=15&width=5
Les données du formulaire sont encodées dans le body de la requête.
POST /App/rectangle HTTP/1.1 ... height=15&width=5
La ressource du POST et les noms des champs viennent du formulaire.
<form action="/App/rectangle" method="post"> <h2>Rectangle area calculator</h2> <div><label for="h">Height:</label> <input id="h" type="text" name="height"></div> <div><label for="w">Width:</label> <input type="text" id="w" name="width"></div> <div><button type="submit">Compute</button></div>
La classe Servlet
de JEE permet d'écrire le code qui transforme une requête HTTP en la réponse correspondante.
JEE donne une spécification que doivent respecter les serveurs Web qui exécutent ce code. La version courante de la spécification est Jakarta Servlet Specification, Version 5.0
Ils existent plusieurs serveurs web respectant la Jakarta Servlet Specification: Tomcat, JBoss, Glassfish ...
Par défaut, Spring utilisse Tomcat et c'est donc le serveur web avec lequel nous allons jouer pendant le TP.
@WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { PrintWriter writer = response.getWriter(); writer.println("<!DOCTYPE html><html><h1>Hello world!</h1></html>"); writer.flush(); } }
HttpServletRequest
représent la requête entrante et HttpServletResponse
représente la réponse sortante.@WevServlet("/hello")
spécifie la route des requêtes qui vont être traitées par ce Servlet
.doPost
.Il faut bien se souvenir que le body de notre réponse n'est qu'une suite d'octets. Le navigateur regarde dans les headers de la réponse (i.e., Content-Type) pour savoir comment l'interpréter.
Quand il n'y pas d'information, il devine:
@WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html ; charset=utf-8"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); writer.println("<!DOCTYPE html><html><h1>Hello world!</h1></html>"); writer.flush(); } }
La spécification dit qu'une seule Servlet est instanciée par le serveur (i.e. Tomcat ici).
Donc toutes les requêtes sur la route /hello sont traitées par le même objet HelloServlet
.
Par contre, les requêtes vont être traitées dans des threads différents!
Donc l'objet HelloServlet doit être thread-safe.
@WebServlet("/hellocount") public class HelloCountServletWrong extends HttpServlet { private int count = 0; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { count++; response.setContentType("text/html ; charset=utf-8"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); writer.println("<!DOCTYPE html><html><h1>Hello world! ("+count+"th times)</h1></html>"); writer.flush(); } }
Data-race sur le champ count
!
@WebServlet("/hellocount") public class HelloCountServlet extends HttpServlet { private final Object lock = new Object(); private int count = 0; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { var countSnaptshot=0; synchronized (lock){ countSnaptshot=++count; } response.setContentType("text/html ; charset=utf-8"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); writer.println("<!DOCTYPE html><html><h1>Hello world!"+ "("+countSnaptshot+"th times)"+ "</h1></html>"); writer.flush(); } }
Pourquoi faire un snapshot ne pas garder le lock pendant l'appel à writer.println
?
Stateless veut dire qu'aucune information sur les requêtes précédentes n'est stockée par le protocole.
Toute est dans la requête !
Alors comment s'avoir:
On utilise Cookie qui est un champ des headers qui est une suite d'association clé=valeur séparées par des point-virgules.
Si dans une réponse HTTP, on inclue un header un champ:
... Set-Cookie: montokenamoi=AZADAZEZFSDFSD ...
Le navigateur web qui a reçu cette réponse, mettra ce cookie dans le champ dans ses prochaines requêtes vers cette application.
... Cookie: montokenamoi=AZADAZEZFSDFSD ...
Le Servlet garde l'association entre les tokens qu'il a déjà donnés et les infos de ce visiteur. Soit dans une Hashmap du Servlet soit en BD (sur une déploiement distribué pas le choix BD).
Quand un requête arrive:
UUID.randomUUID
jusqu'à avoir un token qu'on n'a pas déjà donné et on le met le champ Cookie de la réponse.Pourquoi pas des tokens séquentiels ?