Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 25 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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.

discovery/src/main/java/com/example/fees/LateFee.java Java
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);
    }
}
One program, three kinds of beans.xml Python · discovery_session.py
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.

XML Formatter Check that a beans.xml, pom.xml or server.xml is well-formed and indent it before you commit it.

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:

The beans of the fee example

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

Two dependent desks share one client proxy of the application-scoped FeeCalculator; NightlyReport reaches RequestTrace through a proxy.Two RegistrationDesk objects@Dependent: a new onefor every injection pointFeeCalculator client proxyone per bean, sharedFeeCalculator@ApplicationScoped: one object,made at the first callNightlyReport@ApplicationScopedRequestTrace client proxyRequestTrace@RequestScoped: one objectper active request contexteach desk's fieldevery call goes tothe current objectits fieldthe object of therequest now active

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:

fees/src/main/java/com/example/fees/Main.java Java
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());
        }
    }
}
Proxies, lazy creation and the request context, recorded Python · scopes_session.py
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:

proxies/src/main/java/com/example/fees/Proxies.java Java
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("");
    }
}
Types the proxy cannot extend, and Weld SE's relaxed construction Python · proxies_session.py
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:

fees/src/test/java/com/example/fees/RegistrationDeskTest.java Java
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));
    }
}
mvn test with JUnit 6 and Weld SE Python · tests_session.py
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:

The WAR: its servlet and its build

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:

Two requests to the WAR on Tomcat 11.0.26 Python · tomcat_session.py
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:

The starter and the sample checks

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");
    }
}
The sample checks against the starter, then against a solution Python · exercise_session.py
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.

  1. Question 1 of 5 A JAR has an empty META-INF/beans.xml and four classes, none of them abstract. Under CDI 4.1, which one becomes a bean?

    Choose one answer.

    Show the answer to question 1

    Answer: @Dependent public class Rounding, with no constructor declared

    Since CDI 4.0 an empty beans.xml means bean-discovery-mode="annotated", so a class needs a bean-defining annotation such as @Dependent. It must also have a constructor the container can call (one without parameters, or one annotated @Inject), and @Vetoed keeps a class out of CDI altogether.

  2. Question 2 of 5 An @ApplicationScoped job has an injected @RequestScoped field and calls a method on it from a plain Java SE thread, with no @ActivateRequestContext anywhere. What happens?

    Choose one answer.

    Show the answer to question 2

    Answer: The call throws ContextNotActiveException, because no request context is active on that thread

    Injecting a shorter-lived bean into a longer-lived one is allowed, because the field holds a client proxy. The proxy looks for the current request's object on every call and throws ContextNotActiveException (Weld's WELD-001303) when no request context is active, as line 4 of the recorded run shows.

  3. Question 3 of 5 Which of these types cannot be the type of an injected @ApplicationScoped bean?

    Choose every answer that is right.

    Show the answer to question 3

    Answer:

    • A record, such as record HostelRate(long perMonth)
    • A sealed interface that only one class may implement
    • A final class

    A client proxy is a subclass, so CDI 4.1 lists final classes (records are final), sealed types and classes with non-private final methods as unproxyable. A protected constructor without parameters is enough for the proxy, and a private method is never called through the proxy, so the last two are fine.

  4. Question 4 of 5 A WAR on Tomcat, with Weld's default settings there, has an @ApplicationScoped class whose constructor adds one to a static counter. Three requests each call one of its methods once. What does the counter show afterwards?

    Type a number.

    Show the answer to question 4

    Answer: 2 objects

    Outside Weld SE, Weld follows the specification and calls the constructor when it builds the client proxy, once for the bean, and again when it creates the real object at the first call. All three requests share that object, so the counter shows 2, as the recorded Tomcat run printed for FeeCalculator.

  5. Question 5 of 5 Which call starts a container for a test that should know only FeeCalculator and RegistrationDesk?

    Choose one answer.

    Show the answer to question 5

    Answer: SeContainerInitializer.newInstance().disableDiscovery().addBeanClasses(FeeCalculator.class, RegistrationDesk.class).initialize()

    disableDiscovery() switches off scanning of the class path, and addBeanClasses() puts the listed classes into the synthetic bean archive. Without disableDiscovery(), the container would also scan every bean archive it finds.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A bean is a concrete class the container can construct; with the default annotated discovery it also needs a scope or another bean-defining annotation, and since CDI 4.0 an empty beans.xml means annotated.
  • Prefer constructor injection; use field injection where something else creates the object, such as a servlet.
  • @ApplicationScoped and @RequestScoped inject client proxies; @Dependent injects a new object per injection point.
  • A @RequestScoped bean needs an active request context: each HTTP request has one, and @ActivateRequestContext starts 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() and addBeanClasses(...) and nothing else.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.