Three minutes to serve a @Path resource over HTTP. Cassini is deployed via the standard SeBootstrap of the REST 4.0 spec — any Application class is started by a RuntimeDelegate provided by the active transport on the classpath.
Prerequisites
-
Java 25 + Maven 3.9.16 —
.sdkmanrcat the repo root (sdk envto align). -
The official Jakarta REST 4.0 TCK is required only for conformance — not for application development.
Maven coordinates
<dependencies>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>io.vidocq.cassini</groupId>
<artifactId>cassini-core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
<!-- reference transport, embeds Chappe -->
<dependency>
<groupId>io.vidocq.cassini</groupId>
<artifactId>cassini-chappe</artifactId>
<version>0.1.0-SNAPSHOT</version>
<scope>runtime</scope>
</dependency>
</dependencies>
For a zero-external-dependency deployment, swap cassini-chappe for cassini-jdk-http: it relies solely on the JDK’s com.sun.net.httpserver.
|
First resource
@Path("/hello")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello(@QueryParam("name") @DefaultValue("world") String name) {
return "Hello, " + name + "!";
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Item getItem(@PathParam("id") long id) {
return new Item(id, "Article #" + id);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createItem(Item item) {
return Response.status(Response.Status.CREATED).entity(item).build();
}
public record Item(long id, String label) {}
}
SE-Bootstrap startup (recommended)
ChappeRuntimeDelegate (or JdkHttpRuntimeDelegate) is discovered via JPMS ServiceLoader — application code references no transport-specific symbol.
import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.Application;
public class Main {
public static void main(String[] args) throws Exception {
var config = SeBootstrap.Configuration.builder()
.host("0.0.0.0")
.port(8080)
.rootPath("/")
.build();
SeBootstrap.start(MyApplication.class, config)
.thenAccept(instance -> System.out.println(
"Cassini started at http://localhost:" + instance.configuration().port()))
.toCompletableFuture()
.join();
Thread.currentThread().join();
}
}
class MyApplication extends Application {
@Override public Set<Class<?>> getClasses() {
return Set.of(HelloResource.class);
}
}
Manual bootstrap via CassiniStack
For fine-grained control (embedding Cassini in an existing Chappe server, unit testing):
try (var stack = CassiniStack.builder()
.application(new MyApplication())
.port(8080)
.build()) {
stack.start();
Thread.currentThread().join();
}
With CDI (Vauban)
Add cassini-cdi-vauban to the classpath. The Vauban BeanProvider is discovered via ServiceLoader; container-managed @Path / @Provider classes are scanned automatically.
@Path("/items")
@ApplicationScoped // or @RequestScoped automatically via CassiniScopeExtension
public class ItemResource {
@Inject ItemService service;
@GET @Produces(MediaType.APPLICATION_JSON)
public List<Item> list() { return service.findAll(); }
}
var container = VaubanContainer.builder()
.addBeanClass(ItemService.class)
.addBeanClass(ItemResource.class)
.build();
SeBootstrap.start(new Application() {}, config); // Empty Application: Vauban fills it
|
The |
Build and run
cd cassini
sdk env
./mvnw -ntp install -DskipTests
./mvnw -pl cassini-examples/cassini-examples-chappe -am exec:exec
Next: advanced use cases (sub-resources, providers, async, SSE), concepts, TCK status.