This page describes how an HTTP request flows through Cassini, the codegen-side implementation choices, and the integrations with Champollion (JSON) and Vauban (CDI). Source of truth: cassini-core/src/main/java/io/vidocq/cassini/internal.

Overview

Diagram

HTTP request sequence

Diagram

Transport decoupling — golden rule

cassini-core contains no io.vidocq.chappe import. The transport is consumed via cassini-api (CassiniHttpExchange, CassiniHttpAdapter). This is a foundational constraint: a new transport (Netty, Helidon, Vert.x…​) can be added without touching the engine.

              ┌─ cassini-api (public SPI)
              │     ├── CassiniHttpExchange, CassiniHttpAdapter
              │     ├── CassiniStack, ResourceFactory
              │     └── (no dependency outside jakarta.ws.rs-api)
              │
              ├─ cassini-core (impl, sealed)
              │     ├── Invoker, UriRouter, ResourceScanner...
              │     ├── CassiniRuntimeDelegate (base for transports)
              │     └── exports internal.runtime to {tck, jdkhttp, chappe}
              │
   ServiceLoader─┬─ cassini-chappe ──→ requires cassini-api + cassini-core
              │     provides RuntimeDelegate with ChappeRuntimeDelegate
              │
              ├─ cassini-jdk-http ──→ requires cassini-api + cassini-core
              │     provides RuntimeDelegate with JdkHttpRuntimeDelegate
              │
              ├─ cassini-client ──→ requires cassini-api + cassini-core + java.net.http
              │     provides ClientBuilder + RuntimeDelegate, uses Feature (VT-based)
              │
              ├─ cassini-cdi-vauban ──→ requires cassini-api + io.vidocq.vauban.core
              │     provides BeanProvider.Factory + BuildCompatibleExtension
              │
              └─ cassini-processor ──→ APT, provides javax.annotation.processing.Processor
                    cassini-maven-plugin (no module-info) — build-time codegen for dep JARs

Static resource codegen

Rather than reflecting on every dispatch, Cassini generates two artefacts per resource class, both resolved by name (Class.forName) at startup — never a classpath annotation scan:

  • <ResourceClass>$$CassiniAdapter — a ResourceAdapter (SPI cassini-api, package io.vidocq.cassini.spi.gen) that performs direct method invocation (no Method.invoke), field injection of @Context / @*Param, and inline @*Param coercion: String/CharSequence, primitives + wrappers, enums (fromString or Enum.valueOf), public valueOf/fromString/(String)-ctor types, and List/Set/SortedSet/Collection of those.

  • <ResourceClass>$$CassiniRoutes — a RouteProvider exposing the route table as string/class literals (RouteDescriptor). RouteRegistry converts each to a ResourceMethod via a targeted getDeclaredMethod(…​) plus UriTemplate.compile(…​) — no annotation scan. Classes carrying sub-resource locators set hasLocators() and fall back to ResourceScanner for the whole class.

Three generation tiers

Adapters come from one of three generators, most-preferred first (AdapterRegistry.lookup):

  1. APT — compile time (cassini-processor): emits CassiniAdapter+` / `+CassiniRoutes source via Filer for every @Path / @Provider of the current build. Plain Java, no bytecode manipulation, AOT-safe.

  2. Maven plugin — build time (cassini-maven-plugin:generate, bound to process-classes): pre-generates adapters for external archives (dependency JARs) by calling RuntimeAdapterGenerator.toBytecode(cls) and writing the .class files to target/classes. AOT-safe.

  3. Runtime generator (RuntimeAdapterGenerator.generate, Class-File API / JEP 484): JVM-only fallback when neither of the above produced an adapter.

AdapterRegistry.lookup tries Class.forName(<class>$$CassiniAdapter) first (APT/plugin output), then the runtime generator, then caches a SENTINEL so the request falls through to the reflective FieldInjector filet without retrying.

Documented residual reflection

