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) |
|
One instance per HTTP session. |
// TODO@user: confirm status with Foy / Cassini |
|
Normal-scoped beans ( |
Proxyable beans and the ProxyLink entry constructor NEW
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.
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. Field initializers still run in every
constructor — keep them side-effect free.
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.
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.
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();
}
}
Integration with Foy (Jakarta Servlet)
Foy exposes servlets, filters and listeners as CDI beans. @RequestScoped follows the HTTP request/response lifecycle.
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.