First servlet, Foy container, Chappe transport: under fifty lines of Java, no mandatory web.xml, no runtime classpath scan.
Prerequisites
-
Java 25 (Temurin) and Maven 3.9.16 —
.sdkmanrcis checked in at the repo root, runsdk envfirst. -
Familiarity with the Jakarta Servlet 6.1 API.
-
For the CDI flavour: a Vauban
BeanManager(see Vauban — getting started).
Maven coordinates
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>io.vidocq.foy</groupId>
<artifactId>foy-core</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>io.vidocq.foy</groupId>
<artifactId>foy-chappe</artifactId>
<version>0.3.0</version>
<scope>runtime</scope>
</dependency>
<!-- Optional CDI through Vauban -->
<dependency>
<groupId>io.vidocq.foy</groupId>
<artifactId>foy-cdi-vauban</artifactId>
<version>0.3.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Hello world: an annotated Servlet
package io.example;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/hello/*")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
var name = req.getPathInfo() == null ? "world" : req.getPathInfo().substring(1);
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().write("Hello, " + name + "!");
}
}
The @WebServlet annotation is resolved at compile time by an APT. Zero classpath scan at startup.
Boot the container
The public Chappe-side API is FoyChappeBoot. It takes a BeanManager (CDI) and exposes a Chappe Handler (Mounted.handler()). Mounting is plain Chappe: hang the handler on a Router.Builder.mount(prefix, handler) — or pass it directly to Server.builder().handler(…) when the contextPath is /.
import io.vidocq.chappe.api.Router;
import io.vidocq.chappe.api.Server;
import io.vidocq.foy.chappe.FoyChappeBoot;
import jakarta.enterprise.inject.spi.CDI;
void main() throws Exception {
var mounted = FoyChappeBoot.builder()
.beanManager(CDI.current().getBeanManager())
.contextPath("/app")
.sessionTimeoutSeconds(1800)
.build()
.orElseThrow(() -> new IllegalStateException("No @WebServlet discovered"));
var router = Router.builder()
.mount(mounted.mountPrefix(), mounted.handler())
.build();
try (var server = Server.builder().port(8080).handler(router).build()) {
server.start(); // non-blocking
mounted.fireContextInitialized();
// ... await your application's shutdown signal ...
mounted.fireContextDestroyed();
}
}
build() returns an empty Optional<Mounted> when the application declares no Servlet/Filter/Listener bean — the caller decides whether that’s an error or a degraded mode.
Annotations or web.xml?
Foy supports annotations and a web.xml subset:
-
@WebServlet,@WebFilter,@WebListener,@MultipartConfig: discovered at compile time by APT, exposed through the VaubanBeanManager. -
WEB-INF/web.xml: parsed byWebXmlParser(supported element subset — see Reference) and contributed byWebXmlContributorafter annotation discovery. A descriptor definition never overrides a name already discovered by annotation.
|
|
Wiring with Chappe
The bridge is ChappeServletBridge (in foy-core, package io.vidocq.foy.internal.bridge; FoyChappeBoot builds it), a Chappe Handler that:
-
exposes the Chappe
RequestasHttpServletRequestImpl; -
exposes the Chappe
ResponseasHttpServletResponseImpl; -
invokes the
Filter→Servletchain; -
returns the response through
ServletOutputStreamImplbacked by Chappe streaming — no intermediate copy.
sdk env
./mvnw -ntp install -DskipTests
./mvnw test
|
Official Jakarta Servlet 6.1 TCK: |
Next steps
-
Usage recipes — filters, listeners, async, multipart, sessions, dispatchers, virtual hosts, CDI integration.
-
Concepts — Servlet vocabulary, what changes in 6.1.
-
Internals — request sequence, threading, container lifecycle.