Codegen does not erase reflection entirely; the remainder is one-time or structurally irreducible, never on the hot path:

  • ResourceScanner runs once at startup, only for classes without a generated $$CassiniRoutes (i.e. those with locators).

  • Resource / bean instantiation (getDeclaredConstructor().newInstance()) — one-time per scope; M6a prefers a generated newInstance() factory when an accessible no-arg constructor exists.

  • The FieldInjector filet applies only when codegen cannot: a closed application module (no opensprivateLookupIn fails), or a field whose type is a package-private class from another package than the resource (a Java language rule, not a bug).

Benefits: cold start under ~50 ms, AOT-compatible (GraalVM native-image, Leyden CDS), and predictable.

Champollion integration (JSON)

CassiniJsonbReaderWriter (in cassini-core/internal) implements MessageBodyReader<Object> + MessageBodyWriter<Object> for application/json and delegates to Champollion (Jsonb.create().toJson(…​) / fromJson(…​)).

The binding itself is static: champollion-codegen-apt emits a <Type>_Binding at compile time per serializable type. No runtime reflection, no on-the-fly setAccessible.

Vauban integration (CDI)

// SPI in cassini-api:
public interface BeanProvider {
    interface Factory {
        BeanProvider create();
    }
    Set<Class<?>> getResourceClasses();
    <T> T resolve(Class<T> resourceClass);
}

cassini-cdi-vauban provides VaubanBeanProviderFactory via module-info.java:

provides io.vidocq.cassini.spi.bean.BeanProvider.Factory
    with io.vidocq.cassini.cdi.vauban.VaubanBeanProviderFactory;

Cassini discovers Vauban via ServiceLoader, walks the BeanManager at stack construction, and stores @Path / @Provider classes. For every dispatch, resolve() calls container.select() — the CDI scope (@RequestScoped, @ApplicationScoped, …​) is honoured.

The CassiniScopeExtension (Vauban Build Compatible Extension) adds @RequestScoped to @Path classes lacking an explicit scope — automatic alignment with the semantics filters and interceptors expect.

JAX-RS Client (cassini-client)

cassini-client is a standalone implementation of the Jakarta REST 4.0 Client API with no external dependency: it builds on the JDK java.net.http.HttpClient running on virtual threads. It exposes no public package — the whole surface is consumed through jakarta.ws.rs.client.* and discovered via ServiceLoader:

  • provides jakarta.ws.rs.client.ClientBuilder with CassiniClientBuilderClientBuilder.newClient() returns a Cassini client whenever cassini-client is on the path.

  • provides jakarta.ws.rs.ext.RuntimeDelegate with CassiniClientRuntimeDelegate — makes the module self-contained for UriBuilder.fromUri(…​); all transports point to the same CassiniRuntimeDelegate, so there is no runtime divergence when several are present.

  • uses jakarta.ws.rs.core.Feature — third-party Feature`s (MicroProfile Telemetry instrumentation, auth, logging…) registered via `ServiceLoader are auto-applied by CassiniClientBuilder.build() on a FeatureContext adapter.

It reuses MessageBodyRegistry, CassiniResponse, CassiniUriBuilder and CassiniRuntimeDelegate from cassini-core — request/response (de)serialization is shared with the server side. ClientRequestFilter / ClientResponseFilter are @Priority-ordered; AsyncContextPropagator carries the async invocation onto a virtual thread.

Virtual threads and async

  • All blocking I/O (body read, backend wait, SSE) runs on a virtual thread created by Executors.newVirtualThreadPerTaskExecutor().

  • @Suspended AsyncResponse immediately releases the carrier thread; resolution is posted by user code on a VT.

  • A CompletionStage returned by a resource method is propagated down to the transport via CassiniAsyncContext.

Full chunked streaming (SSE backpressure, native StreamingOutput) is scheduled for M2h/M2i — see TCK status.

Extension points

CassiniHttpAdapter

Implement to wire a new transport.

BeanProvider.Factory

Implement to wire a different CDI/DI container.

ResourceFactory

Override resource instantiation without a full CDI container.

MessageBodyReader/Writer

Extend serialization to new media types.

ParamConverterProvider

Convert custom types in @*Param.