Spring Integration

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) with its struts2-spring-plugin 7.4.0, Spring Framework 7.0.9, Apache Tomcat 11.0.26 and Java 21, verified on 2026-10-08 and 2026-10-10. Every example was built as a WAR, deployed on Tomcat 11 and requested with curl; the plugin jar was also opened with javap to check class, interface and constant names. Struts creates its own actions, interceptors and results with a class called the ObjectFactory, and the Spring plugin replaces that factory with one that asks a Spring ApplicationContext first. The result is that the services an action needs (a catalogue, a repository, a mail sender) can be ordinary Spring beans with Spring’s scopes, proxies and annotations, while the action classes stay as they were. The running scenario is the Bookshelf: a BookService that lists titles, an action that prints them, a Spring-managed action, a Spring-managed interceptor and a session-scoped shelf cart. Related pages of this section are linked where they come up: Testing, Deployment and Production, Other Plugins and Dependency Injection and CDI.

What the Spring Plugin Does

Struts has its own small dependency injection container (the @Inject annotation and the <bean> element, described on the dependency injection page of this group). That container wires the framework’s own objects. Objects that the application owns (actions, interceptors, result types, validators, converters) are built by the ObjectFactory, and the plugin page states the mechanism in one sentence: the Spring plugin works by overriding the Struts ObjectFactory to enhance the creation of core framework objects. The class attribute of an action, interceptor or result in the Struts configuration is matched with the id of a Spring bean; if there is no such bean, the class is created as usual and then autowired by Spring.

The plugin jar contains fifteen classes and a struts-plugin.xml. The ones the application sees are these:

Type (package org.apache.struts2.spring) Role

StrutsSpringObjectFactory

The factory the plugin registers under the name spring; reads the struts.objectFactory.spring.* constants and finds the ApplicationContext that ContextLoaderListener put in the servlet context

SpringObjectFactory

The base class. buildBean(String className, Map extraContext, boolean injectInternal) returns the Spring bean named className if there is one, otherwise loads the class and autowires a new instance with the configured strategy. Implements ApplicationContextAware

SpringProxyableObjectFactory

A variant for apps that proxy beans; named here for completeness, not needed for the normal setup

interceptor.ActionAutowiringInterceptor

The interceptor named autowiring in the package spring-default; autowires an action after the fact when another object factory is in use

ClassReloadingXMLWebApplicationContext

A Spring web application context that can reload changed classes; development only

SpringConstants and config.entities.SpringConstantConfig

The names of the class reloading constants and the code that reads them

The plugin’s own struts-plugin.xml (inside the jar) does four things that matter: it registers the object factory under the name spring, it sets struts.objectFactory to spring (so adding the jar is enough), it sets struts.disallowProxyMemberAccess to true and struts.json.result.excludeProxyProperties to true (Spring proxies must not be reachable from OGNL or serialised by the JSON plugin), and it declares the spring-default package with the autowiring interceptor.

flowchart TD A["Struts needs an object: action class, interceptor, result"] --> B{"Is there a Spring bean whose id equals the class attribute?"} B -- yes --> C["Spring returns the bean, scope respected: prototype gives a new action per request"] B -- no --> D["Struts loads the class and creates it"] D --> E["Spring autowires the new object with the strategy of struts.objectFactory.spring.autoWire"] E --> F["Setters and annotated fields are filled; the object is ready"] C --> G["ApplicationContextAware objects receive the context"] F --> G

The Bookshelf Used on This Page

All examples are classes and files of one Maven WAR, built three times with small changes: g11spring (XML Spring configuration and the default autowire mode), g11springjc (Java configuration, autowire by type) and a few throw-away variants for the failure cases. Each was deployed under its own context path on Tomcat 11.0.26. The package is org.example.bookshelf; imports and package lines are left out of the listings. The service is a plain class with a call counter, so that sharing between requests is visible:

public interface BookService {
    List<String> titles();
    int lookups();
}

public class InMemoryBookService implements BookService {
    private final AtomicInteger lookups = new AtomicInteger();

    public List<String> titles() {
        lookups.incrementAndGet();
        return List.of("Dune", "Neuromancer");
    }

    public int lookups() {
        return lookups.get();
    }
}

The Getting Started tutorial motivates the same split: an action that does new EditServiceInMemory() hard-codes its dependency, cannot be tested with a stub and has to be edited everywhere the service is replaced; an action with only a field and a setter can be given any implementation at run time.

Installation: Jar, Listener and Application Context

Three things are needed, and the plugin page lists the same ones: the plugin jar, the Spring listener in web.xml and the bean definitions. With Maven the plugin is managed by the struts2-bom that every page of this section imports, and it pulls the Spring jars it needs as transitive dependencies (spring-beans, spring-core, spring-context, spring-web; spring-aop, spring-aspects, spring-context-support and commons-jci-fam are declared optional).

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>7.0.9</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.struts</groupId>
            <artifactId>struts2-bom</artifactId>
            <version>7.4.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.apache.struts</groupId>
        <artifactId>struts2-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.struts</groupId>
        <artifactId>struts2-spring-plugin</artifactId>
    </dependency>
</dependencies>

Importing the Spring BOM is what moves the Spring jars to a newer version than the one the plugin was built with. Struts 7.4.0 itself is built against Spring 6.2.19; the example WAR ran on Spring Framework 7.0.9 (spring-core-7.0.9.jar and friends in WEB-INF/lib) on Tomcat 11 without any other change, and the Getting Started page gives the same advice in other words: exclude the transitive Spring dependencies and declare the version you want. Spring 7 keeps a Java 17 baseline and raises the servlet baseline to Servlet 6.1 (Tomcat 11, Jetty 12.1) according to its 7.0 release notes, so Spring 7 with Struts 7 means Tomcat 11 or Jetty 12.1; on Tomcat 10.1 stay on Spring 6.2, which is the line the plugin is built against.

The listener goes in web.xml, before or after the Struts filter (the filter looks the context up when it starts, and listeners are initialised before filters, so the order of the elements does not matter):

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

By default ContextLoaderListener reads /WEB-INF/applicationContext.xml. The service is one bean:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bookService" class="org.example.bookshelf.InMemoryBookService"/>

</beans>

The context parameter contextConfigLocation changes where Spring looks. The plugin page shows a pattern that loads every applicationContext-*.xml from WEB-INF and from the class path:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext-*.xml,classpath*:applicationContext-*.xml</param-value>
</context-param>

The First Spring-Injected Action

The action is an ordinary Struts action with a field and a JavaBeans setter. There is no @Autowired and nothing in struts.xml mentions Spring:

public class BookAction extends ActionSupport {
    private BookService bookService;
    private String message;

    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }

    @Override
    public String execute() {
        message = "titles=" + bookService.titles()
                + " lookups=" + bookService.lookups()
                + " service=" + System.identityHashCode(bookService)
                + " action=" + System.identityHashCode(this);
        return SUCCESS;
    }

    public String getMessage() {
        return message;
    }
}
<action name="books" class="org.example.bookshelf.BookAction">
    <result>/WEB-INF/views/result.jsp</result>
</action>

Two requests to /books print (the numbers are identity hash codes and differ on every run):

curl http://localhost:18080/g11spring/books
curl http://localhost:18080/g11spring/books
titles=[Dune, Neuromancer] lookups=1 service=2065885581 action=1709014946
titles=[Dune, Neuromancer] lookups=2 service=2065885581 action=1922376303

The service number is the same in both lines and lookups grows: Spring’s default scope is singleton, so one InMemoryBookService serves every request. The action number differs: Struts still creates a new action instance per request (see the thread-per-request section of the actions page), and Spring only filled the bookService property. The setter name is what connected them: the default autowire mode is name, which matches the bean id bookService with the property bookService.

Autowire Modes

The constant struts.objectFactory.spring.autoWire selects how a Struts-created object is autowired. The plugin page and the 7.4.0 default.properties agree on the default, name.

Value Meaning on the plugin page

name

Match the name of the Spring bean with the name of the property. The default

type

Look for a bean of the same type as the property; there must be exactly one bean of that type

auto

Spring tries to detect the best method (the SpringObjectFactory log message calls it "autodetect")

constructor

Autowire the parameters of the constructor

no

Turn off externally defined autowiring; annotation-driven injection and the Spring *Aware interfaces still apply

The difference shows with a property whose name is not a bean id. The action below has a setCatalog(BookService) setter, but the only bean is bookService:

public class CatalogAction extends ActionSupport {
    private BookService catalog;

    public void setCatalog(BookService catalog) {
        this.catalog = catalog;
    }

    public String getMessage() {
        return "catalog=" + (catalog == null ? "null" : catalog.titles());
    }
}

With the default name mode, nothing matches and nothing fails:

curl http://localhost:18080/g11spring/catalog
catalog=null

One line in struts.xml (a <constant> element can also be a struts.properties entry) switches the application to autowiring by type:

<constant name="struts.objectFactory.spring.autoWire" value="type"/>

and the same action, in the g11springjc WAR, now finds the only BookService bean:

catalog=[Dune, Neuromancer]

The name mode is the most predictable for teams that keep property names and bean ids aligned; the type mode removes the naming convention but fails as soon as a second bean of the same type is added, which is why the plugin page says it "requires you to have only one bean of this type registered".

The other settings of the plugin:

Constant Default Meaning

struts.objectFactory.spring.autoWire

name

The strategy above

struts.objectFactory.spring.autoWire.alwaysRespect

true (since 7.2.0)

Apply the configured strategy consistently; the plugin page says the old false default caused problems such as broken redirect URLs when Spring String beans were involved. Set it to false to restore the pre-7.2 behaviour

struts.objectFactory.spring.useClassCache

true

Let Spring cache classes; the default.properties comment says to leave it true

struts.objectFactory.spring.enableAopSupport

false

Deprecated switch to an older bean construction logic for AOP; try it only if Spring beans and AOP misbehave

struts.objectFactory.spring.class.reloading.watchList, ...acceptClasses, ...reloadConfig

empty, empty, false

Class reloading (see below); the plugin page names these struts.class.reloading.*, which the jar does not read

Letting Spring Create the Action

The first example left the creation of the action to Struts and let Spring inject the properties. The plugin page calls the alternative "initializing actions from Spring": declare the action as a Spring bean and put the bean id in the class attribute of the mapping. This is useful for constructor injection, for AOP (transactions, security annotations) that needs a Spring proxy, and for anything else that depends on Spring creating the instance. The page’s recommendation is to prefer the declarative way (name or type autowiring, annotations) and keep the Spring XML for the cases that need it, so that configuration is not maintained twice.

public class ManagedBookAction extends ActionSupport {
    private final BookService bookService;
    private String message;

    public ManagedBookAction(BookService bookService) {
        this.bookService = bookService;
    }

    @Override
    public String execute() {
        message = "managed titles=" + bookService.titles() + " action=" + System.identityHashCode(this);
        return SUCCESS;
    }

    public String getMessage() {
        return message;
    }
}
<bean id="managedBookAction" class="org.example.bookshelf.ManagedBookAction" scope="prototype">
    <constructor-arg ref="bookService"/>
</bean>
<action name="managed" class="managedBookAction">
    <result>/WEB-INF/views/result.jsp</result>
</action>
curl http://localhost:18080/g11spring/managed
managed titles=[Dune, Neuromancer] action=277069676

The scope matters. A Struts action holds request data in its fields, so it must be created for every request. Spring’s default scope is singleton; the Getting Started page says it plainly: actions must be created on each request, they cannot be singletons, which is why the bean is declared scope="prototype". A singleton action would share its form fields between users, the same defect as a static field on the actions page. The plugin page adds that other scopes (session, application or a custom scope) are possible; that turns an action into a long-lived object and is almost never what a form action wants.

A second way to reach a Spring bean from a Struts mapping, listed in the 7.4.0 Version Notes, is a bean whose id is the fully qualified class name of the action. The mapping keeps class="org.example.bookshelf.CatalogAction", but the object factory finds a bean with that exact id and uses it. The failing catalog example above becomes:

<bean id="org.example.bookshelf.CatalogAction" class="org.example.bookshelf.CatalogAction" scope="prototype">
    <property name="catalog" ref="bookService"/>
</bean>
catalog=[Dune, Neuromancer]

(verified on the g11spring WAR with the default name mode, where the same action without the bean printed catalog=null).

Annotation-Driven Injection

Because a Struts-created action is autowired by Spring’s AutowireCapableBeanFactory, Spring’s annotation processing applies to it, so a field can be marked @Autowired and needs no setter. The g11springjc WAR contains

public class AnnotatedBookAction extends ActionSupport {
    @Autowired
    private BookService books;

    public String getMessage() {
        return "annotated titles=" + books.titles();
    }
}
<action name="annotated" class="org.example.bookshelf.AnnotatedBookAction">
    <result>/WEB-INF/views/result.jsp</result>
</action>
annotated titles=[Dune, Neuromancer]

The name of the field (books) differs from the bean id, and the lookup by type of @Autowired is what made it work; the autowire constant was type in that WAR. The no mode of the plugin page is the one that leaves the XML-style autowiring off and relies on annotations alone. Field injection hides the dependency from the constructor, which is the usual argument for the setter and constructor styles above in code that has to be unit-tested without Spring.

Spring Framework 7 Java Configuration

The XML file is optional. Spring’s web context can be told to read annotated classes with two context parameters, and Struts only sees the resulting ApplicationContext:

<context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>org.example.bookshelf.AppConfig</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
@Configuration
public class AppConfig {

    @Bean
    public BookService bookService() {
        return new InMemoryBookService();
    }

    @Bean
    @Scope("prototype")
    public ManagedBookAction managedBookAction(BookService bookService) {
        return new ManagedBookAction(bookService);
    }

    @Bean
    public AuditInterceptor auditInterceptor(BookService bookService) {
        return new AuditInterceptor(bookService);
    }
}

All five mappings of the g11springjc WAR (books, managed, audited, catalog, annotated) answered 200 with the expected output on this configuration, with Spring Framework 7.0.9 on Tomcat 11. The @Bean method name is the bean id, which is therefore what the class attribute of the Struts mapping uses (managedBookAction). A @ComponentScan with @Component and @Scope("prototype") on the action class works the same way. The WebApplicationInitializer route (registering the context from Java, with no web.xml listener) is also a Spring feature that ends in the same servlet context attribute; it is not covered by the Struts documentation and was not tested for this page.

Interceptors and Other Objects from Spring

Actions are not the only objects the class attribute names. The plugin page lists actions, interceptors and results among the objects that can be created by Spring. An interceptor bean can receive collaborators by constructor injection and be referenced from the stack by its bean id:

public class AuditInterceptor extends AbstractInterceptor {
    private final BookService bookService;

    public AuditInterceptor(BookService bookService) {
        this.bookService = bookService;
    }

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        System.out.println("AUDIT before " + invocation.getProxy().getActionName()
                + " lookups=" + bookService.lookups());
        try {
            return invocation.invoke();
        } finally {
            System.out.println("AUDIT after " + invocation.getProxy().getActionName()
                    + " lookups=" + bookService.lookups());
        }
    }
}
<bean id="auditInterceptor" class="org.example.bookshelf.AuditInterceptor">
    <constructor-arg ref="bookService"/>
</bean>
<interceptors>
    <interceptor name="audit" class="auditInterceptor"/>
    <interceptor-stack name="auditedStack">
        <interceptor-ref name="audit"/>
        <interceptor-ref name="defaultStack"/>
    </interceptor-stack>
</interceptors>

<action name="audited" class="org.example.bookshelf.BookAction">
    <interceptor-ref name="auditedStack"/>
    <result>/WEB-INF/views/result.jsp</result>
</action>
curl http://localhost:18080/g11spring/audited
titles=[Dune, Neuromancer] lookups=4 service=2065885581 action=439060776

and the servlet container’s log shows the two lines around the action (the lookups value was 3 before and 4 after, the shared singleton having served books twice and managed once earlier):

AUDIT before audited lookups=3
AUDIT after audited lookups=4

An interceptor is a singleton shared by all requests (see the interceptors page and the custom interceptors page), so a Spring-managed interceptor bean must stay a singleton too and must not hold request state; Spring only saves the new and the wiring code.

Scoped Beans: Session and Request

Spring’s other scopes (request, session, application) are available for the beans actions use. A cart that lives as long as the HTTP session is the Bookshelf example. A session-scoped bean cannot be injected into an action without help, because the action is built for one request while the bean belongs to the session: the injected value must be a scoped proxy, which resolves the real bean of the current session on each call.

<bean id="shelfCart" class="org.example.bookshelf.ShelfCart" scope="session">
    <aop:scoped-proxy/>
</bean>
public class ShelfCart {
    private final List<String> titles = new ArrayList<>();

    public int add(String title) {
        titles.add(title);
        return titles.size();
    }
}

public class CartAction extends ActionSupport {
    private ShelfCart shelfCart;
    private String message;

    public void setShelfCart(ShelfCart shelfCart) {
        this.shelfCart = shelfCart;
    }

    @Override
    public String execute() {
        message = "cart size=" + shelfCart.add("Dune") + " class=" + shelfCart.getClass().getSimpleName();
        return SUCCESS;
    }

    public String getMessage() {
        return message;
    }
}

The aop namespace comes from spring-aop, which the plugin declares optional but which was present in the WAR. The scope also needs a way to find the current request, which the Struts filter does not provide, so web.xml gets the second Spring listener:

<listener>
    <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

Three requests with one cookie jar and one with a fresh jar:

curl -c jar.txt -b jar.txt http://localhost:18080/g11spring/cart
curl -c jar.txt -b jar.txt http://localhost:18080/g11spring/cart
curl -c jar.txt -b jar.txt http://localhost:18080/g11spring/cart
curl -c other.txt -b other.txt http://localhost:18080/g11spring/cart
cart size=1 class=ShelfCart$$SpringCGLIB$$0
cart size=2 class=ShelfCart$$SpringCGLIB$$0
cart size=3 class=ShelfCart$$SpringCGLIB$$0
cart size=1 class=ShelfCart$$SpringCGLIB$$0

The injected object is a CGLIB proxy (ShelfCartSpringCGLIB0), each session has its own cart and the plugin’s struts.disallowProxyMemberAccess constant keeps OGNL away from the proxy internals. Without the RequestContextListener the same request answers 500 with Spring’s ScopeNotActiveException: Scope 'session' is not active for the current thread, followed by the hint that the code is running outside DispatcherServlet and needs RequestContextListener or RequestContextFilter. In an application that also stores the cart in the Struts session map, the Spring bean replaces that map; the sessions page of this section explains the Struts side.

The plugin page’s remark that Spring 2’s scope feature can scope an action to the session, the application or a custom scope is the same mechanism applied to the action itself.

Reaching the ApplicationContext

An action that implements Spring’s ApplicationContextAware receives the context after creation, because SpringObjectFactory injects it (the class has a private step for this, and implements the interface itself). This is a diagnostic and an escape hatch, not a style to copy: it ties the action to Spring.

public class ContextAction extends ActionSupport implements ApplicationContextAware {
    private ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        this.context = context;
    }

    public String getMessage() {
        return context.getClass().getSimpleName() + " " + String.join(",", context.getBeanDefinitionNames());
    }
}
XmlWebApplicationContext bookService,managedBookAction,auditInterceptor,scopedTarget.shelfCart,shelfCart

The list is exactly the beans of the XML file, plus Spring’s scopedTarget. bean that stands behind the cart proxy.

The Autowiring Interceptor

The plugin page says it provides two interceptors that autowire actions "if not using the Spring ObjectFactory"; in 7.4.0 the jar contains one, ActionAutowiringInterceptor, registered as autowiring in the package spring-default. It is for applications that deliberately run another object factory (the page’s example is a second IoC plugin) and still want Spring to wire the action. The application below keeps the ContextLoaderListener, sets struts.objectFactory back to the default and extends both packages:

<constant name="struts.objectFactory" value="struts"/>

<package name="default" extends="struts-default,spring-default" namespace="/">
    <action name="books" class="org.example.bookshelf.BookAction">
        <interceptor-ref name="autowiring"/>
        <interceptor-ref name="defaultStack"/>
        <result>/WEB-INF/views/result.jsp</result>
    </action>
    <action name="plain" class="org.example.bookshelf.BookAction">
        <result>/WEB-INF/views/result.jsp</result>
    </action>
</package>

/books printed titles=[Dune, Neuromancer] lookups=1 ...: the interceptor wired bookService into an action that Struts itself had created. /plain, with the same class and no interceptor, answered 500 with a NullPointerException in BookAction.execute, because nothing wired it. Its setAutowireStrategy(Integer) setter lets a mapping choose a strategy for that action. For normal applications the Spring object factory is simpler and covers every action at once.

Transactions, Security and AOP

The plugin page suggests using JDK 5 annotations for transaction and security requirements instead of explicit proxies in the Spring XML, and mentions that a bean fully managed by Spring is the way to apply "more complex AOP or Spring-enabled technologies" to an action. In the Bookshelf that means @Transactional on a service method (the service is a Spring bean, so the proxy is normal Spring) rather than on the action. Two details of the Struts side are worth knowing; both come from the 7.x Version Notes rather than from an experiment on this page. WW-5534 (fixed in 7.1.1) concerns actions with Spring’s @Transactional combined with ModelDriven, which is the case where the Struts model-driven machinery meets a proxied action; the model-driven page of this section explains the interface. WW-5717 (in 7.4.0) updates the Spring Framework dependency after an OWASP dependency-check finding in spring-core. The legacy switch struts.objectFactory.spring.enableAopSupport is deprecated and false by default.

Class Reloading

The plugin can reload changed classes without restarting the container. It is documented as experimental and never for production. The steps on the plugin page are: set struts.devMode to true, list the directories or jars to watch, add a context parameter that switches the Spring web context class, and put commons-jci-fam 1.0 on the class path.

<context-param>
    <param-name>contextClass</param-name>
    <param-value>org.apache.struts2.spring.ClassReloadingXMLWebApplicationContext</param-value>
</context-param>

The reloading class loader is also the source of the plugin page’s warning: instances of the same class loaded by two class loaders cannot be assigned to each other, so reloading everything produces ClassCastException`s; limiting the reloaded classes with a regular expression, so that only actions are handled, avoids it. One more point from the check of the 7.4.0 jar: the plugin page names the constants `struts.class.reloading.watchList, ...acceptClasses and ...reloadConfig, but SpringConstants, struts-plugin.xml and StrutsSpringObjectFactory of 7.4.0 use struts.objectFactory.spring.class.reloading.watchList, ...acceptClasses and ...reloadConfig. Class reloading was not run for this page (it needs the experimental JCI jar and a watched directory); use the names in the jar and check the log when trying it.

Spring Boot and Struts

There is no official Spring Boot support for Struts. The pages of the Struts documentation checked on 2026-10-08 (the plugins index, Getting Started, the Core Developer Guide and the security pages) contain no Spring Boot guide, plugin or starter, and the Struts examples repository has no Spring Boot module (it has a Quarkus one). The plugin works with a WebApplicationContext in the servlet context, which is what the classic ContextLoaderListener provides; an embedded Boot application is a different way of building that context. Two routes are commonly described in community answers: a Boot WAR that is deployed to an external Tomcat (SpringBootServletInitializer) with the Struts filter registered through a FilterRegistrationBean, and a fully classic WAR that does not use Boot. Neither route is documented by Struts, and the first was not tested on this page, so treat it as unsupported and test it yourself before depending on it.

Two further facts shape the decision. Spring Boot’s own reference says JSPs are not supported in an executable jar and recommends avoiding them when possible (the JSP Limitations section; see Spring MVC with JSP), and Struts views in 7.x are JSP, FreeMarker or Velocity files, so a Struts application on Boot is in the same position as a JSP one. A team that wants Boot’s packaging, configuration and ecosystem for new work is better served by Spring MVC (see Web UI frameworks on Spring Boot and, for Spring fundamentals, Spring Boot core concepts). For the services behind a Struts application the Spring plugin is the integration; for the front end, moving to Spring MVC is the modernisation path that a later page of this section describes.

The Official spring-struts Example

The Getting Started page points to spring-struts in the struts-examples repository. It is the Themes tutorial’s EditAction with its EditService dependency moved to Spring: an EditServiceInMemory bean named editService, a setter in EditAction, the ContextLoaderListener in web.xml and the alternative mapping in which Spring creates the action. It is the same two options as the books and managed mappings above, with other names. Read the tutorial for the full source; this page shows the Bookshelf versions instead.

Pitfalls

Table 1. Spring integration pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Struts 7.4.0, Spring 7.0.9 and Tomcat 11.0.26)
Symptom or error Cause Fix

The application does not start; the log says "FATAL ERROR STARTING UP STRUTS-SPRING INTEGRATION" and the Struts filter fails with Dispatcher initialization failed and a NullPointerException on appContext (observed, with a class attribute that names a Spring bean)

ContextLoaderListener is not in web.xml, so WebApplicationContextUtils returns no context

Add the listener (the log message prints the XML to add)

A property of the action is null and the action throws NullPointerException (observed)

The autowire mode is name and the property name matches no bean id, or the autowiring interceptor is not in a stack that uses another object factory

Rename the property, switch to type, add a bean named after the FQCN, or use @Autowired

NoUniqueBeanDefinitionException or "expected single matching bean"

Mode type and two beans of the same type

Keep one bean per type, mark one @Primary, or go back to name

Scope 'session' is not active for the current thread (observed, HTTP 500)

A request or session scoped bean is used and no RequestContextListener or RequestContextFilter is registered

Add the listener to web.xml

A session-scoped bean cannot be injected into the action

The bean has no scoped proxy

Declare <aop:scoped-proxy/> or @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)

Two users see each other’s form data

The action bean has Spring’s default singleton scope

scope="prototype" on every action bean

Attribute "singleton" must be declared for element type "bean" (observed)

The plugin page’s XML example uses the removed singleton attribute of the old DTD

Use scope="prototype" and the XSD header shown above

ClassCastException after enabling class reloading

Classes from two class loaders meet

Limit the reloading class loader to the action classes with the accept pattern

Reloading constants have no effect

The constants named on the plugin page are not the ones the jar reads

Use struts.objectFactory.spring.class.reloading.*

The Spring jars are an older version than expected

The plugin pulls the version it was built with (6.2.19 for 7.4.0) unless the application manages it

Import spring-framework-bom before struts2-bom, or exclude the transitive Spring jars

An OGNL expression or the JSON result touches Spring proxy internals

A proxied bean exposes proxy properties

The plugin already sets struts.disallowProxyMemberAccess and struts.json.result.excludeProxyProperties; do not turn them off

The service works on one request but changes between requests

A prototype or request scoped service bean is used where a singleton was meant

Make shared services singletons; prototypes only for per-request holders

What Changed Recently

Table 2. Changes in the Spring integration between the books' Struts 2.0 and Struts 7.4
Release Change

Struts 7.0

The Jakarta namespace and the org.apache.struts2 package names: the book’s com.opensymphony.xwork2.ObjectFactory is now org.apache.struts2.ObjectFactory (checked in the 7.4.0 jars)

Struts 7.1.1

WW-5534: actions with Spring’s @Transactional and ModelDriven fixed

Struts 7.2.0

struts.objectFactory.spring.autoWire.alwaysRespect defaults to true (was false); migration note on the plugin page

Struts 7.2.1

WW-4291: a Spring bean name can be used for a type converter; WW-3647: a jndi-lookup Spring bean no longer breaks ServletActionRedirectResult

Struts 7.4.0

WW-5717: Spring Framework dependency updated after an OWASP finding; WW-3257 closed with the note that a bean registered under the fully qualified class name is picked up. The plugin is built against Spring 6.2.19 and verified here on Spring 7.0.9

Spring Framework 7

Jakarta EE 11 and Servlet 6.1 baseline, JDK 17 baseline; ContextLoaderListener and the contextClass and contextConfigLocation parameters worked unchanged in the examples of this page

Where the Books Are Outdated

The Spring chapter of Struts 2 in Action was written for Struts 2.0 and Spring 2. These differences matter today:

In the book Today

Spring 2 XML with a DTD header and the singleton attribute

XSD headers and scope; a DTD with singleton="false" no longer loads on Spring 7.0.9 (observed)

com.opensymphony.xwork2.ObjectFactory and SpringObjectFactory in the XWork packages

org.apache.struts2.ObjectFactory, org.apache.struts2.spring.SpringObjectFactory

Autowire by name as the only default, with alwaysRespect unknown

Default name, alwaysRespect true since 7.2.0

JPA with Hibernate and OpenEntityManagerInViewFilter shown together with the Spring plugin

Spring Framework 7 with Spring Data JPA or Hibernate; for an open-session-per-request pattern read the Spring and Hibernate pages of this site (see Hibernate and Spring Data JPA); filter-based open session in view is a Spring MVC topic and is usually avoided

Spring 2 annotations such as @Transactional on actions mixed with XML proxies

Annotation-driven configuration and Java @Configuration classes; Spring Boot is the current default for new Spring work (not available with Struts, see above)

Struts 2.0 and struts.properties as the place for constants

struts.xml constants or struts.properties; the defaults are in default.properties of the 7.4.0 core jar

JSP only (Spring tags and form tags) for views

JSP, FreeMarker and Velocity; for JSP with Spring MVC see the Spring MVC with JSP page

Inconsistencies in the Official Documentation

  • The plugin page’s XML example (singleton="false", a -//SPRING//DTD BEAN//EN header) does not load on Spring 7.0.9; use scope="prototype".

  • The plugin page’s class reloading constants (struts.class.reloading.*) are not the ones the 7.4.0 jar reads (struts.objectFactory.spring.class.reloading.*).

  • The plugin page says struts.objectFactory needs to be set only when several object factories are present; the plugin’s own struts-plugin.xml already sets it to spring.

  • The plugin page’s text refers to two autowiring interceptors; the 7.4.0 jar has one.

  • Typing slips remain on the page (applicationConext.xml, ransactional), and the Getting Started page refers to "section 3.5" of the Spring documentation, which no longer exists as a numbered section.

Summary

To use Spring with Struts 7.4.0, add struts2-spring-plugin, add ContextLoaderListener (plus RequestContextListener for request and session scopes), define the services as Spring beans in XML or Java configuration and give the actions setters whose names match the bean ids (or switch to autowire by type, or annotate). Let Spring create the action itself only when it needs a Spring proxy or constructor injection, and then make the bean prototype. Spring Framework 7.0.9 works with the 7.4.0 plugin, class reloading is experimental and its constants are named differently in the jar, and Spring Boot is not officially supported.

References