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:
-
vauban-indexer— walks the module path at compile time and builds the bean index, persisted asMETA-INF/vauban-beans.list. The static equivalent of Weld’s runtimeBeanArchive. Zero dependency. -
vauban-processor— ajavax.annotation.processing.Processorthat consumes the index and emits bytecode through the Class-File API (JEP 484). No ASM, no Byte Buddy, no Gizmo.
The processor generates:
| Generated artefact | Role |
|---|---|
|
One per bean. Implements |
|
Subclass generated for normal scopes. Intercepts every public method and delegates to the active context. |
|
Subclass generated for beans with interceptor bindings — routes the intercepted methods through the interceptor chain. |
|
One per module. Implements |
|
Drift between |
APT pipeline — sequence diagram
The whole index, the whole injection grid, every proxy is written before javac finishes its job.
Client-proxy weaving tiers NEW
A normal-scoped bean without a non-private no-arg constructor needs a side-effect-free
client-proxy entry point: the synthetic protected <init>(ProxyLink) constructor
(Usage — proxyable beans). Annotation processing cannot add members to an
existing class (JSR 269 only creates new files), so the marker is woven into the compiled
bytes by the Class-File API transformations of the vauban-weaver module — one canonical
implementation, applied at three possible moments:
| Tier | When | Covers |
|---|---|---|
javac plugin |
|
Maven, Gradle, plain |
|
|
Belt-and-braces for Maven builds; ecosystem jars whose classes do not go through javac (enrichment, sjar packaging) |
Load-time agent |
Container boot — |
IDE builds — IntelliJ’s build system (JPS) flushes hand-written classes to disk after javac finishes, so both build-time tiers see stale files |
Details worth knowing about the load-time tier:
-
Why an agent and not reflection — Vidocq application modules keep bean packages fully encapsulated (zero
opens, zeroexports). Any reflective instantiation trick (ReflectionFactory-style relaxed construction) is blocked by Java Modules; aClassFileTransformerworks below access control and produces bytecode identical to a woven build. -
Self-attach through a child process — the JVM refuses
VirtualMachine.attach()on itself;LoadTimeWeavingspawnsjava -cp vauban-weaver.jar io.vidocq.vauban.weaver.AttachBack <pid> <jar> <plan>, which attaches back to the parent and loads the agent. The IDE debugger session is unaffected. The JDK prints its standard dynamic-agent warning. -
Plan-driven, no heuristics — the agent only ever touches the classes named in the plan computed from the container’s own index: unwoven beans get the marker constructor, their
_ClientProxycompanions are retargeted to it. -
Ordering constraint — adding a constructor is not a valid retransformation, so a class loaded before the attach can no longer be woven. The hook therefore runs inside
VaubanContainerBuilder.build()right after the index is final and before bean discovery, and the detection itself never loads classes. -
Opt-out —
-Dvauban.weaving.loadtime=disabled; unwoven beans then fail deployment validation with the regular vauban#24 diagnostic. Explicit-javaagent:vauban-weaver.jar=<plan>is also supported (Premain-Class).
Class loading
Vauban owns application class loading, as a single engine with two plugin natures
chained before defineClass:
┌─ sources ─────────────┐ ┌─ transformers ───────────┐
archive ───▶ │ dir | jar | sjar | … │ ─▶│ cdi-proxifier | (future) │ ─▶ defineClass
└───────────────────────┘ └──────────────────────────┘
-
vauban-classloader-spi— the contracts:ByteSourcePlugin(where the bytes of an archive come from — the encrypted-jar support invauban-sjaris one plugin) andClassTransformerPlugin(what happens to the bytes before definition — experimental SPI). Both are ServiceLoader-discovered and priority-ordered. -
vauban-classloader— the engine:VaubanClassLoaderresolves each archive through the first source plugin that handles it (plain jars and exploded directories built in) and pipes every class through the transformer chain. The shippedcdi-proxifiertransformer applies the vauban#24 weaving (ProxyLinkWeaver, same bytes as a build-time weave — idempotent) at definition; an unwoven bean inside an encrypted archive can be fixed nowhere else, the decrypt → weave composition falls out of the chaining for free. The container’s scanner defines sjar classes through this engine. Diagnostics: transformations are logged,-Dvauban.classloader.dump=<dir>writes transformed bytes,-Dvauban.classloader.transformers.disabled=<names>disables transformers by name, and a transformer failure fails the class load — nothing silent. -
VaubanLayerFactory— the module-path story: application archives stay off the JVM module path and resolve into a childModuleLayerwhose single defining loader is the engine. Exports and opens keep being enforced inside the child layer;ServiceLoaderprovides/uses work across it. The Vidocq launcher activates this with-Dvidocq.app.path=<archives>(VidocqAppLayer, installed before anything can touch an application class), and every Vidocq launch vehicle uses it by default: packaged distributions ship the application jar inapp/(scripts boot the runtime,vidocq.package.layer=falseopts out),vidocq devhandstarget/classesover the same way (vidocq.dev.layer=falseopts out), and in-process embedders (the CLI) get the layer fromVidocqBootstrap.configure(). An applicationmainis run through the layer via-Dvidocq.app.main— its package is exported to the runtime through the layerModuleLayer.Controller, the same technique the JDK launcher uses. The load-time weaving agent then stands down — the loader does the weaving, withoutjava.lang.instrumentand without the JDK’s dynamic-agent warning. Implementation note for custom layer loaders: the JVM loads layer-module classes throughfindClass(String moduleName, String name), whose inherited default returnsnull— both name-based and module-based lookups are overridden. Known limitation: custom JUL log handlers declared inlogging.propertiesmust live on the module path —LogManagerinstantiates handlers through the system class loader and cannot see the layer. -
The container itself never reflects into application packages: generated
_VaubanComponentsproviders (ServiceLoader) instantiate beans and proxies in-module, which is what keepsopens-free deployments possible.
The layer is also what makes the dev-mode in-JVM hot reload possible (M3): after a
successful recompile, vidocq dev signals the running JVM (reload file touch,
vidocq.dev.hotReload=false restores the respawn cycle) — the runtime shuts the
deployment down, discards the application layer and its loader, resolves a fresh layer
over the recompiled classes and boots again. Same process, warm JIT, debugger and dev
services untouched. Hosts embedding Cassini across such reloads must discard its static
discovery caches (io.vidocq.cassini.runtime.CassiniMaintenance.resetDiscoveryCaches(),
called by the Vidocq Cassini extension’s onStop): they are keyed by application
Class objects, which a new layer replaces wholesale.
Design study and milestones: tasks/vauban-classloader-universal.md (M1 engine + sjar
fold-in, M2 application layer, M2b launch vehicles and M3 in-JVM hot reload are
implemented).
Runtime bootstrap sequence
The ApplicationContext is built in memory, with no reflection, no package opening.
Threading model
Vauban uses virtual threads (JEP 444) in two places:
-
Async observers — every
@ObservesAsyncis dispatched on anExecutors.newVirtualThreadPerTaskExecutor()internal to theEventDispatcher. No platform-thread pool, no bounded queue. -
Request context — when Vauban is embedded in Cassini or Foy, the
RequestContextis carried by aScopedValue(JEP 487) rather than aThreadLocal. That lets structured virtual threads propagate the context without leaks.
|
The |
AOT compatibility
No Class.forName, no Method.invoke, no Class.getDeclaredFields() runs hot. The runtime relies on:
-
direct invocations on the generated
_Factoryclasses; -
MethodHandlesfor@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 Java modules
| Module | Main exports |
|---|---|
|
|
|
|
|
|
|
(internal, requires |
|
|
|
|
|
|
|
AES-256-GCM encrypted implementation of the SPI (optional) |
See the reference for the exhaustive list.
Sources
-
vauban-core — container runtime
-
vauban-processor — APT and Class-File API generators
-
vauban-indexer — build-time scanner
-
BUG.md — tracked reproducible bugs (VAU-PRX-002, VAU-INJ-001)