Cette page rassemble les recettes pour exploiter Foy au-delà du « hello world ». Elle suit les chapitres canoniques de la spec Jakarta Servlet 6.1.
Filtres
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class RequestIdFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
var id = java.util.UUID.randomUUID().toString();
req.setAttribute("requestId", id);
((jakarta.servlet.http.HttpServletResponse) resp).addHeader("X-Request-Id", id);
chain.doFilter(req, resp);
}
}
L’ordre des filtres suit l’ordre déclaré dans web.xml (<filter-mapping>) ou, à défaut, l’ordre alphabétique des classes — voir Référence pour le détail.
Listeners
@WebListener couvre ServletContextListener, HttpSessionListener, ServletRequestListener et leurs variantes *AttributeListener. Tous sont enrôlés par ListenerRegistry à partir des beans découverts par Vauban.
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
@WebListener
public class StartupHook implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("started-at", java.time.Instant.now());
}
}
Les évènements contextInitialized / contextDestroyed se déclenchent via FoyChappeBoot.Mounted.fireContextInitialized() / fireContextDestroyed().
Async (AsyncContext)
Foy implémente request.startAsync(), AsyncContext.dispatch(…), complete(), et setTimeout. Chaque requête est portée par un virtual thread — le passage en async ne change pas le coût (pas de pool plateforme à protéger).
@WebServlet(value = "/long", asyncSupported = true)
public class LongServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
var ctx = req.startAsync();
ctx.setTimeout(5_000);
Thread.startVirtualThread(() -> {
try {
Thread.sleep(2_000);
resp.getWriter().write("done");
ctx.complete();
} catch (Exception e) {
ctx.complete();
}
});
}
}
|
Quelques cas pointus du TCK 6.1 sur |
Upload de fichiers (@MultipartConfig)
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
@WebServlet("/upload")
@MultipartConfig(maxFileSize = 10 * 1024 * 1024)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
Part part = req.getPart("file");
try (var in = part.getInputStream()) {
in.transferTo(java.nio.file.Files.newOutputStream(
java.nio.file.Path.of("/tmp", part.getSubmittedFileName())));
}
resp.setStatus(204);
}
}
Le parsing multipart se fait en streaming sur le ServletInputStream adossé au body Chappe — pas de copie intermédiaire en mémoire pour les gros uploads.
Sessions
Le SessionManager interne s’appuie par défaut sur InMemorySessionStore. Le timeout est piloté par FoyChappeBoot.builder().sessionTimeoutSeconds(…) ou par <session-config> dans web.xml.
Pour une session distribuée ou persistée, implémenter io.vidocq.foy.spi.session.SessionStore (voir Référence).
RequestDispatcher : forward et include
@WebServlet("/router")
public class Router extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
var rd = req.getRequestDispatcher("/target?key=42");
rd.forward(req, resp); // ou rd.include(req, resp);
}
}
ForwardedRequest et IncludedRequest injectent les attributs standard (jakarta.servlet.forward., jakarta.servlet.include.) et réutilisent le routage Chappe.
Virtual hosts et déploiement WAR
Foy peut être démarré :
-
programmatiquement via
FoyChappeBoot.builder()— un container par contextPath ; -
en empilant plusieurs
Mountedsur le mêmeChappeMountPointpour servir plusieurs applications sur le même connecteur ; -
à terme via packaging WAR —
// TODO@user: documenter la commande de déploiement WAR quand stable.
Intégration CDI via Vauban
Quand foy-cdi-vauban est sur le classpath, Foy expose les beans suivants :
| Bean | Scope |
|---|---|
|
|
|
|
|
|
Toute Servlet ou Filter peut donc utiliser @Inject :
import jakarta.inject.Inject;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
@WebServlet("/me")
public class MeServlet extends HttpServlet {
@Inject HttpServletRequest request;
@Inject MyBusinessService service;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
resp.getWriter().write(service.greet(request.getRemoteUser()));
}
}
Toute la résolution est faite à la compilation par Vauban — pas de proxy dynamique runtime, pas de réflexion à chaud.
Sécurité applicative
-
<security-constraint>duweb.xmlest appliqué parSecurityConstraintEnforcer. -
BasicAuthenticatorcouvre l’authentification HTTP Basic. -
AnonymousSecurityProviderest le fallback ouvert (développement uniquement). -
Form-based et Digest :
// TODO@user: documenter une fois stables.
Pour un provider custom, implémenter io.vidocq.foy.spi.security.SecurityProvider et l’enregistrer comme bean CDI.
Cohabitation avec Cassini
Foy et Cassini partagent le même runtime Chappe. Sur le même ChappeMountPoint, on peut monter /app (Foy) et /api (Cassini) — chacun reste maître de son contextPath, les deux containers n’interagissent pas.