This page collects recipes for using Foy beyond "hello world". It follows the canonical chapters of Jakarta Servlet 6.1.
Filters
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);
}
}
Filter ordering follows web.xml (<filter-mapping>) order or, if absent, the alphabetical order of class names — see Reference for details.
Listeners
@WebListener covers ServletContextListener, HttpSessionListener, ServletRequestListener and their *AttributeListener variants. All are enrolled by ListenerRegistry from beans discovered by 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());
}
}
contextInitialized / contextDestroyed events fire through FoyChappeBoot.Mounted.fireContextInitialized() / fireContextDestroyed().
Async (AsyncContext)
Foy implements request.startAsync(), AsyncContext.dispatch(…), complete(), and setTimeout. Each request runs on a virtual thread — switching to async carries no extra cost (no platform pool to protect).
@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();
}
});
}
}
|
A few edge cases of the 6.1 TCK around |
File upload (@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);
}
}
Multipart parsing streams over the ServletInputStream backed by the Chappe body — no in-memory copy for large uploads.
Sessions
The internal SessionManager is backed by InMemorySessionStore by default. Timeout is driven by FoyChappeBoot.builder().sessionTimeoutSeconds(…) or by <session-config> in web.xml.
For a distributed or persistent session, implement io.vidocq.foy.spi.session.SessionStore (see Reference).
RequestDispatcher: forward and 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); // or rd.include(req, resp);
}
}
ForwardedRequest and IncludedRequest inject the standard attributes (jakarta.servlet.forward., jakarta.servlet.include.) and reuse the Chappe routing.
Virtual hosts and WAR deployment
Foy can be started:
-
programmatically through
FoyChappeBoot.builder()— one container per contextPath; -
by stacking multiple
Mountedinstances on the sameChappeMountPointto serve several applications on the same connector; -
eventually through WAR packaging —
// TODO@user: document the WAR deploy command once stable.
CDI integration through Vauban
When foy-cdi-vauban is on the classpath, Foy exposes the following beans:
| Bean | Scope |
|---|---|
|
|
|
|
|
|
Any Servlet or Filter can therefore use @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()));
}
}
Resolution is fully build-time through Vauban — no runtime dynamic proxy, no hot-path reflection.
Application security
-
<security-constraint>fromweb.xmlis enforced bySecurityConstraintEnforcer. -
BasicAuthenticatorcovers HTTP Basic. -
AnonymousSecurityProvideris the open fallback (development only). -
Form-based and Digest:
// TODO@user: document once stable.
For a custom provider, implement io.vidocq.foy.spi.security.SecurityProvider and register it as a CDI bean.
Cohabitation with Cassini
Foy and Cassini share the same Chappe runtime. On the same ChappeMountPoint you can mount /app (Foy) and /api (Cassini) — each owns its contextPath, the two containers do not interact.