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.
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— unResourceAdapter(SPIcassini-api, packageio.vidocq.cassini.spi.gen) qui fait l'invocation directe de méthode (pas deMethod.invoke), l'injection de champs@Context/@*Param, et la coercition@*Paraminline : String/CharSequence, primitifs + wrappers, enums (fromStringouEnum.valueOf), types àvalueOf/fromString/ctor(String)publics, etList/Set/SortedSet/Collectionde ceux-ci. -
<ResourceClass>$$CassiniRoutes— unRouteProviderexposant la table de routes sous forme de littéraux string/class (RouteDescriptor).RouteRegistryconvertit chacun enResourceMethodvia ungetDeclaredMethod(…)ciblé plusUriTemplate.compile(…)— sans scan d’annotations. Les classes portant des sub-resource locators positionnenthasLocators()et retombent surResourceScannerpour 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) :
-
APT — à la compilation (
cassini-processor) : émet le sourceCassiniAdapter+` / `+CassiniRoutesviaFilerpour chaque@Path/@Providerdu build courant. Java pur, aucune manipulation de bytecode, AOT-safe. -
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 appelantRuntimeAdapterGenerator.toBytecode(cls)puis en écrivant les.classdanstarget/classes. AOT-safe. -
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 :
-
ResourceScannerne tourne qu’une fois au démarrage, uniquement pour les classes sans$$CassiniRoutesgénéré (celles avec locators). -
Instanciation ressource / bean (
getDeclaredConstructor().newInstance()) — one-time par scope ; M6a préfère une fabriquenewInstance()générée quand un constructeur sans argument accessible existe. -
Le filet
FieldInjectorne 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 CassiniClientBuilder—ClientBuilder.newClient()retourne un client Cassini dès quecassini-clientest sur le path. -
provides jakarta.ws.rs.ext.RuntimeDelegate with CassiniClientRuntimeDelegate— rend le module autonome pourUriBuilder.fromUri(…); tous les transports pointent vers le mêmeCassiniRuntimeDelegate, donc pas de divergence runtime quand plusieurs sont présents. -
uses jakarta.ws.rs.core.Feature— lesFeature`s tierces (instrumentation MicroProfile Telemetry, auth, logging…) enregistrées via `ServiceLoadersont auto-appliquées parCassiniClientBuilder.build()sur un adapterFeatureContext.
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 AsyncResponselibère immédiatement le thread porteur ; la résolution est posée par le code utilisateur sur un VT. -
CompletionStageretourné par une resource method est plombé jusqu’au transport viaCassiniAsyncContext.
Le streaming chunked complet (SSE backpressure, StreamingOutput natif) est planifié pour M2h/M2i — voir état TCK.
Points d’extension
|
Implémenter pour brancher un nouveau transport. |
|
Implémenter pour brancher un autre container CDI/DI. |
|
Surcharger l’instanciation des ressources sans CDI complet. |
|
Étendre la sérialisation à de nouveaux media types. |
|
Convertir des types custom dans les |