Vauban repose sur un principe simple : tout ce qui peut être calculé à la compilation l’est. Aucune réflexion à chaud, aucun proxy dynamique, aucune librairie de bytecode externe. Cette page décrit les choix d’implémentation qui supportent cette discipline.

Codegen via Class-File API et APT

CDI 4.1 décrit un modèle d’objets richement réflexif : Bean<T>, BeanManager, InjectionPoint, AnnotatedType. Vauban refuse d’évaluer ce modèle au runtime. Il le compile.

Deux outils interviennent à process-classes :

  1. vauban-indexer — parcourt le module-path à la compilation et produit un fichier d’index (META-INF/vauban/index.bin). L’équivalent statique du BeanArchive runtime de Weld. Zéro dépendance.

  2. vauban-processorjavax.annotation.processing.Processor qui consomme l’index et émet du bytecode via la Class-File API (JEP 484). Pas d’ASM, pas de Byte Buddy, pas de Gizmo.

Pour chaque bean, le processeur génère :

Artefact généré Rôle

MyBean$$_Factory

Implémente BeanFactory<MyBean>. Constructeur sans argument. Appelle le constructeur cible avec les dépendances déjà résolues, puis exécute @PostConstruct.

MyBean$$_ClientProxy

Sous-classe générée pour les scopes normaux. Intercepte chaque méthode publique et délègue au contexte actif via Context.get(bean, ctx).

MyProducer$$_ProducerFactory

Pour chaque méthode @Produces, encapsule la création et la destruction (@Disposes).

MyObserver$$_ObserverMethod

Pour chaque méthode @Observes, fournit notify(event) sans réflexion.

Le drift entre ClientProxyGenerator (compile-time) et RuntimeClientProxyGenerator (fallback) est un risque tracé dans BUG.md (entrée VAU-PRX-002). Toute évolution d’un générateur doit être répliquée dans l’autre.

Pipeline APT — diagramme de séquence

Diagram

Tout l’index, toute la grille d’injection, tous les proxies sont écrits avant que javac ne finisse son travail.

Séquence de bootstrap runtime

Diagram

L'`ApplicationContext` est créé en mémoire, sans réflexion, sans ouverture de packages.

Modèle de threading

Vauban utilise les virtual threads (JEP 444) pour deux usages :

  1. Observateurs asynchrones — chaque @ObservesAsync est dispatché sur un Executors.newVirtualThreadPerTaskExecutor() interne au EventDispatcher. Pas de pool de threads plateforme, pas de file d’attente bornée.

  2. Contexte de requête — quand Vauban est embarqué dans Cassini ou Foy, le RequestContext est porté par un ScopedValue (JEP 487) plutôt qu’un ThreadLocal. Cela permet aux virtual threads structurés de propager le contexte sans fuite.

Le passage ThreadLocalScopedValue est documenté dans le CLAUDE.md du repo Vauban comme principe directeur.

Compatibilité AOT

Aucun Class.forName, aucun Method.invoke, aucun Class.getDeclaredFields() n’est appelé à chaud. Le runtime utilise :

  • des invocations directes sur les _Factory générés ;

  • des MethodHandles pour l’invocation des @PostConstruct, @PreDestroy, observateurs et intercepteurs ;

  • aucun chargement dynamique de classe au-delà du ServiceLoader standard.

Conséquence : Vauban se compile en image native GraalVM sans fichier reflect-config.json. Il est compatible avec Project Leyden CDS et avec une image JLink minimale.

Modules JPMS exportés

Module Exports principaux

io.vidocq.vauban.api

io.vidocq.vauban.api (façade Vauban, BeanFactory)

io.vidocq.vauban.core

io.vidocq.vauban.core.container, io.vidocq.vauban.core.bean.model, io.vidocq.vauban.core.context, io.vidocq.vauban.core.event, io.vidocq.vauban.core.interceptor, io.vidocq.vauban.core.extensions

io.vidocq.vauban.indexer

io.vidocq.vauban.indexer, io.vidocq.vauban.indexer.model, io.vidocq.vauban.indexer.scanner

io.vidocq.vauban.processor

(interne, requiert java.compiler)

io.vidocq.vauban.classloader.spi

io.vidocq.vauban.classloader.spi — SPI ByteSourcePlugin

io.vidocq.vauban.sjar

Implémentation chiffrée AES-256-GCM du SPI (optionnelle)

Voir la référence pour la liste exhaustive.

Sources