Vauban rests on a simple principle: anything that can be computed at compile time is. No hot reflection, no dynamic proxies, no external bytecode library. This page documents the implementation choices that uphold that discipline.

Code generation via Class-File API and APT

CDI 4.1 describes a richly reflective object model: Bean<T>, BeanManager, InjectionPoint, AnnotatedType. Vauban refuses to evaluate that model at runtime. It compiles it.

Two tools run at process-classes:

  1. vauban-indexer — walks the module path at compile time and writes an index file (META-INF/vauban/index.bin). The static equivalent of Weld’s runtime BeanArchive. Zero dependency.

  2. vauban-processor — a javax.annotation.processing.Processor that consumes the index and emits bytecode through the Class-File API (JEP 484). No ASM, no Byte Buddy, no Gizmo.

For each bean the processor generates:

Generated artefact Role

MyBean$$_Factory

Implements BeanFactory<MyBean>. No-arg constructor. Calls the target constructor with dependencies already resolved, then runs @PostConstruct.

MyBean$$_ClientProxy

Subclass generated for normal scopes. Intercepts every public method and delegates to the active context via Context.get(bean, ctx).

MyProducer$$_ProducerFactory

For each @Produces method, encapsulates creation and destruction (@Disposes).

MyObserver$$_ObserverMethod

For each @Observes method, exposes notify(event) reflection-free.

Drift between ClientProxyGenerator (compile time) and RuntimeClientProxyGenerator (fallback) is a tracked risk in BUG.md (entry VAU-PRX-002). Any change to one generator must be replicated in the other.

APT pipeline — sequence diagram

Diagram

The whole index, the whole injection grid, every proxy is written before javac finishes its job.

Runtime bootstrap sequence

Diagram

The ApplicationContext is built in memory, with no reflection, no package opening.

Threading model

Vauban uses virtual threads (JEP 444) in two places:

  1. Async observers — every @ObservesAsync is dispatched on an Executors.newVirtualThreadPerTaskExecutor() internal to the EventDispatcher. No platform-thread pool, no bounded queue.

  2. Request context — when Vauban is embedded in Cassini or Foy, the RequestContext is carried by a ScopedValue (JEP 487) rather than a ThreadLocal. That lets structured virtual threads propagate the context without leaks.

The ThreadLocalScopedValue move is documented in the Vauban repo’s CLAUDE.md as a guiding principle.

AOT compatibility

No Class.forName, no Method.invoke, no Class.getDeclaredFields() runs hot. The runtime relies on:

  • direct invocations on the generated _Factory classes;

  • MethodHandles for @PostConstruct, @PreDestroy, observer and interceptor invocations;

  • no dynamic class loading beyond standard ServiceLoader.

Consequence: Vauban compiles to a GraalVM native image without a reflect-config.json. It works with Project Leyden CDS and with a minimal JLink image.

Exported JPMS modules

Module Main exports

io.vidocq.vauban.api

io.vidocq.vauban.api (Vauban façade, 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

(internal, requires java.compiler)

io.vidocq.vauban.classloader.spi

io.vidocq.vauban.classloader.spiByteSourcePlugin SPI

io.vidocq.vauban.sjar

AES-256-GCM encrypted implementation of the SPI (optional)

See the reference for the exhaustive list.

Sources