Java Module 25 – Jakarta REST (JAX-RS) and CDI: services beyond Faces
CDI outside Faces: beans, scopes and injection
What makes a Java class a CDI bean, how @ApplicationScoped, @RequestScoped and @Dependent differ, and CDI with Weld SE, in JUnit tests and on Tomcat.
What you will learn
- Explain what makes a class a CDI bean, how bean discovery works and what CDI Lite and Full are
- Choose between @ApplicationScoped, @RequestScoped and @Dependent, and inject dependencies through constructors and fields
- Run CDI in plain Java SE with Weld SE, in JUnit tests and in a Tomcat WAR
Before you start
What you’ll learn · Before you start
What you will learn
- Explain what makes a class a CDI bean, how bean discovery works and what CDI Lite and Full are
- Choose between @ApplicationScoped, @RequestScoped and @Dependent, and inject dependencies through constructors and fields
- Run CDI in plain Java SE with Weld SE, in JUnit tests and in a Tomcat WAR
Before you start
On this page
Contexts and Dependency Injection (CDI) is the Jakarta EE standard for wiring objects together. You write ordinary
classes, mark what each one needs with @Inject, and a container creates the objects, hands them their
dependencies and decides how long each one lives. A class the container can create this way is a bean. Its scope
sets its lifetime: @ApplicationScoped means one object for the whole application, @RequestScoped one object for
each request, and @Dependent, the default, a new object for every place it is injected. CDI is often met inside
Jakarta Faces, but it does not need a web page or a server: the same beans run in a plain main method with Weld SE,
in JUnit tests and in a WAR on Tomcat. Every run in this lesson is recorded with Weld 6.0.4.Final, which implements
CDI 4.1, the CDI version of Jakarta EE 11.
Which classes become beans
The CDI 4.1 specification makes a class a managed bean when the container can construct it and nothing rules it
out: it is a concrete class that is not an inner class, it has either a constructor without parameters or one
constructor annotated @Inject, it is not a CDI extension, and it is not marked @Vetoed. No interface to implement
and no registration file is needed.
Whether the container actually looks at a class is a separate question, answered by bean discovery. Weld SE and
Weld on Tomcat scan a JAR or a classes folder only when it holds a beans.xml file (META-INF/beans.xml in a JAR,
WEB-INF/beans.xml in a WAR), and the specification encourages that file in every archive with beans, although full
Jakarta EE servers also find annotated classes without it. The file’s bean-discovery-mode decides which classes
become beans:
| Mode | Which classes the container turns into beans | When it is used |
|---|---|---|
annotated |
only classes with a bean-defining annotation: a scope such as @ApplicationScoped or @Dependent, a stereotype or @Interceptor |
the default since CDI 4.0, also for an empty beans.xml |
all |
every class that meets the rules above, annotated or not | older code that never annotated its beans (CDI Full only) |
none |
no class at all | an archive that must stay out of CDI |
CDI 4.0 changed what an empty beans.xml means, from all to annotated, which is why older tutorials that rely
on unannotated beans now fail. The program below has one annotated bean, LateFee, which needs a Rounding helper
without any annotation, and it runs three times: with the project’s empty beans.xml, with a beans.xml that says
all, and with no beans.xml at all.
package com.example.fees;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
/** 37 rupees for every day a fee is paid late, rounded to the nearest 10. */
@ApplicationScoped
public class LateFee {
private final Rounding rounding;
@Inject
public LateFee(Rounding rounding) {
this.rounding = rounding;
}
/** For the client proxy: a normal-scoped bean needs a constructor without parameters that is not private. */
protected LateFee() {
this.rounding = null;
}
public long forDays(int days) {
return rounding.toNearestTen(days * 37L);
}
} from session import run
# The same program three times: with the empty beans.xml of src/main/resources/META-INF, with beans-all.xml in its
# place, and with no beans.xml at all.
EMPTY = "java -cp target/classes:$(cat cp.txt) com.example.fees.Main # beans.xml is empty"
ALL = 'java -cp target/classes:$(cat cp.txt) com.example.fees.Main # bean-discovery-mode="all"'
NONE = "java -cp target/classes:$(cat cp.txt) com.example.fees.Main # no beans.xml"
run(
f"""
cd discovery
mvn -q compile dependency:build-classpath -Dmdep.outputFile=cp.txt
wc -c < target/classes/META-INF/beans.xml
{EMPTY}
cp beans-all.xml target/classes/META-INF/beans.xml
{ALL}
rm target/classes/META-INF/beans.xml
{NONE}
""",
fails=(EMPTY, NONE),
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0
$ cd discovery
$ mvn -q compile dependency:build-classpath -Dmdep.outputFile=cp.txt
$ wc -c < target/classes/META-INF/beans.xml
0
$ java -cp target/classes:$(cat cp.txt) com.example.fees.Main # beans.xml is empty
INFO: WELD-000900: 6.0.4 (Final)
INFO: WELD-ENV-000014: Falling back to Java Reflection for bean-discovery-mode="annotated" discovery. Add io.smallrye:jandex to the classpath to speed-up startup.
INFO: WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.
the container did not start: WELD-001408: Unsatisfied dependencies for type Rounding with qualifiers @Default
at injection point [BackedAnnotatedParameter] Parameter 1 of [BackedAnnotatedConstructor] @Inject public com.example.fees.LateFee(Rounding)
at com.example.fees.LateFee.<init>(LateFee.java:0)
[exit status 1]
$ cp beans-all.xml target/classes/META-INF/beans.xml
$ java -cp target/classes:$(cat cp.txt) com.example.fees.Main # bean-discovery-mode="all"
INFO: WELD-000900: 6.0.4 (Final)
INFO: WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.
INFO: WELD-ENV-002003: Weld SE container <id> initialized
late fee for 7 days: 260 rupees
INFO: WELD-ENV-002001: Weld SE container <id> shut down
$ rm target/classes/META-INF/beans.xml
$ java -cp target/classes:$(cat cp.txt) com.example.fees.Main # no beans.xml
the container did not start: WELD-ENV-000016: Missing beans.xml file in META-INF
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 discovery_session.py
With the empty file, Rounding was not a bean, so the container refused to start with WELD-001408: Unsatisfied dependencies and named the injection point, the first parameter of LateFee’s constructor. The problem appeared at
start-up, before any business code ran, which is one of CDI’s strengths: wiring mistakes surface when the
application starts rather than when a user reaches the broken path. With all the helper became a bean and the late
fee for 7 days came out as 259 rupees rounded to 260. Without any beans.xml, Weld SE stopped with WELD-ENV-000016:
it scans only archives that have the file. Annotating the helper with @ApplicationScoped or @Dependent would fix
the first run too, and that is the better habit, because CDI Lite implementations must treat all as a deployment
problem.
Injecting through constructors and fields
CDI injects in three places: a constructor annotated @Inject, fields annotated @Inject, and initializer methods
annotated @Inject. The fee example uses the first two:
fees/src/main/java/com/example/fees/RegistrationDesk.java
package com.example.fees;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
/** Registers a student for a number of credits. @Dependent: every injection point gets a new desk. */
@Dependent
public class RegistrationDesk {
private final FeeCalculator fees;
@Inject // constructor injection: the container calls this constructor with a FeeCalculator
public RegistrationDesk(FeeCalculator fees) {
this.fees = fees;
}
public String register(String student, int credits) {
return student + " registered for " + credits + " credits, fee " + fees.feeFor(credits) + " rupees";
}
/** What the container injected, for the program to print. */
FeeCalculator fees() {
return fees;
}
} fees/src/main/java/com/example/fees/FeeCalculator.java
package com.example.fees;
import jakarta.enterprise.context.ApplicationScoped;
/** Course fees: one object for the whole application, which the container creates the first time it is used. */
@ApplicationScoped
public class FeeCalculator {
/** How many FeeCalculator objects exist, so that the program can show when the container makes one. */
static int objects;
private static final long RUPEES_PER_CREDIT = 2_500;
public FeeCalculator() {
objects++;
}
public long feeFor(int credits) {
if (credits < 1 || credits > 30) {
throw new IllegalArgumentException("credits must be from 1 to 30, not " + credits);
}
return credits * RUPEES_PER_CREDIT;
}
} fees/src/main/java/com/example/fees/NightlyReport.java
package com.example.fees;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
/** A job that runs outside any web request, so no request context is active unless it starts one. */
@ApplicationScoped
public class NightlyReport {
@Inject // field injection: the container sets the field after the constructor has run
RequestTrace trace;
public String runWithoutContext() {
trace.step("load registrations");
return trace.summary();
}
@ActivateRequestContext // a request context for the length of this call
public String run() {
trace.step("load registrations");
trace.step("total the fees");
return trace.summary();
}
} fees/src/main/java/com/example/fees/RequestTrace.java
package com.example.fees;
import jakarta.enterprise.context.RequestScoped;
import java.util.ArrayList;
import java.util.List;
/** What happened during one request. @RequestScoped: one object per request, thrown away when the request ends. */
@RequestScoped
public class RequestTrace {
private static int next = 1;
private final int number = next++;
private final List<String> steps = new ArrayList<>();
public void step(String what) {
steps.add(what);
}
public String summary() {
return "request " + number + ": " + String.join(", ", steps);
}
} Constructor injection is the one to prefer. The dependency is a final field, the class cannot exist without it, and
a unit test can build the object with new RegistrationDesk(new FeeCalculator()), no container needed. Field
injection is shorter, but the field cannot be final, it stays null until the container fills it, and a test has
to set it by reflection or start a container. It still has its place: a servlet is created by Tomcat, not by CDI, so
its dependencies arrive in fields or initializer methods, never through its constructor, as the Tomcat example below
shows. Whichever form you use, the container
matches injection points by type (and by qualifiers, the subject of the next lesson), so RegistrationDesk asks for a
FeeCalculator and never says how to make one.
Three scopes and the client proxy
Scopes and client proxies in the fee example
Text description of the diagram
Two chains of boxes, read from top to bottom.
1. "Two RegistrationDesk objects, @Dependent: a new one for every injection point". An arrow labelled "each desk's field" leads to "FeeCalculator client proxy, one per bean, shared". From the proxy, an arrow labelled "every call goes to the current object" leads to "FeeCalculator, @ApplicationScoped: one object, made at the first call". 2. "NightlyReport, @ApplicationScoped". An arrow labelled "its field" leads to "RequestTrace client proxy", and from there an arrow labelled "the object of the request now active" leads to "RequestTrace, @RequestScoped: one object per active request context".
The desks and the report never hold the scoped objects themselves, only proxies, which find the right object on every call.
| Scope | How many objects | When the object is made | Injected as |
|---|---|---|---|
@ApplicationScoped |
one for the application | at the first method call | a client proxy |
@RequestScoped |
one per active request context | at the first call within that request | a client proxy |
@Dependent (the default) |
one for every injection point or lookup | together with the object it is injected into | the object itself |
@ApplicationScoped and @RequestScoped are normal scopes: what gets injected is a client proxy, a generated
subclass that finds the right object on every call. @Dependent is a pseudo-scope, so its objects are injected
directly and belong to whatever they were injected into. Main starts a Weld SE container with the specification’s
SeContainerInitializer and looks at what it receives:
package com.example.fees;
import jakarta.enterprise.context.ContextNotActiveException;
import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
public class Main {
public static void main(String[] args) {
// Weld logs through java.util.logging; this format prints each message on one line, without the date.
System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s%n");
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
RegistrationDesk first = container.select(RegistrationDesk.class).get();
System.out.println("1. the desk received " + first.fees().getClass().getName());
System.out.println(" FeeCalculator objects so far: " + FeeCalculator.objects);
System.out.println("2. " + first.register("Asha", 20));
System.out.println(" FeeCalculator objects so far: " + FeeCalculator.objects);
RegistrationDesk second = container.select(RegistrationDesk.class).get();
System.out.println("3. " + second.register("Ravi", 16));
System.out.println(" two desks, one object? " + (first == second)
+ "; one proxy in both? " + (first.fees() == second.fees())
+ "; FeeCalculator objects: " + FeeCalculator.objects);
NightlyReport report = container.select(NightlyReport.class).get();
try {
report.runWithoutContext();
} catch (ContextNotActiveException e) {
System.out.println("4. " + e.getClass().getName() + ": " + e.getMessage());
}
System.out.println("5. " + report.run());
System.out.println(" " + report.run());
}
}
} from session import run
# Compiles the project, asks Maven for the class path of Weld SE and its dependencies, then runs Main.
run(
"""
cd fees
mvn -q compile dependency:build-classpath -Dmdep.outputFile=cp.txt
java -cp target/classes:$(cat cp.txt) com.example.fees.Main
"""
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 $ cd fees $ mvn -q compile dependency:build-classpath -Dmdep.outputFile=cp.txt $ java -cp target/classes:$(cat cp.txt) com.example.fees.Main INFO: WELD-000900: 6.0.4 (Final) INFO: WELD-ENV-000014: Falling back to Java Reflection for bean-discovery-mode="annotated" discovery. Add io.smallrye:jandex to the classpath to speed-up startup. INFO: WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously. INFO: WELD-ENV-002003: Weld SE container <id> initialized 1. the desk received com.example.fees.FeeCalculator$Proxy$_$$_WeldClientProxy FeeCalculator objects so far: 0 2. Asha registered for 20 credits, fee 50000 rupees FeeCalculator objects so far: 1 3. Ravi registered for 16 credits, fee 40000 rupees two desks, one object? false; one proxy in both? true; FeeCalculator objects: 1 4. org.jboss.weld.contexts.ContextNotActiveException: WELD-001303: No active contexts for scope type jakarta.enterprise.context.RequestScoped 5. request 1: load registrations, total the fees request 2: load registrations, total the fees INFO: WELD-ENV-002001: Weld SE container <id> shut down
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scopes_session.py
Line 1 shows the proxy: the desk’s field holds a FeeCalculator$Proxy$_$$_WeldClientProxy, and no FeeCalculator
existed yet. The real object appeared with the first call, in line 2, and line 3 shows two desks, each a separate
object as @Dependent promises, both working with that same single FeeCalculator. Weld even handed both desks the
same proxy object; the specification allows that but does not require it, so never compare proxies with == in
real code.
The proxy is what makes mixing lifetimes safe. NightlyReport lives as long as the application, while each
RequestTrace lives for one request, so the report cannot hold a trace itself. It holds a proxy that asks, on every
call, for the trace of the request that is active on the current thread. When none is active, the call fails, as
line 4 shows with WELD-001303: No active contexts for scope type jakarta.enterprise.context.RequestScoped. In a web
application Weld starts a request context for every HTTP request. A job outside any request starts one itself:
@ActivateRequestContext on a method gives each call its own context, which is why the two runs in line 5 got
request 1 and request 2, each with both of its steps. The specification also offers RequestContextController for
code that needs to start and stop the context by hand.
What a client proxy needs from your class
A proxy is a subclass, so the container can only build it for classes that can be subclassed. CDI 4.1 lists the bean
types it cannot proxy: final classes, classes with non-static final methods that are public, protected or
package-private, classes without a non-private constructor that takes no parameters, sealed classes and interfaces,
primitives and arrays. Records are final, so they belong on that list as well. The next program tries three such
types as @ApplicationScoped beans, each in a container of its own, and then counts objects:
package com.example.fees;
import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
import java.util.logging.Level;
import java.util.logging.Logger;
/** Three normal-scoped beans whose bean types cannot be proxied, each in a container of its own. */
public class Proxies {
// Only Weld's warnings and errors, so that the results stand out.
private static final Logger WELD = Logger.getLogger("org.jboss.weld");
public static void main(String[] args) {
WELD.setLevel(Level.WARNING);
tryWith("final class Tariff", TariffQuote.class, Tariff.class);
tryWith("record HostelRate", HostelQuote.class, HostelRate.class);
tryWith("sealed interface Rates", RatesQuote.class, SlabRates.class);
}
static void tryWith(String what, Class<?> consumer, Class<?> bean) {
System.out.println(what + ":");
SeContainer container;
try {
container = SeContainerInitializer.newInstance().disableDiscovery().addBeanClasses(consumer, bean).initialize();
} catch (RuntimeException e) {
System.out.println(" the container did not start: " + firstLine(e));
return;
}
try (container) {
container.select(consumer).get();
System.out.println(" injected");
} catch (RuntimeException e) {
System.out.println(" the container started, the first lookup failed: " + firstLine(e));
for (Throwable cause = e.getCause(); cause != null; cause = cause.getCause()) {
System.out.println(" caused by " + cause.getClass().getSimpleName() + ": " + firstLine(cause));
}
}
}
static String firstLine(Throwable t) {
return t.getMessage().lines().findFirst().orElse("");
}
} from session import run
# The unproxyable types first; then the same container twice, with Weld SE's default and with the rule of the
# specification, which Weld follows everywhere else.
run(
"""
cd proxies
mvn -q compile dependency:build-classpath -Dmdep.outputFile=cp.txt
java -cp target/classes:$(cat cp.txt) com.example.fees.Proxies
java -cp target/classes:$(cat cp.txt) com.example.fees.Construction
java -Dorg.jboss.weld.construction.relaxed=false -cp target/classes:$(cat cp.txt) com.example.fees.Construction
"""
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 $ cd proxies $ mvn -q compile dependency:build-classpath -Dmdep.outputFile=cp.txt $ java -cp target/classes:$(cat cp.txt) com.example.fees.Proxies final class Tariff: the container did not start: WELD-001410: The injection point has non-proxyable dependencies: [BackedAnnotatedField] @Inject com.example.fees.TariffQuote.tariff record HostelRate: the container did not start: WELD-001410: The injection point has non-proxyable dependencies: [BackedAnnotatedField] @Inject com.example.fees.HostelQuote.rate sealed interface Rates: the container started, the first lookup failed: WELD-001524: Unable to load proxy class for bean Managed Bean [class com.example.fees.SlabRates] with qualifiers [@Any @Default] with class class com.example.fees.SlabRates caused by IncompatibleClassChangeError: Failed listed permitted subclass check: class com.example.fees.SlabRates$Proxy$_$$_WeldClientProxy is not a permitted subclass of com.example.fees.Rates $ java -cp target/classes:$(cat cp.txt) com.example.fees.Construction org.jboss.weld.construction.relaxed=(not set) ExamFees objects after the lookup: 0 ExamFees objects after the first call: 1 Penalties, with no constructor without parameters: late paper costs 200 rupees $ java -Dorg.jboss.weld.construction.relaxed=false -cp target/classes:$(cat cp.txt) com.example.fees.Construction org.jboss.weld.construction.relaxed=false ExamFees objects after the lookup: 1 ExamFees objects after the first call: 2 Penalties, with no constructor without parameters: WELD-001435: Normal scoped bean class com.example.fees.Penalties is not proxyable because it has no no-args constructor - <unknownjakarta.enterprise.inject.spi.Bean instance>.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 proxies_session.py
The final class and the record were caught at start-up with WELD-001410: The injection point has non-proxyable dependencies. The sealed interface was not: the container started, and the first lookup failed with WELD-001524,
because the JVM refused to load a proxy class that the interface does not permit. Keep records and sealed hierarchies
as the values your beans pass around, and put behaviour in ordinary non-final service classes.
The last two runs show a difference that catches many teams. Weld SE switches on what Weld calls relaxed
construction, so it allocates proxies without calling a constructor: no ExamFees object existed after the lookup,
and Penalties, which has only an @Inject constructor, worked. Elsewhere Weld’s default is false, the
specification’s rule, reproduced here with -Dorg.jboss.weld.construction.relaxed=false. Then the proxy runs the
class’s constructor, so 1 object existed before any call and 2 after it, and Penalties failed with WELD-001435,
because a normal-scoped bean needs a constructor without parameters that is not private. Two habits keep code portable: give normal-scoped
beans that use constructor injection a protected constructor without parameters, as LateFee above has, and never
do work in their constructors. Start-up work belongs in a @PostConstruct method, which runs once for the real
object, as a later lesson of this module shows.
Tests that start only the beans they need
A test does not have to scan the class path. SeContainerInitializer.disableDiscovery() switches scanning off, and
addBeanClasses(...) names exactly the classes the test needs. The specification puts them in a synthetic bean
archive that behaves like one with bean-discovery-mode="all", so they become beans even without annotations, and no
beans.xml is involved:
package com.example.fees;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class RegistrationDeskTest {
static SeContainer container;
@BeforeAll
static void startContainer() {
// No scanning: the container knows exactly the two classes this test needs.
container = SeContainerInitializer.newInstance()
.disableDiscovery()
.addBeanClasses(FeeCalculator.class, RegistrationDesk.class)
.initialize();
}
@AfterAll
static void stopContainer() {
container.close();
}
@Test
void feeIsCreditsTimesTheRate() {
RegistrationDesk desk = container.select(RegistrationDesk.class).get();
assertEquals("Asha registered for 20 credits, fee 50000 rupees", desk.register("Asha", 20));
}
@Test
void everyLookupGetsItsOwnDesk() {
assertNotSame(container.select(RegistrationDesk.class).get(), container.select(RegistrationDesk.class).get());
}
@Test
void onlyTheListedBeansExist() {
assertTrue(container.select(NightlyReport.class).isUnsatisfied());
}
@Test
void constructorInjectionNeedsNoContainer() {
// A plain unit test: pass the dependency yourself.
RegistrationDesk desk = new RegistrationDesk(new FeeCalculator());
assertEquals("Ravi registered for 16 credits, fee 40000 rupees", desk.register("Ravi", 16));
}
} from session import run
# Runs the JUnit 6 tests, which start a container that knows only the beans they list.
run(
"""
cd fees
mvn test
"""
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 $ cd fees $ mvn test [INFO] T E S T S [INFO] Running com.example.fees.RegistrationDeskTest INFO: WELD-000900: 6.0.4 (Final) INFO: WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously. INFO: WELD-ENV-002003: Weld SE container <id> initialized INFO: WELD-ENV-002001: Weld SE container <id> shut down [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: … s -- in com.example.fees.RegistrationDeskTest [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tests_session.py
All four tests passed. onlyTheListedBeansExist proves the container knows nothing beyond the two classes: a lookup
of NightlyReport is unsatisfied. The last test needs no container at all, because constructor injection lets it
pass the dependency by hand. Starting the container in @BeforeAll means Weld starts once for the whole test
class, not once for every test.
The same beans in a WAR on Tomcat
Tomcat 11 is a servlet container: it implements Servlet 6.1 and ships no CDI. A WAR that uses CDI therefore carries an
implementation in WEB-INF/lib, here weld-servlet-shaded, which bundles the CDI API and Weld in one JAR, and it
needs WEB-INF/beans.xml. When Tomcat deploys the WAR, Weld starts through the ServletContainerInitializer
mechanism and injects into servlets, filters and listeners:
webapp/src/main/java/com/example/fees/FeeServlet.java
package com.example.fees;
import jakarta.inject.Inject;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/** GET /fees/fee?credits=20. Tomcat creates the servlet; Weld injects its fields. */
@WebServlet("/fee")
public class FeeServlet extends HttpServlet {
@Inject FeeCalculator calculator;
@Inject RequestTrace trace;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
int credits = Integer.parseInt(request.getParameter("credits"));
long fee = calculator.feeFor(credits);
response.setContentType("text/plain; charset=UTF-8");
response.getWriter().println("trace " + trace.number() + ": " + credits + " credits, fee " + fee + " rupees; "
+ "calculator calls " + calculator.calls() + ", FeeCalculator objects " + FeeCalculator.OBJECTS.get());
}
} webapp/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- The same kind of beans in a WAR for Tomcat 11. Tomcat ships no CDI, so Weld travels in WEB-INF/lib. -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>fees</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Weld for servlet containers in one jar: the CDI API, Weld itself and its start-up hook for Tomcat. -->
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-shaded</artifactId>
<version>6.0.4.Final</version>
</dependency>
<!-- Tomcat provides the Servlet API at run time, so it stays out of the WAR. -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>fees</finalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project> webapp/server.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- A minimal Tomcat 11 for local work: HTTP on 127.0.0.1:28465 only, and a shutdown port that needs this long
command. The WAR in webapps/ is deployed when Tomcat starts. -->
<Server port="28475" shutdown="stop-fees-28475-a7c3e9">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Service name="Catalina">
<Connector port="28465" address="127.0.0.1" protocol="HTTP/1.1" connectionTimeout="20000" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false" />
</Engine>
</Service>
</Server> The session builds the WAR, gives Tomcat a CATALINA_BASE of its own with that server.xml, starts it, sends two
requests and stops it again:
from session import run
# Builds the WAR, gives Tomcat a CATALINA_BASE of its own (conf/, logs/, webapps/, work/ and temp/ in this folder),
# starts it, sends two requests, shows Weld's lines of the log and stops Tomcat.
run(
"""
cd webapp
mvn -q package
unzip -Z1 target/fees.war | grep -E "beans.xml|lib/"
mkdir -p base/conf base/logs base/temp base/webapps base/work
cp $CATALINA_HOME/conf/web.xml $CATALINA_HOME/conf/logging.properties base/conf/
cp server.xml base/conf/
cp target/fees.war base/webapps/
$CATALINA_HOME/bin/catalina.sh start
curl -s "http://127.0.0.1:28465/fees/fee?credits=20"
curl -s "http://127.0.0.1:28465/fees/fee?credits=16"
grep -o "WELD-[A-Z0-9-]*: .*" base/logs/catalina.out
$CATALINA_HOME/bin/catalina.sh stop
""",
port=28465,
base="webapp/base",
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 · Apache Tomcat 11.0.26 $ cd webapp $ mvn -q package $ unzip -Z1 target/fees.war | grep -E "beans.xml|lib/" WEB-INF/lib/ WEB-INF/beans.xml WEB-INF/lib/jakarta.annotation-api-3.0.0.jar WEB-INF/lib/weld-servlet-shaded-6.0.4.Final.jar $ mkdir -p base/conf base/logs base/temp base/webapps base/work $ cp $CATALINA_HOME/conf/web.xml $CATALINA_HOME/conf/logging.properties base/conf/ $ cp server.xml base/conf/ $ cp target/fees.war base/webapps/ $ $CATALINA_HOME/bin/catalina.sh start Tomcat started. $ curl -s "http://127.0.0.1:28465/fees/fee?credits=20" trace 2: 20 credits, fee 50000 rupees; calculator calls 1, FeeCalculator objects 2 $ curl -s "http://127.0.0.1:28465/fees/fee?credits=16" trace 3: 16 credits, fee 40000 rupees; calculator calls 2, FeeCalculator objects 2 $ grep -o "WELD-[A-Z0-9-]*: .*" base/logs/catalina.out WELD-ENV-001008: Initialize Weld using ServletContainerInitializer WELD-000900: 6.0.4 (Final) WELD-ENV-000014: Falling back to Java Reflection for bean-discovery-mode="annotated" discovery. Add io.smallrye:jandex to the classpath to speed-up startup. WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously. WELD-ENV-001100: Tomcat 7+ detected, CDI injection will be available in Servlets, Filters and Listeners. $ $CATALINA_HOME/bin/catalina.sh stop
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tomcat_session.py
Weld logged that it started through the ServletContainerInitializer and that injection works in servlets, filters
and listeners. Each request got a trace of its own while the calculator’s count of calls kept growing, so the scopes
behaved as in Java SE. The numbers show the other side of relaxed construction, though. The first request printed
trace 2, not 1, and there were 2 FeeCalculator objects for 1 calculator: Weld on Tomcat follows the
specification, so building each proxy ran the class’s constructor and field initializers once. That is harmless here
and costly when a constructor opens a connection, which is one more reason to keep constructors of normal-scoped
beans empty.
CDI Lite and CDI Full
Since CDI 4.0 the specification comes in two parts. CDI Lite is the subset that can be implemented in restricted
environments, including ones that wire beans at build time: beans, the built-in scopes, qualifiers, producers, events,
interceptors bound by annotation and alternatives chosen with @Priority. Jakarta EE 11’s Core Profile includes CDI
Lite 4.1. CDI Full adds everything else, among it decorators, specialization, the session and conversation scopes,
portable extensions and bean-discovery-mode="all", and it is part of the Web Profile and the full Platform. Weld
implements CDI Full, and the specification requires every implementation that supports Java SE to do the same, so
everything in this module runs on it.
CDI 4.1 and CDI 5.0
CDI 4.1 belongs to Jakarta EE 11, and Weld 6 implements it. CDI 5.0, for Jakarta EE 12, requires Java SE 17 or later
and moves the API to the Maven coordinates jakarta.cdi:jakarta.cdi-api; Weld 7 implements it. The lessons of this
module stay on CDI 4.1 with Weld 6.0.4.Final, the versions of the Jakarta EE 11 generation.
Your turn: let the container wire a hostel desk
HandWired.java builds a small hostel allotment system by hand: one RoomRates table and one AllotmentLedger,
shared by two AllotmentDesk objects. Turn the three classes into beans with the scopes they need and constructor
injection, without changing what they compute. The sample checks in AllotmentTest.java start a container that
knows only these three classes and look at what it made of them:
exercise/src/main/java/com/example/hostel/HandWired.java
package com.example.hostel;
/** The object graph wired by hand: what the container should do for you. */
public class HandWired {
public static void main(String[] args) {
RoomRates rates = new RoomRates();
AllotmentLedger ledger = new AllotmentLedger();
AllotmentDesk morning = new AllotmentDesk(rates, ledger);
AllotmentDesk evening = new AllotmentDesk(rates, ledger);
morning.allot("Meera", "DOUBLE", 10);
evening.allot("Kabir", "SINGLE", 10);
System.out.println(ledger.count() + " allotments, " + ledger.total() + " rupees");
}
} exercise/src/main/java/com/example/hostel/AllotmentDesk.java
package com.example.hostel;
/** Allots a room to one student. A desk keeps nothing between allotments, so each user may have its own. */
public class AllotmentDesk {
private final RoomRates rates;
private final AllotmentLedger ledger;
public AllotmentDesk(RoomRates rates, AllotmentLedger ledger) {
this.rates = rates;
this.ledger = ledger;
}
public long allot(String student, String roomType, int months) {
long fee = rates.monthly(roomType) * months;
ledger.record(student, roomType, fee);
return fee;
}
} exercise/src/main/java/com/example/hostel/RoomRates.java
package com.example.hostel;
import java.util.Map;
/** Monthly hostel rent by room type, in rupees. It holds no state of its own. */
public class RoomRates {
private static final Map<String, Long> MONTHLY = Map.of("SINGLE", 9_000L, "DOUBLE", 6_500L, "TRIPLE", 5_000L);
public long monthly(String roomType) {
Long rent = MONTHLY.get(roomType);
if (rent == null) {
throw new IllegalArgumentException("unknown room type " + roomType);
}
return rent;
}
} exercise/src/main/java/com/example/hostel/AllotmentLedger.java
package com.example.hostel;
import java.util.ArrayList;
import java.util.List;
/** Every allotment of the hostel: one ledger that all desks must share. */
public class AllotmentLedger {
private final List<String> entries = new ArrayList<>();
private long total;
public synchronized void record(String student, String roomType, long fee) {
entries.add(student + " " + roomType + " " + fee);
total += fee;
}
public synchronized int count() {
return entries.size();
}
public synchronized long total() {
return total;
}
} exercise/src/test/java/com/example/hostel/AllotmentTest.java
package com.example.hostel;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.inject.Inject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Set;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/** The sample checks: a container that knows the three classes, and what it must make of them. */
class AllotmentTest {
static SeContainer container;
@BeforeAll
static void start() {
container = SeContainerInitializer.newInstance().disableDiscovery()
.addBeanClasses(RoomRates.class, AllotmentLedger.class, AllotmentDesk.class).initialize();
}
@AfterAll
static void stop() {
container.close();
}
static Class<?> scopeOf(Class<?> type) {
Set<Bean<?>> beans = container.getBeanManager().getBeans(type);
assertEquals(1, beans.size(), type.getSimpleName() + " should be a bean");
return beans.iterator().next().getScope();
}
@Test
void ratesAreOneObjectForTheApplication() {
assertEquals(ApplicationScoped.class, scopeOf(RoomRates.class), "the scope of RoomRates");
}
@Test
void ledgerIsOneObjectForTheApplication() {
assertEquals(ApplicationScoped.class, scopeOf(AllotmentLedger.class), "the scope of AllotmentLedger");
}
@Test
void everyLookupGetsItsOwnDesk() {
assertEquals(Dependent.class, scopeOf(AllotmentDesk.class), "the scope of AllotmentDesk");
assertNotSame(container.select(AllotmentDesk.class).get(), container.select(AllotmentDesk.class).get());
}
@Test
void deskGetsItsDependenciesThroughItsConstructor() {
Constructor<?>[] injected = Arrays.stream(AllotmentDesk.class.getDeclaredConstructors())
.filter(c -> c.isAnnotationPresent(Inject.class)).toArray(Constructor[]::new);
assertEquals(1, injected.length, "AllotmentDesk constructors annotated @Inject");
assertTrue(Arrays.stream(AllotmentDesk.class.getDeclaredFields()).allMatch(f -> Modifier.isFinal(f.getModifiers())),
"the fields of AllotmentDesk are final");
}
@Test
void sharedBeansKeepAConstructorTheProxyCanUse() {
for (Class<?> type : new Class<?>[] { RoomRates.class, AllotmentLedger.class }) {
assertTrue(Arrays.stream(type.getDeclaredConstructors())
.anyMatch(c -> c.getParameterCount() == 0 && !Modifier.isPrivate(c.getModifiers())),
type.getSimpleName() + " has a constructor without parameters that is not private");
}
}
@Test
void allotmentsFromTwoDesksLandInOneLedger() {
AllotmentLedger ledger = container.select(AllotmentLedger.class).get();
int before = ledger.count();
long totalBefore = ledger.total();
container.select(AllotmentDesk.class).get().allot("Meera", "DOUBLE", 10);
container.select(AllotmentDesk.class).get().allot("Kabir", "SINGLE", 10);
assertEquals(before + 2, ledger.count(), "allotments in the ledger");
assertEquals(totalBefore + 155_000, ledger.total(), "rupees in the ledger");
}
} from session import run
# The sample checks (src/test/java/com/example/hostel/AllotmentTest.java) against the starter, then against a
# solution copied over it. Of Maven's output, only the summary of the results is shown.
STARTER = "mvn test # the starter"
SOLVED = "mvn test # a solution"
run(
f"""
cd exercise
{STARTER}
cp solution/*.java src/main/java/com/example/hostel/
{SOLVED}
""",
fails=(STARTER,),
only=r"^\[(?:ERROR|INFO)\] (?:Tests run: \d+, Failures: \d+, Errors: \d+, Skipped: \d+$|Failures:|Errors:| \w+\.\w+|BUILD )",
) Output
# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 $ cd exercise $ mvn test # the starter [ERROR] Failures: [ERROR] AllotmentTest.deskGetsItsDependenciesThroughItsConstructor:62 AllotmentDesk constructors annotated @Inject ==> expected: <1> but was: <0> [ERROR] AllotmentTest.everyLookupGetsItsOwnDesk:54->scopeOf:38 AllotmentDesk should be a bean ==> expected: <1> but was: <0> [ERROR] AllotmentTest.ledgerIsOneObjectForTheApplication:49 the scope of AllotmentLedger ==> expected: <jakarta.enterprise.context.ApplicationScoped> but was: <jakarta.enterprise.context.Dependent> [ERROR] AllotmentTest.ratesAreOneObjectForTheApplication:44 the scope of RoomRates ==> expected: <jakarta.enterprise.context.ApplicationScoped> but was: <jakarta.enterprise.context.Dependent> [ERROR] Errors: [ERROR] AllotmentTest.allotmentsFromTwoDesksLandInOneLedger:81 » UnsatisfiedResolution WELD-001334: Unsatisfied dependencies for type AllotmentDesk with qualifiers [ERROR] Tests run: 6, Failures: 4, Errors: 1, Skipped: 0 [ERROR] BUILD FAILURE [exit status 1] $ cp solution/*.java src/main/java/com/example/hostel/ $ mvn test # a solution [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py
You are done when mvn test reports 6 tests with no failures. First hint: ask of each class how many objects of it
the hostel needs. A desk that keeps nothing between calls can be made per use, but rates and the ledger must be
shared, or two desks would write into two different ledgers.
Compare your solution
One solution adds @ApplicationScoped to RoomRates and AllotmentLedger, and @Dependent with @Inject on the
existing constructor of AllotmentDesk. Nothing else changes: the fields of the desk stay final, and the two shared
classes keep the constructor without parameters that Java gives them, which their proxies need. Leaving the desk
without a scope annotation would also work in this test, because the synthetic archive treats every class as a bean,
but in a real project with an empty beans.xml the desk would then not be a bean at all, so the annotation belongs
there.
Interview questions
Fresher · What is the difference between @ApplicationScoped and @Dependent in CDI?
@ApplicationScoped gives one object for the whole application. Every injection point receives a client proxy, a
generated subclass that sends each call to that single object, which the container creates lazily at the first call
and destroys when the application stops. Because one object serves every caller, it must be safe to use from many
threads. @Dependent is the default scope: every injection point and every lookup gets a new object of its own,
created together with the object it is injected into and destroyed with it. No proxy is involved, so the class may
even be final. Choose @ApplicationScoped for stateless services, caches and shared configuration, and @Dependent
for small helpers whose state belongs to one user. A normal-scoped class needs a non-private constructor without
parameters and no final methods that other classes can call; a dependent one does not.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A service uses a @RequestScoped bean and works when servlets call it. The same service, called from a nightly job on an executor thread, fails with ContextNotActiveException. Why, and what are your options?
-
Mid to senior
Your JUnit tests start Weld SE and pass. On Tomcat, one @ApplicationScoped bean fails with WELD-001435, "not proxyable because it has no no-args constructor", and another bean's constructor logs twice. Explain both and fix them.
Key takeaways
- A bean is a concrete class the container can construct; with the default
annotateddiscovery it also needs a scope or another bean-defining annotation, and since CDI 4.0 an emptybeans.xmlmeansannotated. - Prefer constructor injection; use field injection where something else creates the object, such as a servlet.
@ApplicationScopedand@RequestScopedinject client proxies;@Dependentinjects a new object per injection point.- A
@RequestScopedbean needs an active request context: each HTTP request has one, and@ActivateRequestContextstarts one for a job. - Final classes, records, sealed types and final methods cannot be proxied, so they cannot be normal-scoped beans.
- Weld SE relaxes the constructor rules, while Weld on Tomcat and on servers does not: test with the server’s rules.
- A Java SE test can start a container with
disableDiscovery()andaddBeanClasses(...)and nothing else.
References
- Jakarta Contexts and Dependency Injection 4.1 (Eclipse Foundation)
- Jakarta Contexts and Dependency Injection 4.1 specification (Eclipse Foundation)
- Jakarta Contexts and Dependency Injection 4.0: changes (Eclipse Foundation)
- Jakarta Contexts and Dependency Injection 5.0 (Eclipse Foundation)
- Jakarta EE 11 release: platform and profiles (Eclipse Foundation)
- Jakarta Dependency Injection 2.0 (Eclipse Foundation)
- Weld 6.0.4.Final reference: application servers and environments (Weld project)
- Weld 6.0.4.Final reference: configuration (Weld project)
- Apache Tomcat 11 documentation (The Apache Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress