Cette page décrit le cheminement d’une requête HTTP dans Cassini, les choix d’implémentation côté codegen, et les intégrations avec Champollion (JSON) et Vauban (CDI). Source de vérité : cassini-core/src/main/java/io/vidocq/cassini/internal.

Vue d’ensemble

Diagram

Séquence d’une requête HTTP

Diagram

Découplage transport — règle d’or

cassini-core ne contient aucun import io.vidocq.chappe. Le transport est consommé via cassini-api (CassiniHttpExchange, CassiniHttpAdapter). C’est une contrainte fondamentale : un nouveau transport (Netty, Helidon, Vert.x…​) peut être ajouté sans toucher au moteur.

              ┌─ cassini-api (SPI publique)
              │     ├── CassiniHttpExchange, CassiniHttpAdapter
              │     ├── CassiniStack, ResourceFactory
              │     └── (zéro dépendance hors jakarta.ws.rs-api)
              │
              ├─ cassini-core (impl, fermée)
              │     ├── Invoker, UriRouter, ResourceScanner...
              │     ├── CassiniRuntimeDelegate (base pour 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 (base VT)
              │
              ├─ 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 (sans module-info) — codegen build-time pour JARs déps

Codegen statique des resources

Plutôt que de réfléchir à chaque dispatch, Cassini génère deux artefacts par classe ressource, tous deux résolus par nom (Class.forName) au démarrage — jamais de scan d’annotations du classpath :

  • <ResourceClass>$$CassiniAdapter — un ResourceAdapter (SPI cassini-api, package io.vidocq.cassini.spi.gen) qui fait l'invocation directe de méthode (pas de Method.invoke), l'injection de champs @Context / @*Param, et la coercition @*Param inline : String/CharSequence, primitifs + wrappers, enums (fromString ou Enum.valueOf), types à valueOf/fromString/ctor (String) publics, et List/Set/SortedSet/Collection de ceux-ci.

  • <ResourceClass>$$CassiniRoutes — un RouteProvider exposant la table de routes sous forme de littéraux string/class (RouteDescriptor). RouteRegistry convertit chacun en ResourceMethod via un getDeclaredMethod(…​) ciblé plus UriTemplate.compile(…​) — sans scan d’annotations. Les classes portant des sub-resource locators positionnent hasLocators() et retombent sur ResourceScanner pour toute la classe.

Trois niveaux de génération

Les adapters viennent de l’un de trois générateurs, du plus préféré au fallback (AdapterRegistry.lookup) :

  1. APT — à la compilation (cassini-processor) : émet le source CassiniAdapter+` / `+CassiniRoutes via Filer pour chaque @Path / @Provider du build courant. Java pur, aucune manipulation de bytecode, AOT-safe.

  2. Plugin Maven — au build (cassini-maven-plugin:generate, lié à process-classes) : pré-génère les adapters pour les archives externes (JARs de dépendances) en appelant RuntimeAdapterGenerator.toBytecode(cls) puis en écrivant les .class dans target/classes. AOT-safe.

  3. Générateur runtime (RuntimeAdapterGenerator.generate, Class-File API / JEP 484) : fallback JVM-only quand aucun des deux précédents n’a produit d’adapter.

AdapterRegistry.lookup essaie d’abord Class.forName(<class>$$CassiniAdapter) (sortie APT/plugin), puis le générateur runtime, puis met en cache un SENTINEL afin que la requête retombe sur le filet réflexif FieldInjector sans réessayer.

Réflexion résiduelle documentée

Le codegen n’efface pas toute réflexion ; le reste est one-time ou structurellement irréductible, jamais sur le hot-path :

  • ResourceScanner ne tourne qu’une fois au démarrage, uniquement pour les classes sans $$CassiniRoutes généré (celles avec locators).

  • Instanciation ressource / bean (getDeclaredConstructor().newInstance()) — one-time par scope ; M6a préfère une fabrique newInstance() générée quand un constructeur sans argument accessible existe.

  • Le filet FieldInjector ne s’applique que quand le codegen le ne peut pas : module applicatif fermé (pas d'`opens` → privateLookupIn échoue), ou champ dont le type est une classe package-private d’un autre package que la ressource (règle du langage Java, pas un bug).

Bénéfices : démarrage froid sous ~50 ms, compatible AOT (GraalVM native-image, Leyden CDS), prévisible.

Intégration Champollion (JSON)

CassiniJsonbReaderWriter (dans cassini-core/internal) implémente MessageBodyReader<Object> + MessageBodyWriter<Object> pour application/json et délègue à Champollion (Jsonb.create().toJson(…​) / fromJson(…​)).

Le binding lui-même est statique : champollion-codegen-apt génère un <Type>_Binding à la compilation pour chaque type sérialisable. Pas de réflexion runtime, pas de setAccessible à chaud.

Intégration Vauban (CDI)

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

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

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

Cassini découvre Vauban via ServiceLoader, itère le BeanManager à la construction du stack, et stocke les classes @Path / @Provider. Pour chaque dispatch, resolve() appelle container.select() — le scope CDI (@RequestScoped, @ApplicationScoped, …​) est respecté.

Le CassiniScopeExtension (Build Compatible Extension Vauban) ajoute @RequestScoped aux classes @Path sans scope explicite — alignement automatique avec la sémantique attendue par les filtres et intercepteurs JAX-RS.

Client JAX-RS (cassini-client)

cassini-client est une implémentation autonome de l'API Client Jakarta REST 4.0 sans dépendance externe : elle repose sur le java.net.http.HttpClient du JDK exécuté sur virtual threads. Elle n’expose aucun package public — toute la surface passe par jakarta.ws.rs.client.* et la découverte ServiceLoader :

  • provides jakarta.ws.rs.client.ClientBuilder with CassiniClientBuilderClientBuilder.newClient() retourne un client Cassini dès que cassini-client est sur le path.

  • provides jakarta.ws.rs.ext.RuntimeDelegate with CassiniClientRuntimeDelegate — rend le module autonome pour UriBuilder.fromUri(…​) ; tous les transports pointent vers le même CassiniRuntimeDelegate, donc pas de divergence runtime quand plusieurs sont présents.

  • uses jakarta.ws.rs.core.Feature — les Feature`s tierces (instrumentation MicroProfile Telemetry, auth, logging…) enregistrées via `ServiceLoader sont auto-appliquées par CassiniClientBuilder.build() sur un adapter FeatureContext.

Il réutilise MessageBodyRegistry, CassiniResponse, CassiniUriBuilder et CassiniRuntimeDelegate de cassini-core — la (dé)sérialisation request/response est mutualisée avec le côté serveur. Les ClientRequestFilter / ClientResponseFilter sont ordonnés par @Priority ; AsyncContextPropagator porte l’invocation async sur un virtual thread.

Virtual threads et async

  • Toute I/O bloquante (lecture body, attente backend, SSE) tourne sur un virtual thread créé par Executors.newVirtualThreadPerTaskExecutor().

  • @Suspended AsyncResponse libère immédiatement le thread porteur ; la résolution est posée par le code utilisateur sur un VT.

  • CompletionStage retourné par une resource method est plombé jusqu’au transport via CassiniAsyncContext.

Le streaming chunked complet (SSE backpressure, StreamingOutput natif) est planifié pour M2h/M2i — voir état TCK.

Points d’extension

CassiniHttpAdapter

Implémenter pour brancher un nouveau transport.

BeanProvider.Factory

Implémenter pour brancher un autre container CDI/DI.

ResourceFactory

Surcharger l’instanciation des ressources sans CDI complet.

MessageBodyReader/Writer

Étendre la sérialisation à de nouveaux media types.

ParamConverterProvider

Convertir des types custom dans les @*Param.