Dependency Injection and CDI
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Struts 7.4.0 (released 2026-10-02) on Apache Tomcat 11.0.26 and Java 21; the
facts were checked on 2026-10-08 and 2026-10-10 against the struts2-core and struts2-cdi-plugin 7.4.0 jars
(struts-beans.xml, struts-default.xml, default.properties, javap), the plugin source tagged STRUTS_7_4_0, and the
Core Developers Guide, plugin and Weld pages named in the references. Every example was built into a scratch WAR from
the bookshelf-struts base and requested on Tomcat 11; the CDI examples add struts2-cdi-plugin 7.4.0 and
weld-servlet-shaded 6.0.4.Final (the Weld version that the Struts build itself tests against), and the persistence
example adds Hibernate ORM 7.4.12.Final and H2 2.5.252. Struts has two things that are called dependency injection and
a third that is optional. The first is the small container inside the framework that builds the framework’s own objects
and the objects you register with it: actions, interceptors, results, validators and converters. The second is a
replacement of the object factory that hands that job to Spring or to a CDI container. This page explains the internal
container, shows how to extend the framework through it, and then wires the Bookshelf catalogue with CDI. The Spring
integration is described on its own page, Spring Integration; the pages on interceptors,
configuration and
actions describe the objects that get injected.
Three Ways to Get an Object into an Action
Every object that a request needs is created by the ObjectFactory (org.apache.struts2.ObjectFactory): the action,
each interceptor, the result and the validators of the mapping. Which implementation of the factory runs is a constant,
struts.objectFactory, and that choice decides who injects what.
| Mechanism | How it is switched on | Annotation on the injection point | What it injects |
|---|---|---|---|
Struts internal container |
Always on; the default object factory is the bean named |
|
Framework objects, constants (as strings) and anything registered with a |
Spring |
|
|
Spring beans; the internal container still injects framework objects |
CDI |
|
|
CDI beans (scoped, qualified, produced); the internal container still injects framework objects |
Source: Apache Struts — ObjectFactory
Only one object factory is active at a time. A plugin that wants to be the factory sets struts.objectFactory in its
struts-plugin.xml; two such plugins on the class path are a conflict that the official CDI page describes as "undetermined",
so a project chooses one. The next diagram follows one action from the dispatcher to the injected instance. The branch
on the right is what the CDI plugin changes: it asks the CDI container to produce, inject and post-construct the object,
and then, when the caller asks for it, runs the internal container’s injection as well, so both annotations work on the
same class (the experiment further down confirms the second half).
The Internal Container
The Core Developers Guide says that the framework "uses its own dependency injection container that is very similar to
Google Guice", and that both were originally written by Bob Lee. In 7.4.0 the container lives in the package
org.apache.struts2.inject of struts2-core (it was com.opensymphony.xwork2.inject before Struts 7). The members that
an application meets are few.
| Type | Role |
|---|---|
|
The annotation, with |
|
The built container: |
|
The class that configuration providers use to register factories, aliases and constants ( |
|
The enum |
|
The creation callbacks that |
An application almost never touches ContainerBuilder. It describes the container in XML with the elements bean and
constant and lets the configuration provider build it; the framework’s own objects are described the same way in
struts-beans.xml, which struts-default.xml includes (113 bean elements in 7.4.0, among them the object
factory, the action mapper, the value stack factory, the OGNL machinery and the type converters). The rest of this page
uses one small domain so that the effect of each attribute is visible.
The Bookshelf Scenario Used on This Page
The scratch WAR has a service interface for the catalogue and two implementations, one action per example and a view that prints the injected values. The interface and the implementations are plain classes with no Struts import, which is the point of dependency injection.
public interface CatalogService {
List<String> titles();
}
public class InMemoryCatalog implements CatalogService {
public List<String> titles() { return List.of("Dune", "Neuromancer", "Hyperion"); }
}
public class ArchiveCatalog implements CatalogService {
public List<String> titles() { return List.of("Solaris"); }
}
The bean Element
The bean element of struts.xml registers a class with the container. The Bean Configuration page lists six
attributes, and the DTD of the 6.0 line (the newest that exists) accepts the same six.
| Attribute | Required | Meaning (verified against XmlDocConfigurationProvider and the experiments below) |
|---|---|---|
|
yes |
The class to create, or the class whose static methods receive values |
|
no |
The primary interface or class the object is registered under; the injection point is matched by type and name |
|
no |
The unique name among beans of the same |
|
no |
|
|
no |
When |
|
no |
When |
The next struts.xml registers the two catalogues under two names, a bean without a type, a static bean and two
constants. The element order is free inside the first group of the DTD: (package|include|bean|constant)* first, then at
most one bean-selection, then at most one unknown-handler-stack.
<struts>
<constant name="bookshelf.pageSize" value="5"/>
<constant name="bookshelf.libraryName" value="Bookshelf"/>
<bean type="org.example.bookshelf.CatalogService" name="default"
class="org.example.bookshelf.InMemoryCatalog" scope="singleton"/>
<bean type="org.example.bookshelf.CatalogService" name="archive"
class="org.example.bookshelf.ArchiveCatalog"/>
<bean class="org.example.bookshelf.RequestStamp"/>
<bean class="org.example.bookshelf.LibraryDefaults" static="true"/>
<package name="default" extends="struts-default" namespace="/">
<action name="injected" class="org.example.bookshelf.InjectedBookAction">
<result>/WEB-INF/views/injected.jsp</result>
</action>
</package>
</struts>
Source: Apache Struts — struts-6.0.dtd
Injecting with the Struts @Inject
An action is built by the object factory and then injected, so a field, a setter or a constructor of the action can ask the
container for an object. The class below uses every form that the container supports: a field that gets the bean named
default, a field that names archive, a bean without a type, the container itself, a constant through a setter, and a
constant that may be absent.
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
public class InjectedBookAction extends ActionSupport {
@Inject
private CatalogService catalog; // the bean named "default"
@Inject("archive")
private CatalogService archive; // the bean named "archive"
@Inject
private RequestStamp stamp; // a bean with no type: registered under its class
@Inject
private Container container; // the container itself
private int pageSize;
private String optionalValue = "not set";
@Inject("bookshelf.pageSize")
public void setPageSize(String pageSize) { // constants are strings: String, not int
this.pageSize = Integer.parseInt(pageSize);
}
@Inject(value = "bookshelf.missing", required = false)
public void setOptionalValue(String value) { // not called: no such constant
this.optionalValue = value;
}
public List<String> getTitles() { return catalog.titles(); }
public List<String> getArchived() { return archive.titles(); }
public Set<String> getCatalogNames() { return container.getInstanceNames(CatalogService.class); }
// getters for stampNumber, pageSize, optionalValue and libraryName omitted
}
The static bean gets its value in a static method. This is how plugins give a helper class the framework settings: the
SiteMesh plugin shown on the Plugins Architecture page registers two classes with static="true" and optional="true".
public class LibraryDefaults {
private static String libraryName;
@Inject("bookshelf.libraryName")
public static void setLibraryName(String name) { libraryName = name; }
public static String libraryName() { return libraryName; }
}
The view prints the values. This is the real output of GET /injected on Tomcat 11 (served by the
scratch WAR), with the markup removed:
Bookshelf
titles: Dune; Neuromancer; Hyperion;
archive: Solaris;
stamp: 6
pageSize: 5
optional: not set
names: default archive
Four facts from this run are worth keeping. The constant arrives as a string: a setter declared as
setPageSize(int) fails when the action is created with MissingDependencyException: No mapping found for dependency
followed by [type=int, name=...], because the container registers a constant under the type String and does not
convert it. The required = false setter was simply not called. A bean registered without a name is found under the name
default, and getInstanceNames reports both catalogue names. The stamp stayed the same on every request, which the
scope section explains.
Scopes
A bean that is injected into an action that lives for one request still has its own life, set by the scope attribute.
The Bean Configuration page says the scope "must be either default, singleton, request, session, thread". The class
Scope and a probe action per scope in the scratch WAR show what 7.4.0 really does: each probe injects a
RequestStamp (a class that numbers its instances) registered under a scope-specific name and prints the number.
scope |
Behaviour | Observed (the number of the instance, three requests each) |
|---|---|---|
omitted or |
One instance for the whole container, the same as |
|
|
One instance per container |
|
|
A new instance for each injection (accepted by |
|
|
One instance per thread |
|
|
Need a scope strategy that the application installs with |
HTTP 500: |
Nothing in struts2-core calls setScopeStrategy (a search of the jar finds no caller), so request, session and
wizard are present in the enum and in the documentation but unusable in a normal web application. The practical rule is
that a bean in the Struts container is a singleton, so it must not keep per-user state; per-request and per-session state
belongs to the action, to the servlet session, or to a CDI scope when the CDI plugin is installed (later on this page).
What Gets Injected
The object factory calls the container for every object it builds, so injection is not limited to actions. The Struts
@Inject works in interceptors, results, validators and converters built by the factory, and in the framework’s own
objects (that is how ObjectFactory receives the container, the factories and the class loader). The next interceptor of
the Bookshelf reads the archive catalogue that the container injects into it, and the view prints what it stored.
public class CatalogInterceptor extends AbstractInterceptor {
private CatalogService catalog;
@Inject("archive")
public void setCatalog(CatalogService catalog) {
this.catalog = catalog;
}
@Override
public String intercept(ActionInvocation invocation) throws Exception {
invocation.getInvocationContext().getServletRequest()
.setAttribute("archiveSize", catalog.titles().size());
return invocation.invoke();
}
}
The mapping puts the interceptor in front of the default stack (the interceptors page explains stacks); a request to
/books then shows archive=1, the size of the archive catalogue, which the view reads with #request.archiveSize.
<interceptors>
<interceptor name="catalog" class="org.example.bookshelf.CatalogInterceptor"/>
<interceptor-stack name="catalogStack">
<interceptor-ref name="catalog"/>
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
</interceptors>
<action name="books" class="org.example.bookshelf.BookAction">
<interceptor-ref name="catalogStack"/>
<result>/WEB-INF/views/books.jsp</result>
</action>
Interceptors are singletons that serve every request, so an injected collaborator must be thread-safe; the writing of interceptors is covered on the custom interceptors page.
Conflicts and Missing Classes
Two beans of one type and name cannot both be registered. The second one stops the start-up with a message that names both files and lines; the way out is a different name, selected by a constant (the extension-point section below).
Bean type interface org.example.bookshelf.CatalogService with the name default has already been loaded by bean - file:.../WEB-INF/classes/struts.xml:10:76
A bean whose class does not exist stops the application too, unless optional="true" is set. With optional="true" the
application starts and the container simply lacks that bean: a <bean type="org.example.bookshelf.CatalogService" name="ghost" class="org.example.bookshelf.DoesNotExist" optional="true"/> was skipped (the
names line above still read default archive), and without the attribute the log has Unable to load bean:
type:org.example.bookshelf.CatalogService class:org.example.bookshelf.DoesNotExist with the cause
java.lang.ClassNotFoundException. This is the mechanism that lets one struts-plugin.xml mention classes of an
optional library.
Extension Points
The framework replaces its own parts through extension points. An extension point is a constant that names a bean: the
constant struts.date.formatter holds the name of the DateFormatter bean that the s:date tag uses, and
default.properties sets it to dateTimeFormatter. The Plugins Architecture page defines it as "a name which will be
used to locate other beans by the name and inject them into a given place. Extension point isn’t a bean, you cannot inject
it." Adding an implementation is two steps: register a bean of the extension type with a new name, and set the constant to
that name.
| Constant | Type (package org.apache.struts2) |
Beans shipped with 7.4.0 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The official table still prints the older package com.opensymphony.xwork2 for most of these types and calls one constant
struts.conversion.properties.processor where the code constant is struts.converter.properties.processor; use the
table above (taken from the 7.4.0 jar) when writing code. The next example replaces the date formatter with the Bookshelf
one. The bean has a new name and the constant selects it; s:date then prints through it.
public class ShelfDateFormatter implements DateFormatter {
@Override
public String format(TemporalAccessor temporal, String format) {
return "shelf:" + DateTimeFormatter.ISO_LOCAL_DATE.format(temporal);
}
}
<bean type="org.apache.struts2.components.date.DateFormatter" name="shelfDates"
class="org.example.bookshelf.ShelfDateFormatter" scope="singleton"/>
<constant name="struts.date.formatter" value="shelfDates"/>
With a LocalDate property published (1965-08-01), <s:date name="published"/> printed shelf:1965-08-01.
Defining an Extension Point of Your Own
A plugin, or an application, can add an extension point of its own. The pattern is the one the core uses: an interface,
a constant name, and a bean-selection provider that ties the two with alias. The Bookshelf defines a LabelFormatter
with a plain and an upper-case implementation and lets the constant bookshelf.labelFormatter choose.
public class LabelBeanSelectionProvider extends AbstractBeanSelectionProvider {
public static final String LABEL_FORMATTER = "bookshelf.labelFormatter";
@Override
public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
alias(LabelFormatter.class, LABEL_FORMATTER, builder, props, Scope.SINGLETON);
}
}
<bean type="org.example.bookshelf.LabelFormatter" name="plain" class="org.example.bookshelf.PlainLabels"/>
<bean type="org.example.bookshelf.LabelFormatter" name="shout" class="org.example.bookshelf.ShoutLabels"/>
<constant name="bookshelf.labelFormatter" value="shout"/>
<package name="default" extends="struts-default">
<!-- actions -->
</package>
<bean-selection name="labelBeans" class="org.example.bookshelf.LabelBeanSelectionProvider"/>
An action with @Inject public void setLabels(LabelFormatter labels) then printed DUNE for labels.label("Dune"). Three
details were learned from the failures on the way. First, the bean-selection element must come after the packages, beans
and constants (the DTD content model is (package|include|bean|constant)*, bean-selection?, unknown-handler-stack?);
put before a package it stops the start-up with a SAXParseException. Second, the DTD allows one bean-selection
per file. Third, alias maps the constant’s value to a bean name, so with the constant missing nothing is registered
under the name default and the injection fails with No mapping found for dependency [type=...LabelFormatter, name='default']; a plugin that wants a default sets the constant in its own struts-plugin.xml. A plugin normally puts
the bean-selection in struts-deferred.xml so that it loads after the application’s struts.xml.
Plugins, struts-plugin.xml and the Load Order
A plugin is a jar with a struts-plugin.xml (and optionally a struts-deferred.xml) that follows the format of
struts.xml, so it can add beans, constants and packages. The Plugins Architecture page gives the order in which the files
are loaded: struts-default.xml from the core jar, every struts-plugin.xml on the class path, the application’s
struts.xml, and finally every struts-deferred.xml. A later file overrides a constant of an earlier one, which is why a
plugin can set a default and the application can change it. The Bookshelf checks this with a plugin jar placed in
WEB-INF/lib; the plugin sets two constants and registers a catalogue bean, and the deferred file sets a third.
<!-- bookshelf-plugin.jar!/struts-plugin.xml -->
<struts>
<constant name="bookshelf.libraryName" value="Set by struts-plugin.xml"/>
<constant name="bookshelf.pluginName" value="Set by struts-plugin.xml"/>
<bean type="org.example.bookshelf.CatalogService" name="plugin"
class="org.example.plugin.PluginCatalog"/>
</struts>
<!-- bookshelf-plugin.jar!/struts-deferred.xml -->
<struts>
<constant name="bookshelf.deferredName" value="Set by struts-deferred.xml"/>
</struts>
The application’s struts.xml sets bookshelf.libraryName to Bookshelf and bookshelf.deferredName to Set by
struts.xml. An action that injects the three constants and the plugin’s catalogue printed:
titles=Contributed by the plugin jar
libraryName=Bookshelf
pluginName=Set by struts-plugin.xml
deferredName=Set by struts-deferred.xml
So struts.xml beat the plugin (libraryName), the plugin value stayed where the application did not set one
(pluginName), and the deferred file beat struts.xml (deferredName). Writing a complete plugin (packages, results,
interceptors, the plugin registry) is covered on the page about the other plugins in this part; the plugins that
matter for layout are on the Tiles and page layouts page.
The ObjectFactory and Its Dedicated Factories
The object factory is the one extension point whose replacement changes who injects. A custom factory extends
ObjectFactory and is registered either by class name (struts.objectFactory=foo.bar.MyFactory, as the page says) or, as
in the experiment below, as a bean and selected by name. The factory’s buildBean is the method that every object goes
through, so overriding it shows everything the framework builds. The Bookshelf factory records the simple class names.
public class AuditObjectFactory extends ObjectFactory {
public static final List<String> BUILT = new CopyOnWriteArrayList<>();
@Override
public Object buildBean(String className, Map<String, Object> extraContext, boolean injectInternal)
throws Exception {
BUILT.add(className.substring(className.lastIndexOf('.') + 1));
return super.buildBean(className, extraContext, injectInternal);
}
}
<bean type="org.apache.struts2.ObjectFactory" name="audit" class="org.example.bookshelf.AuditObjectFactory"/>
<constant name="struts.objectFactory" value="audit"/>
After one request to /books, /injected and /audit the list held 87 entries; the beginning and the end read:
NoOpInterceptor
ExceptionMappingInterceptor
ServletConfigInterceptor
HttpMethodInterceptor
PrepareInterceptor
...
RequiredFieldValidator
RequiredStringValidator
IntRangeFieldValidator
...
BookAction
ServletDispatcherResult
InjectedBookAction
ServletDispatcherResult
AuditAction
ServletDispatcherResult
The interceptors and validators were built while the configuration was loaded and on first use, and the
actions and results were built per request: an action and its result are created for every request, which is why an
action field is never shared between users. The factory that builds only actions is smaller. The page "ObjectFactory"
explains that the original factory was split into dedicated factories, each with its own constant. The Bookshelf
ActionFactory records the name and the class of each action and leaves the building to the default factory.
public class AuditActionFactory extends DefaultActionFactory {
public static final List<String> BUILT = new CopyOnWriteArrayList<>();
@Override
public Object buildAction(String actionName, String namespace, ActionConfig config,
Map<String, Object> extraContext) throws Exception {
BUILT.add(namespace + actionName + " -> " + config.getClassName());
return super.buildAction(actionName, namespace, config, extraContext);
}
}
<bean type="org.apache.struts2.factory.ActionFactory" name="audit" class="org.example.bookshelf.AuditActionFactory"/>
<constant name="struts.objectFactory.actionFactory" value="audit"/>
After two requests the list was /books -> org.example.bookshelf.BookAction and /audit -> org.example.bookshelf.AuditAction.
The official page lists dedicated factories for results, actions, interceptors, validators, converters and (since 2.3.18)
unknown handlers; the 7.4.0 constants are struts.objectFactory.actionFactory, .resultFactory,
.interceptorFactory, .validatorFactory, .converterFactory and .unknownHandlerFactory.
The CDI Plugin
CDI (Contexts and Dependency Injection) is the Jakarta EE standard for dependency injection, and it builds on the
jakarta.inject annotations. The plugin struts2-cdi-plugin makes Struts build its objects through a CDI container, so an
action can inject scoped, qualified and produced beans with @Inject. The 7.4.0 jar is tiny: CdiObjectFactory,
CdiProxyService and a struts-plugin.xml that registers both and makes them the defaults.
<!-- struts-plugin.xml of struts2-cdi-plugin 7.4.0 -->
<struts>
<bean type="org.apache.struts2.ObjectFactory" name="cdi" class="org.apache.struts2.cdi.CdiObjectFactory" />
<constant name="struts.objectFactory" value="cdi" />
<bean type="org.apache.struts2.util.ProxyService" name="cdi" class="org.apache.struts2.cdi.CdiProxyService" />
<constant name="struts.proxyService" value="cdi" />
</struts>
The plugin does not bring a CDI container. A Jakarta EE application server (GlassFish, WildFly, TomEE and similar) already has one;
on Tomcat or Jetty the application ships one, and Weld Servlet is the reference choice. The plugin’s own POM declares the
Jakarta CDI API and weld-api as provided; it compiles against Weld (the Struts build uses 6.0.4.Final), so Weld is the
implementation that this page uses. The CDI page of the official site describes a javax setup (Maven coordinates
javax.enterprise:cdi-api:1.0-SP1 and Weld 1.0.1) that does not apply to a Jakarta EE 10 or 11 container; the table
sets the old and the 7.4.0 names side by side.
| The official page says | Struts 7.4.0 |
|---|---|
|
|
|
|
|
|
|
|
DTD |
|
Setup on Tomcat 11 with Weld Servlet
Four things make the combination work, and each one failed in turn when it was left out. The Maven dependencies come first; the Struts BOM manages the plugin’s version.
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-cdi-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-shaded</artifactId>
<version>6.0.4.Final</version>
</dependency>
Second, Tomcat has no BeanManager in JNDI by itself; the META-INF/context.xml of the web application binds one with
Weld’s object factory, and web.xml declares the reference and registers Weld’s listener, which must be present so that
Weld is started before the Struts filter initialises.
<!-- META-INF/context.xml -->
<Context>
<Resource name="BeanManager" auth="Container"
type="jakarta.enterprise.inject.spi.BeanManager"
factory="org.jboss.weld.resources.ManagerObjectFactory"/>
</Context>
<!-- WEB-INF/web.xml (before the Struts filter) -->
<listener>
<listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
</listener>
<resource-env-ref>
<resource-env-ref-name>BeanManager</resource-env-ref-name>
<resource-env-ref-type>jakarta.enterprise.inject.spi.BeanManager</resource-env-ref-type>
</resource-env-ref>
Source: Weld Reference — Tomcat
Third, WEB-INF/beans.xml must exist; an empty beans element with the discovery mode annotated is enough.
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
version="4.0" bean-discovery-mode="annotated">
</beans>
Without beans.xml the Weld listener logs WELD-ENV-000028: Weld initialization skipped - no bean archive found, and the
Struts plugin then fails (see the pitfalls table), which confirms the official page’s sentence that "it is an inevitable
requirement to provide at least an empty beans.xml in WEB-INF" for a servlet container. On an application server that
supplies CDI, none of the three Tomcat steps is needed and the plugin finds the BeanManager at java:comp/BeanManager;
the application server case was not run for this page, and the official page describes it.
How the Plugin Finds the BeanManager
CdiObjectFactory looks the BeanManager up in JNDI, trying three names in order. With Struts' log level raised to debug
for the package org.apache.struts2.cdi the start-up of the working Bookshelf printed:
INFO CdiObjectFactory - Initializing Struts2 CDI integration...
INFO CdiObjectFactory - [lookup]: Checking for BeanManager under JNDI key java:comp/BeanManager
DEBUG CdiObjectFactory - [lookup]: BeanManager lookup failed for JNDI key java:comp/BeanManager
INFO CdiObjectFactory - [lookup]: Checking for BeanManager under JNDI key java:app/BeanManager
DEBUG CdiObjectFactory - [lookup]: BeanManager lookup failed for JNDI key java:app/BeanManager
INFO CdiObjectFactory - [lookup]: Checking for BeanManager under JNDI key java:comp/env/BeanManager
INFO CdiObjectFactory - Struts2 CDI integration initialized.
| JNDI name | Where it applies |
|---|---|
|
The name in the CDI specification; Jakarta EE application servers |
|
The name in the Weld documentation |
|
Servlet containers such as Tomcat and Jetty; this is the one that the |
The official page adds a constant, struts.objectFactory.cdi.jndiKey, for a container that uses another name. On 7.4.0
that constant has no effect. The experiment bound the BeanManager only at java:comp/env/bookshelf/BeanManager, set the
constant to that name, and the log showed the three default lookups and then Struts2 CDI integration could not be
initialized. The reason is in the source: the lookup runs in the constructor of CdiObjectFactory, while the key arrives
afterwards through the @Inject setter setJndiKey, so the field is still empty when the lookup runs. On a server whose
BeanManager has another name, bind the manager under one of the three names, or subclass CdiObjectFactory and override
findBeanManager(), which is protected for that purpose.
Using CDI Beans in Actions
With the plugin in place the Bookshelf catalogue becomes CDI beans. The service is application scoped and the reader’s
list is session scoped; both are plain annotated classes (only the discovery mode annotated and a bean-defining
annotation are needed).
@ApplicationScoped
public class ShelfService {
private final AtomicInteger calls = new AtomicInteger();
public List<String> titles() {
calls.incrementAndGet();
return List.of("Dune", "Neuromancer", "Hyperion");
}
public int getCalls() { return calls.get(); }
}
@SessionScoped
public class ReaderSession implements Serializable {
private final List<String> reading = new ArrayList<>();
public void add(String title) { reading.add(title); }
public List<String> getReading() { return reading; }
}
The action uses the Jakarta annotation. Everything else is ordinary Struts: an action with a method, a request
parameter that needs @StrutsParameter, and a view.
import jakarta.inject.Inject;
public class CdiBookAction extends ActionSupport {
@Inject
private ShelfService shelf;
@Inject
private ReaderSession reader;
private String title;
@Override
public String execute() { return SUCCESS; }
public String borrow() {
reader.add(title);
return SUCCESS;
}
@StrutsParameter
public void setTitle(String title) { this.title = title; }
public List<String> getTitles() { return shelf.titles(); }
public List<String> getReading() { return reader.getReading(); }
public int getCalls() { return shelf.getCalls(); }
}
Two readers, one cookie each. The first GET /shelf and two POST /borrow requests with the same cookie jar, then a
GET /shelf without a cookie, printed:
GET shelf -> titles=Dune, Neuromancer, Hyperion, reading= calls=1
POST borrow title=Dune -> titles=Dune, Neuromancer, Hyperion, reading=Dune, calls=2
POST borrow title=Solaris -> titles=Dune, Neuromancer, Hyperion, reading=Dune, Solaris, calls=3
GET shelf (new session) -> titles=Dune, Neuromancer, Hyperion, reading= calls=4
The call counter climbs across both readers (one application-scoped instance), while each session has its own list. The JSESSIONID cookie value is not shown.
Injection into Interceptors, and the Request Scope
The CDI object factory builds interceptors too, so an interceptor can inject CDI beans. A request-scoped bean inside an
interceptor is the usual way to carry a per-request value between the interceptor and the action without a servlet
attribute. The Bookshelf LoanCounter is @RequestScoped; the interceptor increments it and stores the number in the
request.
@RequestScoped
public class LoanCounter {
private int loans;
public int next() { return ++loans; }
}
public class LoanInterceptor extends AbstractInterceptor {
@Inject
private LoanCounter counter;
@Override
public String intercept(ActionInvocation invocation) throws Exception {
int n = counter.next();
invocation.getInvocationContext().getServletRequest().setAttribute("loanNumber", n);
return invocation.invoke();
}
}
Mapped in front of defaultStack for the borrow action, two borrow requests both printed loan=1: the interceptor is a
singleton, but the injected counter is a client proxy for the counter of the current request, so every request starts at
one.
The Two @Inject Annotations
The official page calls the choice of @Inject "the most common pitfall": the container inside Struts has its own
annotation, and the one that a CDI developer types is the Jakarta one. Both were run against the CDI plugin and against the
default factory.
| Annotation on the field | Object factory | Observed on Struts 7.4.0 |
|---|---|---|
|
|
The CDI bean is injected |
|
|
HTTP 500: |
|
|
No error and no injection: the field stays |
|
|
Still injected: the plugin runs the internal injection after the CDI injection |
The failure that stays silent is the third row: a Jakarta-annotated action on a Struts application without the plugin compiles, deploys and returns 200, and the property comes out empty. Check the imports, and check that the plugin jar and its CDI container are in the WAR.
Proxied Beans and OGNL (Struts 7.3.0 and later)
A normal-scoped CDI bean (@SessionScoped, @RequestScoped, @ApplicationScoped) is injected as a client proxy, a
generated subclass of the bean. OGNL checks every member it reaches against the allowlist and refuses proxies, so a
view expression that walks through a proxy is a security question. Since 7.3.0 the plugin ships CdiProxyService, which
teaches Struts to find the real class behind a Weld client proxy (WW-5604). The experiment exposes getReader() on the
action and prints reader.reading in the view; the log shows how the pieces interact.
| Configuration | Result |
|---|---|
Defaults ( |
Empty output; WARN |
|
Empty output; WARN |
The two above plus |
|
The same with |
Empty output; the allowlist check names the proxy class |
So on 7.4.0 the plugin’s proxy support is necessary but not sufficient: the allowlist names the real class (as for any
model class), and the proxy-access switch must be relaxed. The safe pattern avoids both: the action exposes a plain getter
that delegates (getReading() in the action above returned the list and printed reading=Dune, with no extra
constants), so the view never walks through the proxy. Relax struts.disallowProxyObjectAccess only when a view really needs
to read properties of a CDI bean, and allowlist that one class, never a whole package of beans.
JPA and the Open EntityManager in View
The Struts in Action books show a Spring plus JPA integration in which a servlet filter (Spring’s
OpenEntityManagerInViewFilter) keeps one EntityManager open for the whole request, so that lazy associations load while
the view renders. The Spring plugin page carries the Spring version of that pattern; with CDI the same effect comes from a
request-scoped producer. The producer creates the EntityManager when the first action injects it and closes it when
the request scope ends, which Weld Servlet does after the response is written, and that is after the JSP has been
rendered. The Bookshelf entity Book has a lazy @OneToMany list of Review objects.
@ApplicationScoped
public class Persistence {
@Produces
@ApplicationScoped
EntityManagerFactory factory() {
return jakarta.persistence.Persistence.createEntityManagerFactory("bookshelf");
}
void closeFactory(@Disposes EntityManagerFactory factory) { factory.close(); }
@Produces
@RequestScoped
EntityManager entityManager(EntityManagerFactory factory) { // one per request
return factory.createEntityManager();
}
void closeEntityManager(@Disposes EntityManager em) { em.close(); } // after the view
}
An action that injects the request-scoped manager returns before the lazy list is touched; the view loads it.
public class LazyAction extends ActionSupport {
@Inject
private EntityManager em; // request scoped: open until the response is written
private Book book;
@Override
public String execute() {
book = em.createQuery("select b from Book b where b.title = 'Dune'", Book.class).getSingleResult();
return SUCCESS; // reviews are still unloaded here
}
public Book getBook() { return book; }
}
The view iterates book.reviews (the model package is allowlisted with struts.allowlist.packageNames). GET /lazy
printed title=Dune, review=A classic and review=Long, but worth it. The contrasting action closes the manager inside
execute(), the way a method-scoped transaction would, and the same view failed with HTTP 500; the log held
org.hibernate.LazyInitializationException: Cannot lazily initialize collection of role 'org.example.bookshelf.Book.reviews' with key '1' (no session).
Source: Hibernate ORM 7.4 documentation
Source: Jakarta Persistence 3.2
The pattern has the usual cost: a database connection stays attached for the whole request, including the JSP rendering
and, on a slow client, the time spent writing the response. Prefer to load what the view needs in the service layer
(a fetch join or a DTO), keep the manager request-scoped only where lazy loading in the view is a deliberate choice, and
see the Hibernate pages of this site on
fetching and N+1 queries and
the persistence context for the options. Transactions are a
separate concern: the example reads only; a write needs a transaction that the application starts and commits (the
seeding method in the example does that with EntityManager.getTransaction()), or a CDI transaction extension.
Choosing Between the Internal Container, CDI and Spring
| Situation | Choice | Why |
|---|---|---|
Few collaborators, nothing outside Struts |
The internal container with |
No library; the beans are singletons and configured in |
Jakarta EE server, or an application already built on CDI |
The CDI plugin |
Scopes ( |
A Spring application (Spring Data, Spring Security, transactions) |
The Spring plugin |
Struts creates actions through the Spring context; see Spring Integration |
Both Spring and CDI wanted |
Do not combine the two factories |
Only one |
A move to Quarkus or Vaadin |
Their own CDI integrations |
The |
The CDI model itself (scopes, producers, qualifiers, events) is described for two other stacks of this site, and the
Struts annotations map directly:
CDI and the programming model in Quarkus explains ArC, the build-time
CDI implementation of Quarkus, and where it differs from a runtime container like Weld, and
Jakarta EE and CDI in Vaadin shows a Jakarta EE server with Vaadin views as
CDI beans. A Struts application that moves to one of them replaces its actions by routes or views and keeps the
@Inject services unchanged.
Pitfalls
| Symptom or error | Cause | Fix |
|---|---|---|
HTTP 500 |
The container registers constants as strings, so an |
Take a |
A field annotated with |
No CDI or Spring plugin: the default object factory ignores the Jakarta annotation |
Add the plugin and its container, or use |
HTTP 500 |
The action imports the Struts |
Import |
|
A bean with |
Use |
The application does not start: |
Two |
Give the second a name and select it with the extension-point constant |
The application does not start with |
A |
Fix the class, or add |
|
A |
Put the single |
An extension point injection fails with |
|
Set the constant, in the plugin’s |
The application does not start, |
No BeanManager in JNDI under the three names (the Tomcat |
Add the |
Log |
|
Add a |
|
The lookup runs in the constructor, before the setter is injected |
Bind the manager under one of the three default names |
A view expression over a |
Weld client proxy plus |
Expose a plain getter on the action, or relax the proxy switch and allowlist the real class |
|
The |
Load what the view needs in the action, or make the manager request-scoped and understand the cost |
A singleton bean keeps data of one user |
Beans in the Struts container are singletons by default |
Keep per-user state in the session, in the action, or in a CDI |
The application behaves differently after adding a second plugin |
Two plugins both set |
Keep one object factory plugin; select it explicitly with the constant in |
What Changed Recently
| Release | Change |
|---|---|
Struts 2.3.16 and 2.3.18 |
The dedicated factories (action, result, interceptor, validator, converter) are split out of |
Struts 6.0 |
|
Struts 6.4 |
|
Struts 7.0 |
Package |
Struts 7.3.0 |
The CDI plugin registers |
Struts 7.4.0 |
Verified here: Java 21 and Tomcat 11.0.26 run |
Where the Books Are Outdated
Struts 2 in Action (Manning, 2008) deals with the Spring and JPA integration in chapter 9 and with the plugin model and the
internal container (bean, constant, @Inject and the extension points) in chapter 12. The ideas stand; the details have
moved, as the table lists.
| The book’s era | Today (Struts 7.4) |
|---|---|
|
|
The Spring plugin as the one injection route, |
Still present in the Spring plugin; CDI is a second route ( |
Spring 2 XML and |
Spring Framework 7 with the plugin; with CDI a |
Request, session and thread scopes of a bean in the internal container |
Only |
|
A |
SiteMesh, Tiles and JFreeChart as plugins of the same era |
The SiteMesh plugin is not published at 7.4.0 (the last release is 6.12.0), while the Tiles and JFreeChart plugins are; the other-plugins page of this part has the removed-plugins table |
A plugin is a jar with a |
Still true, and |
Dojo, |
Removed; see the Ajax page of this part |
Source: Apache Struts — Plugins
Notes on the Official Documentation
Reading the pages against the 7.4.0 jars showed inconsistencies that a reader will meet; they are listed so that a mismatch is not mistaken for an error in the example code.
-
The CDI Plugin page is written for
javaxand CDI 1.0: its Maven coordinates, imports (javax.inject,javax.enterprise,com.opensymphony.xwork2.ActionSupport) and the 2.3 DTD must be read asjakarta.*,org.apache.struts2.*and the 6.0 DTD. -
The constant
struts.objectFactory.cdi.jndiKeyis described as a working option; on 7.4.0 it is ignored, because the plugin looks up the BeanManager in its constructor before the key is injected. -
The "Proxied beans" section does not mention that
struts.disallowProxyObjectAccess(defaulttrue) and the allowlist still apply to a view expression through a client proxy. -
The Bean Configuration page lists the scopes
requestandsession; no scope strategy is installed, so they fail withScope strategy not set. It also showscom.opensymphony.xwork2.ObjectFactoryin its example. -
The Plugins Architecture extension-point table prints
com.opensymphony.xwork2types (noworg.apache.struts2), namesstruts.conversion.properties.processorwhere the constant isstruts.converter.properties.processor, and shows the DTDs 2.5 and 2.6 in its examples (the DTD of 7.4.0 is 6.0). -
The Dependency Injection page names the Plexus plugin; Plexus is deprecated since 6.0.0 and not published at 7.4.0.