This page covers the CDI 4.1 Lite patterns Vauban supports and how they integrate with the rest of the ecosystem. Every example compiles; the entire injection bytecode is generated at process-classes.
Scopes
CDI 4.1 Lite ships four standard scopes. Vauban supports all of them, plus @RequestScoped whenever a contextual transport is wired in.
| Scope | Semantics | Implemented |
|---|---|---|
|
One bean per JVM, no proxy. |
✅ |
|
One bean per container, normal-scope proxy. |
✅ |
|
One instance per injection point, no proxy. |
✅ (default) |
|
One instance per request, normal-scope proxy. |
✅ with Cassini or Foy active, or activated by your code (Activating the request context) NEW |
|
One instance per HTTP session. |
✅ with |
|
Normal-scoped beans ( |
Proxyable beans and the ProxyLink entry constructor
Because the client proxy subclasses the bean, CDI 4.1 declares some classes unproxyable
for normal scopes: final classes, classes with non-private final methods, and classes
without a non-private no-arg constructor. Final members are reported as errors (weaving
cannot fix them); the missing-constructor case — the common single-@Inject-constructor
style — is handled automatically:
In any javac-based build (Maven, Gradle, plain javac, the standard Vidocq setup),
nothing to do. The Vauban annotation processor ships an auto-started javac plugin that,
when compilation finishes, weaves a synthetic (ProxyLink) entry constructor into the
compiled bean class and points the generated proxy’s constructor at it. Your source never
declares it. Creating the proxy then runs no bean logic — no duplicated construction side
effects, no NullPointerException on dereferenced @Inject parameters. The
vauban-maven-plugin applies the same weaving at process-classes as a second layer and
for ecosystem jars that do not go through javac.
When the build output was not woven — typically an IDE build: IntelliJ’s build system
writes classes to disk after javac finishes, which defeats the javac plugin — the
container detects it at boot, before any bean class is loaded, and attaches the
vauban-weaver agent to the current JVM: the same transformation is applied when the
class is defined. You will see a Vauban warning listing the affected beans plus the JDK’s
standard dynamic-agent notice; behaviour is otherwise identical to a woven build. Disable
this tier with -Dvauban.weaving.loadtime=disabled, or avoid it entirely in IntelliJ by
enabling Delegate IDE build/run actions to Maven. See
Internals — client-proxy weaving tiers for the full
mechanics.
In a Vauban layer, no agent at all — under the Java SE launcher or Vidocq.run,
the layer’s class loader applies the same transformation as it defines each class, and the load-time
tier detects it and stands down.
Without any weaving tier, declare the entry constructor yourself
(io.vidocq.vauban.api.ProxyLink, a Vauban extension comparable to Weld’s relaxed
construction):
@ApplicationScoped
public class Oidc {
private final String issuer;
@Inject
public Oidc(Config config) {
this.issuer = config.issuer();
}
protected Oidc(ProxyLink link) { // client-proxy entry point — keep the body empty
this.issuer = null; // javac requires blank finals to be assigned
}
}
Either way the proxy chains to that constructor (always passing null), and the container
never selects it for injection or bean instantiation. The two differ on one point. The woven
constructor runs no field initializer at all: javac inlines initializers into the constructors it
compiles, and this one is added once javac has finished. A hand-written one is compiled like any
other and does run them — if you declare it yourself, keep field initializers side-effect free.
Which proxy do I get, and what do I have to do? NEW
Find your situation in the left column. Most of them ask nothing of you; the few that do, say so in one line. The mechanics behind each row are in Internals — one strategy per case.
| Your situation | What happens | What you have to do |
|---|---|---|
A normal-scoped bean you compile — an ordinary top-level class |
The annotation processor writes |
Nothing. |
The same bean, but nested (a |
The same, with one wrinkle handled for you: the proxy is named |
Nothing. |
|
The proxy is generated at build time in your producer’s package. |
Nothing. |
|
A forwarding implementation is generated in your producer’s package. It forwards to whatever your producer method returns — it is not a substitute for one. |
Return a real implementation. |
|
The proxy must live in the produced type’s own package. The build ships its bytes as a resource and the Vauban class loader places them there at run time. |
Start through the Vauban launcher
(Java SE or |
A dependency jar with no |
On the module path it is an automatic module: it reads everything, exports everything, and
|
Run |
A |
CDI 4.1 §3.10 forbids it: a client proxy is a subclass, and neither can be overridden. |
Drop |
What failure looks like
None of these fail silently. A produced type that needs an in-package proxy, started without the launcher, refuses deployment and names the way out:
Cannot reflectively access com.acme.lib.FraudScreen on the module path. Either (preferred)
provide a generated VaubanComponentProvider for its module — the APT or packaging plugin emits
a `_VaubanComponents` and the module declares
`provides io.vidocq.vauban.api.VaubanComponentProvider with …;` — so the container instantiates
it in-module without reflection; or open the package:
`opens com.acme.lib to io.vidocq.vauban.core;`
An unproxyable shape is a deployment error, reported with the bean and the reason:
Normal-scoped bean com.acme.Ledger cannot be a final class
Normal-scoped bean com.acme.Ledger has final method charge
And a build that ships an in-package proxy says so, so the launcher requirement is never a surprise at run time:
[WARNING] Shipped an in-package client proxy for 1 produced type(s) that cannot be proxied
from the producer's package: [com.acme.lib.FraudScreen]. The Vauban class loader defines them
inside the produced type's own package, which requires the application to start through
io.vidocq.vauban.classloader.Launch or Vidocq.run, and that package to be exported. Started
without a Vauban layer, the container falls back to opening the package at runtime and says
so at boot.
Qualifiers
Discriminate multiple implementations of the same type.
import jakarta.inject.Qualifier;
import java.lang.annotation.*;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface Audit {}
@ApplicationScoped @Audit
public class AuditLogger implements Logger { /* ... */ }
@ApplicationScoped
public class Service {
@Inject @Audit Logger logger;
}
@Named, @Default, @Any are supported. Custom qualifiers with @Nonbinding members work too.
NEW A @Named without a value on a static nested bean class takes the class’s simple name, as CDI 4.1 §3.1.5 requires: Outer.ReportService is named reportService, where it used to be named outer$ReportService.
Producers and @Disposes
@ApplicationScoped
public class DataSourceProducer {
@Produces @ApplicationScoped
public DataSource dataSource(Config config) {
return DataSource.builder().url(config.url()).build();
}
public void close(@Disposes DataSource ds) {
ds.close();
}
}
The @Disposes callback runs when the producing bean is destroyed. No dedicated class is generated per producer method: the container invokes producers and disposers through the module’s generated _VaubanComponents provider, with reflection only as a fallback.
NEW The disposer runs once per destroyed instance; it used to run twice, because its own call released the produced instance’s context a second time. It now gets a context of its own (CDI 4.1 §6.4.2). Every parameter of a disposer other than the @Disposes one is an injection point, qualifiers included (CDI 4.1 §10.4.3): a qualified parameter used to fail validation as an unsatisfied dependency, or receive the @Default bean at run time.
Events
public record OrderCreated(String id) {}
@ApplicationScoped
public class OrderService {
@Inject Event<OrderCreated> events;
public void create(String id) {
events.fire(new OrderCreated(id));
}
}
@ApplicationScoped
public class OrderListener {
public void onOrder(@Observes OrderCreated event) {
System.out.println("new order " + event.id());
}
}
Async observers (@ObservesAsync) are dispatched on the EventDispatcher’s internal `Executors.newVirtualThreadPerTaskExecutor(). See the threading model.
Interceptors
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Logged {}
@Logged
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor {
@AroundInvoke
public Object around(InvocationContext ctx) throws Exception {
long t0 = System.nanoTime();
try {
return ctx.proceed();
} finally {
System.out.printf("%s : %d ns%n", ctx.getMethod(), System.nanoTime() - t0);
}
}
}
@ApplicationScoped @Logged
public class Service { /* intercepted methods */ }
@AroundConstruct is also supported. Interceptor chains are resolved at compile time and stored in the generated _Factory.
Programmatic Instance<T>
@ApplicationScoped
public class Router {
@Inject Instance<Handler> handlers;
public Handler resolve(String name) {
return handlers.select(NamedLiteral.of(name)).get();
}
}
Instance<T> enables dynamic resolution from application code while keeping the injection grid static inside the container.
Integration with Cassini (Jakarta REST)
vauban-core is the underlying DI layer of Cassini. @Path resources are CDI beans; @Provider providers (MessageBodyReader/Writer) too. No configuration: Cassini consumes the Vauban BeanManager directly.
@Path("/orders")
@ApplicationScoped
public class OrderResource {
@Inject OrderService service;
@POST
public Response create(OrderRequest req) {
service.create(req.id());
return Response.created(URI.create("/orders/" + req.id())).build();
}
}
Activating the request context NEW
Outside a request served by Cassini or Foy, a batch job, a message listener, a scheduled task, the request context is not active. CDI Lite gives two ways to activate it (CDI 4.1 §6.5.8, §6.5.9), and Vauban provides both:
-
@ActivateRequestContexton a bean class or method: the built-in interceptor, at priorityInterceptor.Priority.PLATFORM_BEFORE + 100, activates the context around the call when it is not active, and deactivates it after. An active context is left as it is. -
RequestContextController, a@Dependentbuilt-in bean:activate()activates the context on the current thread and returnstrue, or returnsfalsewhen it is already active;deactivate()deactivates it only if that controller activated it, and throwsContextNotActiveExceptionwhen it is not active.
@Inject RequestContextController requestContext;
void runBatch() {
boolean activated = requestContext.activate();
try {
// @RequestScoped beans live until deactivate()
} finally {
if (activated) requestContext.deactivate();
}
}
The context is activated per thread: two threads each have their own request and their own @RequestScoped instances. Activation fires @Initialized(RequestScoped.class); deactivation fires @BeforeDestroyed, destroys the instances, then fires @Destroyed(RequestScoped.class).
Integration with Foy (Jakarta Servlet)
Foy exposes servlets, filters and listeners as CDI beans, and @SessionScoped follows the HTTP session, through the web contexts below. @RequestScoped follows the HTTP request: Foy activates the request context around each request through the built-in RequestContextController, each request on its own thread with its own instances.
Web contexts: @SessionScoped NEW
CDI Lite has no session context: CDI 4.1 §6.7.2 leaves it to the container that implements Servlet. vauban-webcontexts provides it, independent of any transport, and a web container drives it. With Foy there is nothing to do: foy-cdi-vauban depends on vauban-webcontexts and binds each request’s HttpSession to it.
-
One instance per session. A
@SessionScopedbean is created on first use in a session and kept in that session’s attributes, one attribute per bean, so every request of the session sees it. The first session-scoped bean a request uses creates the session, asgetSession()would. -
Ends with the session. When the session is invalidated or expires, Vauban fires
@BeforeDestroyed(SessionScoped.class), destroys the session’s instances (their@PreDestroymethods run, and may still use session-scoped beans), then fires@Destroyed(SessionScoped.class). A new session fires@Initialized(SessionScoped.class). The event payload is theHttpSession. -
Outside a request, the context is inactive: a call to a session-scoped bean throws
ContextNotActiveException. -
Not covered: passivation. Vauban does not check at deployment that a session-scoped bean is
Serializable, and the instances are not written out with the session.@ConversationScopedis not provided. -
The extension must be seen at build time.
vauban-webcontextsregisters its context from a build-compatible extension. When the application is compiled with Vauban’s annotation processor,foy-cdi-vaubanon the processor path brings it along.
The same scenarios, two clients, one cart each, a new cart after invalidate(), the events, pass on Foy with Vauban and on Foy hosting Weld’s own Servlet integration, which serves as the reference (Foy’s foy-it-vauban-session and foy-it-weld-servlet modules).
Another web container drives the context through WebContexts:
// When a request starts and ends, on the thread that serves it:
WebContexts.activateSession(store); // store: a SessionStore over the request's session
WebContexts.deactivateSession();
// When the web container creates and ends a session:
WebContexts.sessionInitialized(beanManager, session);
WebContexts.destroySession(beanManager, store, session);
SessionStore adapts the session’s attributes: get, set, remove, list, and the object to lock while an instance is created, the same for every request of one session.
JUnit tests with @VaubanTest
@VaubanTest
class GreetingServiceTest {
@Inject GreetingService greeting;
@Test
void hello() {
assertEquals("Hello, Vauban!", greeting.hello("Vauban"));
}
}
The extension boots a minimal container per test class. See the JUnit reference.