This page lists every artefact published by Vauban, the Java Modules packages they export, the Maven-plugin goals, and the comparison between the CDI Lite and Full profiles.
Maven artefacts
| Artefact | Recommended scope | Role |
|---|---|---|
|
|
Version marker ( |
|
|
CDI 4.1 Lite container runtime |
|
|
APT (emits |
|
|
Build-time bean indexer (zero dependency) |
|
|
Maven plugin (goals: |
|
|
ClassLoader SPI ( |
|
|
Universal class-loader engine ( |
|
|
The session context for a web container to drive ( |
|
|
Client-proxy weaving transforms (Class-File API), also packaged as a load-time agent |
|
|
|
|
|
JUnit 5 extension ( |
|
|
Official integration test suite |
|
not published |
JMH benchmarks of qualifier resolution on injection, lookups, events and interceptors, run from |
Exported Java modules
| Module | Exported packages |
|---|---|
|
|
|
|
|
|
|
(internal — requires |
|
|
|
|
|
|
|
|
|
|
|
|
io.vidocq.vauban.core provides jakarta.enterprise.inject.spi.CDIProvider (VaubanCDIProvider), jakarta.enterprise.inject.se.SeContainerInitializer (VaubanSeContainerInitializer) and jakarta.enterprise.inject.build.compatible.spi.BuildServices (VaubanBuildServices) via provides. No unjustified opens.
Maven plugin goals
| Goal | Role |
|---|---|
|
Bound to |
|
Bound to |
|
Bound to |
|
Bound to |
|
Bound to |
vauban:modularize in detail
A jar with no module-info.class is an automatic module on the module path: it works, but it
reads every module, exports every package, and jlink refuses to link it. This goal writes a copy
of it into target/vauban-modularized/, under the original file name, with a synthesized
descriptor: requires derived from the types the jar uses, every package exported,
META-INF/services promoted to provides, and an open module by default. How the descriptor is
derived is described in Internals; the runnable example is
vauban-examples/example-legacy-app.
The goal has no binding of its own — declare an execution. Its default phase is prepare-package:
<plugin>
<groupId>io.vidocq.vauban</groupId>
<artifactId>vauban-maven-plugin</artifactId>
<executions>
<execution>
<id>modularize</id>
<goals><goal>modularize</goal></goals>
<configuration>
<mode>all-automatic</mode>
</configuration>
</execution>
</executions>
</plugin>
Nothing is installed, deployed or redistributed: the copies live in target/ and only this build
sees them. The Vidocq runtime plugin’s dev, jlink and package goals resolve every dependency
through that directory first, so a patched copy replaces the original jar on the module path, in
the staged image and in the distribution’s lib/.
uses directives, and only legal ones
The descriptor also carries uses directives, scanned off the bytecode of the whole dependency
closure. They are not optional: an automatic module may consume any service, an explicit one only
what it declares, so a jar promoted without them fails its own ServiceLoader.load with
module … does not declare uses`. Lookups written through a helper that takes the service type
as a `Class parameter are followed across jars, since it is the module reaching ServiceLoader —
not the one naming the service — that must declare the directive. A service type passed as a
variable rather than a class literal cannot be seen by any bytecode scan; declare that one by hand.
Only the directives the module system will accept are emitted. A uses is not a hint: the JVM
rejects the whole graph at resolution time with Module M uses S but does not read a module that
exports P to M, so a directive that cannot be satisfied trades one broken lookup for a runtime
that does not start at all. A scanned service is kept only when its type resolves to a package of
the closure or of the JDK, and the declaring module can read the module exporting it (following
requires, and the transitive closure of requires transitive). Everything else is dropped,
listed in report.txt and logged as a warning naming the jar, the service and the reason:
not in closure, <module> cannot read <exporter>, or not a class literal.
Why some jars stay automatic
A jar whose own code reaches ServiceLoader for a type defined in a jar that depends on it has no
legal explicit form at all. LangChain4j is the textbook case: langchain4j-core ships a generic
ServiceHelper.loadFactories(Class), and langchain4j calls it with types from its own packages.
ServiceLoader checks the calling class’s module, so the JVM demands the uses on
langchain4j.core — but the service type belongs to langchain4j, which already
requires langchain4j.core. Declaring it would need a requires back, and Java modules allow no
cycle. No descriptor satisfies both constraints.
Such a jar is left automatic rather than patched into a graph that cannot resolve. The build logs a
kept automatic: line naming the jar and the lookup that cannot be declared, and report.txt
records it. Automatic modules work on the module path, so a development loop or a plain
--module-path run is unaffected; only jlink refuses them, and an application depending on such
a library cannot be linked into an image until it changes upstream — each caller doing its own
ServiceLoader.load, or the library shipping a hand-written module-info.
Set vauban.modularize.forceExplicit to patch the jar anyway, the illegal directives still dropped,
when you know the failing lookup is one your application never reaches.
derived or all-automatic
<mode> |
Patches | Use it when |
|---|---|---|
|
only jars whose automatic name is derived from the file name — no |
you want to stabilise the modules nobody has named yet, and leave untouched every jar whose author already committed to a name |
|
every automatic jar, including those declaring |
you link an image with |
Module naming
By default a patched jar keeps the very name it already had as an automatic module — the
Automatic-Module-Name entry, or the name the JDK derives from the file name. That is deliberate:
it is the name javac saw when it compiled your module-info.java against the original jar, so
the requires you wrote keeps resolving after patching.
<moduleNames> overrides that name per artifactId:
<moduleNames>
<langchain4j-open-ai>dev.langchain4j.openai</langchain4j-open-ai>
</moduleNames>
An override is a run-time-only rename. Compilation still resolves against the original jar,
which announces its automatic name, so a module renamed out from under a requires <old.name>;
compiles and then fails resolution at run time. Only override the name of a jar your code does not
requires by name — one reached transitively or through services — or whose derived name is not a
legal module name at all.
Split packages fail the build
Two automatic jars sharing a package cannot both become named modules, and the module system would reject them side by side anyway. The goal checks the whole automatic closure up front and fails with the package and the jars holding it:
vauban:modularize — split package(s) between dependency jars, the module system cannot host them together:
com.acme.util in acme-core-1.2.jar, acme-legacy-1.2.jar
<excludes> does not silence this: leaving one side unpatched does not make the packages any less
split. Remove one side from the dependency graph with a Maven <exclusion>.
report.txt
Every run writes target/vauban-modularized/report.txt with, per patched jar, the module name, why
it was selected and the descriptor it received, plus one line per jar left as is. The output
directory is emptied at the start of each run, so the report always describes the jars next to it.
Licence gate (opt-in)
Patching rewrites someone else’s jar. When <allowedLicenses> is set, every artifact that ends up
patched must declare one of the listed licences, or the build fails:
<allowedLicenses>
<license>Apache-2.0</license>
<license>EPL-2.0</license>
<license>MIT</license>
</allowedLicenses>
Names are normalised before comparison (The Apache Software License, Version 2.0 matches
Apache-2.0). An artifact declaring no licence at all is a violation. Left empty — the default —
the gate is off.
Other parameters
| Parameter / property | Default | Effect |
|---|---|---|
|
|
Skip the goal. |
|
empty |
Restrict patching to, or away from, these |
|
|
Generate |
|
|
Patch a jar even when one of its own |
A development loop needs an earlier binding
Vidocq’s dev goal does not fork a lifecycle: its rebuild loop runs mvn process-classes. Bound at
its default prepare-package, modularize never runs in such a session, and dev mode sees the
original jars. Bind it to process-classes when dev mode must run against the patched copies:
<execution>
<id>modularize</id>
<phase>process-classes</phase>
<goals><goal>modularize</goal></goals>
</execution>
Annotation-processor options
vauban-processor accepts these APT options (-A flags on javac, <compilerArgs> in Maven):
| Option | Effect |
|---|---|
|
Skips the static deployment validation (unsatisfied/ambiguous, unproxyable and circular-dependency checks). Useful when beans rely on injections that only a runtime Build Compatible Extension can satisfy (e.g. MicroProfile |
|
Default |
|
Default |
System properties
| Property | Effect |
|---|---|
|
Turns off the load-time weaving tier. Beans missing the |
|
Lets the container open a third-party module’s package to itself at boot ( |
|
What the container may do when an annotation reaches it as an object rather than as index data. Matching itself never reflects — qualifiers and interceptor bindings are compared as normalized keys — and injection takes a point’s qualifiers from its descriptor. Four things still can, and each goes through this switch: reading the members of an annotation instance, reading what an annotation type declares when neither the index nor a class file describes it, building an instance to hand to application code, and reading the annotations off a field or a parameter no descriptor describes. |
|
Comma-separated module-name prefixes that the Java SE launcher ( |
Java SE launcher NEW
java -p mods --add-modules ALL-MODULE-PATH \
-m io.vidocq.vauban.classloader/io.vidocq.vauban.classloader.Launch <module>/<main-class> [args]
--add-modules ALL-MODULE-PATH is required: with -m, the launcher is the only root module. Launch re-layers the application modules of the boot layer under a Vauban class loader and invokes the application’s public static void main(String[]) from inside the layer. Launch.run(target, args) is the programmatic form: it returns true when it ran the target in a fresh layer, and false, doing nothing, when the calling thread already runs in one — so if (Launch.run("app/app.Main", args)) return; can open an application’s own main. In a Vauban layer, in-package client proxies of the libraries it re-layers are placed (zero opens, no agent, no rewritten jar), their constructors do not run for the proxy, and unwoven beans are woven at definition without the load-time agent. The selection is public as VaubanLayerFactory.applicationPaths(configuration, keptPrefixes, roots), which Vidocq.run uses too, with the Vidocq bricks as extra kept prefixes. A named target module is a root: no keep prefix holds it back. Full story: Vauban in Java SE.
JUnit extension (vauban-junit)
@VaubanTest boots a lightweight container for tests:
@VaubanTest
@AddBeans(GreetingService.class)
class GreetingServiceTest {
@Inject GreetingService greeting;
@Test
void hello() {
assertEquals("Hello, Vauban!", greeting.hello("Vauban"));
}
}
Bean selection:
-
@VaubanTest— a plain marker (no members). Boots the container viaVaubanContainer.builder()before all tests, closes it after the last one, and injects the test instance’s@Injectfields. -
@AddBeans({Foo.class, Bar.class})— a separate annotation that registers the listed classes on the container builder (addBeanClass(…)).
CDI 4.1 Lite vs Full comparison
| Feature | Lite | Full | Vauban |
|---|---|---|---|
Managed beans, standard scopes |
✅ |
✅ |
✅ |
Producers, disposers |
✅ |
✅ |
✅ |
Events ( |
✅ |
✅ |
✅ |
Interceptors ( |
✅ |
✅ |
✅ |
Build Compatible Extensions (BCE) |
✅ |
✅ |
✅ |
Portable Extensions (runtime |
❌ |
✅ |
❌ |
|
✅ |
✅ |
|
|
❌ |
✅ |
❌ |
|
❌ |
✅ |
✅ with |
Passivation, bean serialisation |
❌ |
✅ |
❌ |
|
❌ |
✅ |
❌ |
EL for managed beans |
❌ |
✅ |
❌ |
Decorators |
Optional |
✅ |
// TODO@user: validate coverage |
Configuration
Vauban needs no application configuration file. Bean selection happens via:
-
the
module-info.java(Java Modules visibility); -
the bootstrap strategy (
scanLocal(),scanClasspath(),addBeanClass()).
For dynamic application configuration, use MicroProfile Config through Vidocq Runtime.
Bugs
-
BUG.md — tracked reproducible bugs.
Compatibility
-
Java 25 (LTS), Maven 3.9.16.
-
Generated class files target Java 25 (class file 69) whatever JDK runs the build, so a brick built on a newer JDK still loads on the baseline runtime and passes
jlink. Weaving keeps the version of the class it patches. -
Strict Java Modules, named modules only.
-
CDI 4.1 — Lite profile.
-
Project Leyden CDS and
jlinkimages. GraalVMnative-imageneeds reflection metadata today: see AOT compatibility.