Java EE / Jakarta EE
Jakarta Servlet 6 : servlets, filtres, sessions et tier web
Débutant10 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Les servlets sont l'API HTTP de plus bas niveau de Jakarta EE et le socle sur lequel reposent Faces, Jakarta REST et Spring MVC. Ce guide explique comment le conteneur associe les requêtes aux servlets, le cycle de vie et le modèle de threads, comment lire les requêtes et écrire les réponses, et comment filtres, listeners, sessions et traitement asynchrone s'articulent, avec des exemples qui compilent avec Servlet 6.0.
Ce que vous allez apprendre
- Expliquer le cycle de vie d'une servlet et pourquoi elle doit être thread-safe
- Associer des URL à des servlets et à des filtres par annotations ou via web.xml
- Lire paramètres, en-têtes, corps et uploads multipart, et écrire des réponses avec le bon encodage
- Utiliser correctement HttpSession et les cookies, en connaissant leurs implications de sécurité
- Appliquer filtres, listeners et traitement asynchrone aux préoccupations transverses
Toute requête HTTP qui atteint une application Jakarta EE passe par le conteneur de servlets, que le code qui la traite au final soit une page Faces, une ressource Jakarta REST, un contrôleur Spring ou une simple servlet. La spécification Servlet définit comment le conteneur reçoit une requête, choisit le composant qui va la traiter, expose la requête et la réponse sous forme d'objets Java, suit les sessions et vous permet d'intercepter le flux. Dans un projet moderne, vous écrirez rarement une méthode doGet à la main ; mais le jour où un bug d'ordre des filtres, un problème d'encodage de caractères ou un constat de fixation de session atterrit sur votre bureau, c'est à ce niveau que se trouve la réponse.
Le conteneur et le cycle d'une requête
Un conteneur de servlets (Tomcat, Jetty, Undertow dans WildFly, le moteur web d'Open Liberty, etc.) écoute sur un port, analyse le HTTP et construit pour chaque requête un HttpServletRequest et un HttpServletResponse. Il parcourt ensuite les applications web déployées pour trouver celle dont le context path correspond (/shop pour shop.war), choisit la servlet dont l'URL pattern correspond le mieux au reste du chemin, exécute les filtres applicables et appelle enfin la méthode service de la servlet sur un thread de son pool. Quand cette méthode rend la main, la réponse est validée (committed) et le thread retourne dans le pool.
La correspondance des URL patterns suit des règles fixes, appliquées dans cet ordre : correspondance exacte (/orders/export), plus long préfixe de chemin (/orders/*), extension (*.xhtml), et enfin la servlet par défaut (/). La servlet par défaut sert les fichiers statiques du WAR ; le pattern /* n'est pas équivalent à / : il capture tout, ressources statiques comprises, ce qui est une erreur de configuration classique.
Écrire une servlet
Une servlet est une classe qui étend jakarta.servlet.http.HttpServlet et redéfinit les méthodes correspondant aux méthodes HTTP qu'elle prend en charge. Depuis Servlet 3.0, on l'enregistre avec une annotation ; web.xml est facultatif.
package shop.web;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet(urlPatterns = "/orders/*", loadOnStartup = 1)
public class OrderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String id = req.getPathInfo(); // "/42" for GET /shop/orders/42
if (id == null || id.length() < 2) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "order id required");
return;
}
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8"); // must precede getWriter()
resp.getWriter().printf("{\"id\":%s,\"status\":\"NEW\"}", id.substring(1));
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String sku = req.getParameter("sku"); // form field or query parameter
int qty = Integer.parseInt(req.getParameter("qty"));
// ... create the order ...
resp.setStatus(HttpServletResponse.SC_CREATED);
resp.setHeader("Location", req.getContextPath() + "/orders/43");
}
}
La classe de base aiguille GET vers doGet, POST vers doPost, et ainsi de suite ; l'implémentation par défaut de chacune renvoie 405 Method Not Allowed, si bien que les méthodes non prises en charge sont traitées correctement sans code supplémentaire. HEAD est dérivé automatiquement de doGet. OPTIONS et TRACE ont aussi un comportement par défaut — beaucoup d'équipes désactivent TRACE au niveau du serveur pour des raisons de sécurité.
Cycle de vie et threads
Le conteneur instancie la servlet une seule fois (à la première requête, ou au démarrage si loadOnStartup est renseigné), appelle init(ServletConfig) une fois, puis service pour chaque requête, potentiellement depuis des dizaines de threads simultanément, et enfin destroy() quand l'application est retirée du serveur. Trois conséquences en découlent :
- Une servlet ne doit pas conserver d'état propre à une requête dans ses champs. Un champ d'instance
private Order current;est une data race qui n'apparaît qu'en charge. - Les ressources coûteuses et thread-safe (une
DataSource, un template déjà analysé, un bean CDI injecté) ont leur place dans des champs initialisés dansinitou par injection. - Depuis Servlet 3.0, les servlets d'une application où CDI est activé sont des cibles d'injection, donc
@Inject OrderService service;fonctionne — mais la servlet elle-même n'est pas un bean CDI et n'a pas de scope CDI.
L'ancienne interface SingleThreadModel, qui demandait au conteneur de sérialiser les appels, a été dépréciée dans Servlet 2.4 puis enfin supprimée dans la 6.0 ; elle n'a jamais résolu le problème, puisque les sessions et les attributs de contexte restaient partagés.
La requête et la réponse en pratique
HttpServletRequest vous donne la méthode, l'URI découpée en morceaux (getContextPath(), getServletPath(), getPathInfo(), getQueryString()), les en-têtes, les cookies, les paramètres et le corps. Quelques points sont une source régulière de bugs :
- Les paramètres fusionnent la query string et le corps d'une requête au format formulaire.
getParameterrenvoie la première valeur ; utilisezgetParameterValuespour les noms répétés. AppelergetParametersur unPOSTlit et consomme le corps : vous ne pouvez plus ensuite le lire comme un flux. - L'encodage d'un corps de formulaire vaut ISO-8859-1 par défaut si la requête n'indique pas de charset, ce que les navigateurs omettent généralement. Définissez la valeur par défaut une fois pour toute l'application plutôt que d'appeler
setCharacterEncoding("UTF-8")dans chaque servlet :
<!-- web.xml, Servlet 4.0+ -->
<request-character-encoding>UTF-8</request-character-encoding>
<response-character-encoding>UTF-8</response-character-encoding>
- Les uploads multipart exigent
@MultipartConfigsur la servlet (ou l'équivalent dansweb.xml) ;req.getPart("file")renvoie alors unPartavec un flux, une taille et le nom du fichier envoyé. FixezmaxFileSizeetmaxRequestSizeà des limites raisonnables — par défaut, il n'y en a aucune. - Les attributs de requête (
setAttribute/getAttribute) sont l'espace de travail propre à chaque requête : ils servent à passer des données d'un filtre ou d'une servlet à la JSP ou à la vue Faces vers laquelle on fait un forward.
Côté réponse, l'ordre compte : statut, en-têtes et type de contenu doivent être définis avant que la réponse soit validée, ce qui arrive dès que le buffer est plein ou que vous faites un flush. Ensuite, sendRedirect et setHeader sont ignorés silencieusement ou lèvent une IllegalStateException. Une redirection (sendRedirect) renvoie au client un 302 avec une nouvelle URL ; un forward (req.getRequestDispatcher("/WEB-INF/views/order.jsp").forward(req, resp)) transfère le contrôle à une autre ressource à l'intérieur du serveur, sans que le client le sache. Placez les vues sous WEB-INF pour qu'elles ne puissent pas être demandées directement.
Servlet 6.0 ajoute getRequestId(), getProtocolRequestId() (l'identifiant de stream HTTP/2) et getServletConnection(), bien utiles pour corréler les lignes de log avec la couche réseau.
Filtres et listeners
Un filtre enveloppe le traitement des requêtes qui lui correspondent. Il s'exécute avant la servlet, peut décider de poursuivre ou non la chaîne, et voit passer la réponse au retour. Les filtres sont l'outil adapté pour les contrôles d'authentification, la journalisation des requêtes, la compression, les en-têtes CORS et de sécurité, ou pour envelopper la requête ou la réponse afin d'en modifier le comportement.
import java.io.IOException;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebFilter(urlPatterns = "/*")
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
var resp = (HttpServletResponse) response;
resp.setHeader("X-Content-Type-Options", "nosniff");
resp.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
long start = System.nanoTime();
try {
chain.doFilter(request, response); // hand over to the next filter or the servlet
} finally {
var req = (HttpServletRequest) request;
System.out.printf("%s %s -> %d in %d ms%n", req.getMethod(), req.getRequestURI(),
resp.getStatus(), (System.nanoTime() - start) / 1_000_000);
}
}
}
Deux détails piègent souvent. D'abord, les filtres déclarés par annotation n'ont pas d'ordre défini ; si l'ordre compte (l'authentification avant la journalisation, par exemple), déclarez-les dans web.xml, où ils s'exécutent dans l'ordre de déclaration, ou utilisez un filtre unique qui délègue. Ensuite, un filtre n'est invoqué par défaut que pour les dispatches REQUEST ; ajoutez dispatcherTypes = {DispatcherType.REQUEST, DispatcherType.FORWARD} s'il doit aussi voir les requêtes transmises par forward, et DispatcherType.ERROR pour le rendu des pages d'erreur.
Les listeners réagissent aux événements du conteneur. ServletContextListener (@WebListener) exécute du code au démarrage et à l'arrêt de l'application : c'était l'endroit traditionnel des initialisations ponctuelles avant l'arrivée de l'événement CDI @Observes @Initialized(ApplicationScoped.class). HttpSessionListener compte les sessions ou fait le ménage à leur expiration, et ServletRequestListener peut mettre en place un contexte propre à chaque requête, comme un MDC de logging.
Sessions et cookies
HTTP est sans état ; HttpSession vous fournit un état côté serveur, indexé par un identifiant que le conteneur envoie au navigateur, presque toujours sous la forme d'un cookie nommé JSESSIONID. req.getSession() crée une session s'il n'en existe pas ; req.getSession(false) renvoie null à la place, ce qui est ce qu'il vous faut dans du code qui se contente de lire. Les attributs stockés en session doivent être Serializable si le serveur persiste ou réplique les sessions dans un cluster.
Les règles de sécurité autour des sessions sont bien établies, et l'API servlet les prend toutes en charge :
- Régénérez l'identifiant après le login avec
req.changeSessionId()(Servlet 3.1+) pour contrer la fixation de session. - Marquez le cookie
HttpOnlyetSecurepour que les scripts ne puissent pas le lire et qu'il ne circule jamais en clair. Configurez-le dansweb.xmlsous<session-config><cookie-config>ou par programmation viaServletContext.getSessionCookieConfig(). - Définissez
SameSite. Servlet 6.0 a enfin ajoutéCookie.setAttribute("SameSite", "Lax")et leSessionCookieConfig.setAttributecorrespondant, ce qui rend inutiles les réglages propres à chaque serveur. - Gardez un timeout court et invalidez la session à la déconnexion avec
session.invalidate(). - Gardez des sessions légères. L'état de session, c'est de la mémoire sur le serveur et, en cluster, du trafic réseau à chaque modification. Stocker des graphes d'entités ou le contenu de fichiers en session est la cause la plus fréquente de pression mémoire dans ce type d'application web.
La réécriture d'URL (;jsessionid=… ajouté aux liens) servait de solution de repli pour les clients sans cookies ; Servlet 6.0 a supprimé l'API isRequestedSessionIdFromUrl, et vous devriez désactiver ce mécanisme avec <tracking-mode>COOKIE</tracking-mode>, car des identifiants dans les URL fuient par les logs et les en-têtes Referer.
Traitement asynchrone
Par défaut, un thread du conteneur est mobilisé pendant toute la durée d'une requête. Quand le travail est lent parce qu'il attend autre chose — un appel distant, une longue requête en base, un message qui n'est pas encore arrivé —, ce thread est gaspillé. Servlet 3.0 a introduit AsyncContext pour que la requête puisse être mise en attente puis terminée plus tard depuis un autre thread :
import java.io.IOException;
import jakarta.annotation.Resource;
import jakarta.enterprise.concurrent.ManagedExecutorService;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
@WebServlet(urlPatterns = "/report", asyncSupported = true)
public class ReportServlet extends HttpServlet {
@Resource // Jakarta Concurrency: a container-managed executor
ManagedExecutorService executor;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext ctx = req.startAsync();
ctx.setTimeout(30_000);
executor.submit(() -> {
String report = buildReport(); // slow work off the request thread
try {
ctx.getResponse().setContentType("text/plain;charset=UTF-8");
ctx.getResponse().getWriter().write(report);
} catch (IOException ignored) {
} finally {
ctx.complete(); // releases the request; forgetting this leaks it
}
});
}
}
Chaque filtre de la chaîne doit lui aussi déclarer asyncSupported = true, sans quoi startAsync lève une exception. Servlet 3.1 a ajouté les E/S non bloquantes (ReadListener/WriteListener) pour streamer de gros corps sans bloquer de thread ; @Suspended AsyncResponse et les Server-Sent Events de Jakarta REST s'appuient sur ces mécanismes, et c'est donc en général à travers cette API que vous les utiliserez.
Gestion des erreurs et contraintes de sécurité
Les exceptions non interceptées et les appels à sendError sont associés à des pages d'erreur déclarées dans web.xml, selon le code de statut ou le type d'exception :
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/errors/not-found.jsp</location>
</error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/WEB-INF/errors/failure.jsp</location>
</error-page>
Sans elles, le conteneur affiche sa propre page, qui contient sur certains serveurs une stack trace — une fuite d'informations à colmater avant la mise en production.
La sécurité déclarative se trouve elle aussi dans web.xml : les éléments <security-constraint> associent des URL patterns et des méthodes HTTP à des rôles, et <login-config> choisit une méthode d'authentification (FORM, BASIC ou un mécanisme d'authentification Jakarta Security). Une contrainte avec <auth-constraint/> sans aucun rôle interdit l'accès à tout le monde, et une contrainte avec <user-data-constraint>CONFIDENTIAL</user-data-constraint> impose HTTPS. Les vérifications par programmation utilisent req.isUserInRole("admin"), req.getUserPrincipal() et, depuis Servlet 3.0, req.login, req.logout et req.authenticate. Attention : les contraintes ne couvrent que les méthodes que vous listez — ou toutes les méthodes si vous n'en listez aucune — et l'oubli d'une méthode a historiquement permis des contournements ; préférez donc n'en lister aucune (tout protéger) et n'utiliser <http-method-omission> qu'en connaissance de cause.
Pages, Expression Language et ce que sont devenues les vues
Jakarta Server Pages (JSP) et la Jakarta Standard Tag Library font toujours partie de la plateforme et fonctionnent toujours, mais c'est une technologie de vue héritée : les pages sont compilées en servlets, elles mélangent trop facilement balisage et logique, et Faces 4.0 ne les accepte plus comme couche de vue. L'Expression Language (EL 5.0) popularisé par JSP est en revanche bien vivant, utilisé par Faces et par les beans @Named de CDI. Pour une nouvelle interface, vous choisirez en général entre Jakarta Faces pour des UI à composants rendues côté serveur et un front-end JavaScript qui dialogue avec des endpoints Jakarta REST ; l'un comme l'autre, tout comme Spring MVC, reposent au bout du compte sur des servlets.
Erreurs fréquentes
- Des champs d'instance qui portent des données de requête. Le symptôme : un utilisateur voit de temps en temps les données d'un autre. Correctif : uniquement des variables locales et des attributs de requête.
- Écrire dans la réponse avant d'avoir fixé le statut. Une fois les octets envoyés, impossible de rediriger ou de modifier les en-têtes. Calculez d'abord, écrivez en dernier, et gardez un buffer de réponse assez grand pour les petites pages.
- Lire le corps deux fois. Après
getParameterougetInputStream, le corps a disparu. Si un filtre en a besoin, enveloppez la requête dans unHttpServletRequestWrapperqui met les octets en cache. - Un mapping
/*sur une servlet applicative. Il avale le CSS, les images et les mappings des servlets Faces ou REST. Utilisez un préfixe (/api/*) ou des chemins exacts. - Compter sur l'ordre des annotations pour les filtres. Déclarez dans
web.xmlles filtres sensibles à l'ordre. - Des sessions trop lourdes et l'absence de
HttpOnly/Secure/SameSite. Ces points figurent dans tous les rapports de test d'intrusion ; configurez le cookie une fois pour toutes dansweb.xml. - Oublier
AsyncContext.complete(). La requête reste bloquée jusqu'à l'expiration du timeout asynchrone et le conteneur journalise un avertissement ; le client, lui, reçoit une erreur au bout d'un très long moment.
À retenir
- Le conteneur de servlets analyse le HTTP, associe l'URL à une servlet, exécute la chaîne de filtres et appelle
servicesur un thread du pool ; Faces, Jakarta REST et Spring MVC reposent tous sur ce mécanisme. - Une seule instance de servlet sert toutes les requêtes en parallèle : ne gardez pas d'état dans les champs.
- Définissez statut et en-têtes avant d'écrire le corps ; utilisez le forward pour un dispatch interne et la redirection quand le client doit voir une nouvelle URL.
- Configurez une fois pour toutes dans
web.xmll'encodage des requêtes et des réponses, les attributs du cookie de session, les pages d'erreur et les contraintes de sécurité ; réservez les annotations aux servlets, filtres et listeners eux-mêmes. - Les filtres prennent en charge les comportements transverses ; déclarez-les dans
web.xmlquand leur ordre compte. - Le traitement asynchrone libère les threads du conteneur pendant les tâches lentes, mais chaque filtre de la chaîne doit le prendre en charge et vous devez toujours appeler
complete(). - Servlet 6.0 a supprimé des API héritées, ajouté la prise en charge de
SameSitevia les attributs de cookie et introduit des identifiants de requête et de connexion.
Sources et lectures complémentaires
Questions fréquentes
- Une seule instance de servlet est-elle partagée entre toutes les requêtes ?
- Oui. Le conteneur crée une seule instance par déclaration de servlet et appelle sa méthode service depuis de nombreux threads en parallèle. Tout ce qui est stocké dans un champ d'instance est un état partagé : gardez les données propres à la requête dans des variables locales ou des attributs de requête, et rendez tout champ partagé immuable ou correctement synchronisé.
- Ai-je encore besoin de web.xml ?
- Pas pour déclarer servlets, filtres et listeners : les annotations s'en chargent depuis Servlet 3.0. web.xml reste l'endroit pour ce que les annotations ne savent pas exprimer ou qui varie selon l'environnement : contraintes de sécurité, pages d'erreur, délai d'expiration des sessions, fichiers d'accueil, encodages par défaut et ordre des filtres déclarés dans différentes bibliothèques.
- Qu'est-ce qui a changé entre Servlet 5.0 et Servlet 6.0 ?
- Servlet 6.0 a supprimé des API dépréciées depuis des années — SingleThreadModel, HttpSessionContext, HttpUtils et les méthodes liées aux identifiants de session dans l'URL —, ajouté Cookie.setAttribute pour des attributs comme SameSite, introduit des identifiants de requête et des informations de connexion, et durci le traitement des URI pour que les chemins ambigus, comme les slashs encodés, soient rejetés par défaut.
- Faut-il écrire des servlets brutes ou utiliser un framework ?
- Pour le code applicatif, utilisez Jakarta REST pour les API et Faces ou un framework front-end pour les interfaces ; tous deux reposent sur l'API servlet et gèrent pour vous le routage, la conversion et la négociation de contenu. Écrivez directement servlets et filtres pour les sujets d'infrastructure : authentification, journalisation, endpoints de santé et intégration avec l'existant.