Architecture & Request Lifecycle

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 on 2026-10-02, with 6.12.0 as the maintained 6.x line), Jakarta Servlet 6.1 and Jakarta Pages 4.0 on Apache Tomcat 11.0.26 and Java 21 (Struts 7 needs Java 17 and a Jakarta Servlet 6 container). Everything was verified on 2026-10-08: the facts against the Core Developer Guide pages, the struts2-core 7.4.0 jar (class listings, struts-default.xml, struts-beans.xml, default.properties) and the 7.4.0 source tag, and every example by building it into a Maven WAR and running it on Tomcat 11.0.26 with curl; the outputs shown are the outputs of that run (only the context path was renamed to bookshelf). The page explains what happens between the moment the servlet container hands a request to Struts and the moment the response leaves: which class does which job, in which order, where each stage can be replaced, and what the secure defaults are. It is the map for the rest of Part II, so later pages (configuration, packages, actions, results, interceptors) each zoom into one box of the diagram on this page. The running scenario is the Bookshelf, a small catalogue of books; here it is a single action, books, that is traced end to end.

The Request in Five Steps

The official "Architecture in a Nutshell" lists five steps. The table adds the 7.4.0 class that performs each one, so that a stack trace or a log line can be placed in the picture.

Table 1. The five steps of the nutshell page and the classes behind them (Struts 7.4.0)
Nutshell step What happens Main classes

The browser requests a resource

A URL such as /books.action reaches the servlet container, which runs the filter chain of the web application

Tomcat, jakarta.servlet.Filter

The filter dispatcher finds the action

The filter asks the action mapper whether the URL is an action and which one

org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter, org.apache.struts2.dispatcher.mapper.ActionMapper

The interceptors apply common functionality

Workflow, parameter population, validation, file upload and so on run before the action

org.apache.struts2.ActionProxy, org.apache.struts2.ActionInvocation, org.apache.struts2.interceptor.Interceptor

The action method executes

The action usually reads or writes the model (a database, a service) and returns a result code

Your class, often a subclass of org.apache.struts2.ActionSupport

The result renders the output

A JSP, a FreeMarker template, a redirect, a stream or JSON is produced

org.apache.struts2.result.Result and its implementations

The same page describes the framework as a Model View Controller design in which Struts provides the controller and the view and integrates other technologies as the model. In that vocabulary the filter, the mapper, the proxy and the interceptors form the controller, the action is the adapter between the web and the model (the "traffic cop", in the page’s words, not the place for the heavy lifting), and the result and the tags are the view. The older books state it as "filter = controller, action = model, result = view"; the official wording is closer to the truth, because the action class normally delegates to services and only carries the data that the view needs. The Model 2 pattern that this repeats, a servlet-style controller forwarding to a view, is explained in MVC with Servlets and JSP; Struts is that pattern turned into a framework with configuration instead of code.

The Full Lifecycle

The next figure is the nutshell extended with the classes of Struts 7 and with the one fact that surprises most readers: the interceptors are not a pipeline that ends at the action, they are nested calls, and the result is executed at the innermost level, before any interceptor has run its "after" code.

The Struts 7 request lifecycle from the filter through the mapper, proxy, interceptors, action and result and back

The same flow as a sequence of calls for one GET /bookshelf/books.action?filter=sci:

sequenceDiagram autonumber participant B as Browser participant F as StrutsPrepareAndExecuteFilter participant M as ActionMapper participant D as Dispatcher participant P as ActionProxy participant I as ActionInvocation participant X as Interceptors participant A as BookAction participant R as Result (JSP) B->>F: GET /bookshelf/books.action?filter=sci F->>F: static content check, bind ActionContext, wrap request F->>M: getMapping(request) M-->>F: ActionMapping with namespace / and name books F->>D: serviceAction(request, response, mapping) D->>P: createActionProxy (ActionProxyFactory) P->>I: init, ObjectFactory builds the action D->>P: execute() P->>I: invoke() I->>X: intercept(invocation), outer interceptor first X->>I: invoke() again, once per interceptor I->>A: execute() A-->>I: result code success I->>I: PreResultListeners run I->>R: execute(invocation) R-->>B: HTML is written to the response I-->>X: invoke() returns, innermost interceptor first X-->>P: result code P-->>D: result code

The next table is the stage list of the figure with the extension point of each stage. "Hook" names the cheapest way to change the behaviour; the later sections show the ones that matter most.

Table 2. The stages, their 7.4.0 classes and where to hook in
Stage Class (default) Responsibility Hook

1 Filter

StrutsPrepareAndExecuteFilter

Initialises the framework once, then for each request decides: excluded URL, static resource, action or "not mine"

struts.action.excludePattern; StrutsPrepareFilter plus StrutsExecuteFilter; the filter mapping

2 Mapper

DefaultActionMapper

Turns the request URI into an ActionMapping (namespace, name, method, parameters), or returns null

struts.mapper.class, struts.action.extension, the allowed-name constants

3 Dispatcher

Dispatcher.serviceAction

Creates the context map and the value stack, asks for a proxy, runs it, turns configuration errors into 404

DispatcherErrorHandler, struts.handle.exception

4 Proxy

StrutsActionProxyFactory, DefaultActionProxy

Looks the action up in the Configuration and wraps one execution

struts.actionProxyFactory

5 Invocation

DefaultActionInvocation

Walks the interceptor list, calls the action, runs the pre-result listeners and the result (command pattern)

PreResultListener, interceptors

6 Interceptors

defaultStack of struts-default.xml

Cross-cutting behaviour before and after the action

<interceptor>, <interceptor-stack>, <interceptor-ref>

7 Action

Your class

Reads the input properties, calls the model, returns a result code

ActionSupport, execute or another method

8 Result

ServletDispatcherResult for dispatcher, others by type

Renders the response

Result types, <result> elements

Stage 1: The Filter

Struts enters the web application as one servlet filter. Struts 7.4 offers two ways to register it, and the Core Developer Guide describes both: the single StrutsPrepareAndExecuteFilter, which is the preferred setup, and the pair StrutsPrepareFilter and StrutsExecuteFilter for integration with other frameworks such as SiteMesh.

The filter registration of the Bookshelf (web.xml, Servlet 6.1 schema)
<web-app 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/web-app_6_1.xsd"
         version="6.1">
    <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>
</web-app>

Filters and their mapping rules (declaration order, dispatcher types, the /* pattern) are the ones of the servlet specification; see Servlet Fundamentals — Filters. Two Struts specific consequences follow from the /* mapping: Struts sees every request, including those for pictures and stylesheets, and it decides for each one whether it is "its" request. That decision is the heart of the filter. Reading the 7.4.0 source of StrutsPrepareAndExecuteFilter.doFilter gives this order.

Table 3. What the filter does for each request, in order (7.4.0 source)
Step Action

1

trackRecursion counts nested uses of the filter (forwards and includes can re-enter it)

2

A request whose URI matches struts.action.excludePattern is passed straight to the next filter in the chain

3

executeStaticResourceRequest: if the path starts with the static content path (default /static/) and static serving is on, the StaticContentLoader answers and the filter stops

4

setEncodingAndLocale applies struts.i18n.encoding (default UTF-8) and the locale

5

createActionContext creates or reuses the ActionContext (and with it the ValueStack) and binds it to the current thread

6

wrapRequest wraps the request, for example to handle multipart/form-data before any parameter is read

7

findActionMapping asks the ActionMapper; the result is stored as a request attribute

8

No mapping: chain.doFilter lets the rest of the web application (default servlet, JSPs, other filters) answer. A mapping: executeAction calls Dispatcher.serviceAction

9

In a finally block the request wrapper and the thread-bound objects are cleaned up

Preparing and Executing in Two Filters

The single filter does steps 1 to 7 and step 8 in one place. When another filter has to run in between, for example a decorator that needs the ActionContext of the request but must run before the action does, the work is split: StrutsPrepareFilter does the preparation, StrutsExecuteFilter does the static content and action execution, and the other filter sits between them in the filter order. The Bookshelf experiment uses a tiny filter that only reports whether the ActionContext is already bound.

Prepare, a filter of your own, and execute (web.xml fragment)
<filter>
    <filter-name>struts-prepare</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareFilter</filter-class>
</filter>
<filter>
    <filter-name>between</filter-name>
    <filter-class>org.example.bookshelf.BetweenFilter</filter-class>
</filter>
<filter>
    <filter-name>struts-execute</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsExecuteFilter</filter-class>
</filter>
<!-- the filter-mapping elements for all three use /* in this order: prepare, between, execute -->
A filter that runs between the two Struts filters
package org.example.bookshelf;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
import org.apache.struts2.ActionContext;

public class BetweenFilter implements Filter {
    private static final System.Logger LOG = System.getLogger("bookshelf.trace");

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        ActionContext ctx = ActionContext.getContext();
        LOG.log(System.Logger.Level.INFO, "between filters: ActionContext bound = {0}", ctx != null);
        chain.doFilter(req, res);
    }
}

The request GET /bookshelf/books.action?filter=sci was answered normally with this registration and the server log contained

mapper         /bookshelf/books.action -> namespace=/ name=books method=null
between filters: ActionContext bound = true

so the custom filter saw a bound ActionContext after the preparation filter had already consulted the mapper. With the single filter, any filter declared before it sees no ActionContext at all, because Struts has not created it yet.

Telling Struts to Ignore a URL

A URL that must never reach Struts, for instance a health probe served by the container, is excluded with a regular expression that is matched against getRequestURI(), so it includes the context path. The pattern is a constant, not a filter mapping, which keeps the /* mapping (and Struts' static content) intact.

Excluding /bookshelf/health from Struts (struts.xml)
<constant name="struts.action.excludePattern" value=".*/health"/>

With a plain file health (content ok) in the web application root, the request below is answered by Tomcat’s default servlet and the mapper is never consulted (the trace shows no mapper line for it).

$ curl -i http://localhost:8080/bookshelf/health
HTTP/1.1 200
...
ok

Without the constant the same URL maps to an action named health and gives the Struts 404 shown in the outcome table for /nothing below. The exclusion is a routing tool, not an access control: an excluded URL is served by whatever the rest of the chain does with it.

Stage 2: The ActionMapper

The ActionMapper interface translates between HTTP requests and action invocation requests in both directions. In 7.4.0 it declares three methods: getMapping(request, configManager), which returns an ActionMapping or null; getMappingFromActionName(name); and getUriFromActionMapping(mapping), which builds a URL for the tags (s:url, s:form) when a page links to an action. The mapper does not have to check that the action exists: it only parses the URL. Whether the action is configured is decided later, by the proxy.

Table 4. How DefaultActionMapper.getMapping reads a URI (7.4.0 source and constants)
Step Behaviour

Strip path parameters

Everything from the first ; on is dropped, so books.action;jsessionid=abc123 maps like books.action

Drop the extension

The extension must be one of struts.action.extension, whose 7.4.0 default is action,, (the extension action or none). Any other extension (.do, .json, .html) returns null

Split namespace and name

The part before the last / is the namespace, the rest is the action name; the namespace is matched against the configured packages (with struts.mapper.alwaysSelectFullNamespace=true since Struts 7 the full namespace must match)

Special parameters

The method: and action: request-parameter prefixes are read only when enabled: struts.enable.DynamicMethodInvocation and struts.mapper.action.prefix.enabled both default to false

Validate the names

The namespace must match [a-zA-Z0-9._/\-]*, the action name [a-zA-Z0-9._!/\-]* (otherwise the defaults / and index are used); the method regex [a-zA-Z_]*[0-9]* applies when dynamic method invocation is enabled. The constants are struts.allowed.namespace.names, struts.allowed.action.names, struts.allowed.method.names and struts.default.*.name

What the Mapper Decides: Observed Outcomes

The Bookshelf was deployed with a mapper that logs its decision (the next section shows the class). The table is what curl and the log showed for each request.

Table 5. Requests against the Bookshelf and what happened (Tomcat 11.0.26, Struts 7.4.0 defaults)
Request Mapper decision Outcome

/books.action?filter=sci

Namespace /, name books

200, the books page

/books

Namespace /, name books (empty extension allowed)

200, same page

/books.action;jsessionid=abc123?filter=x

Path parameter dropped

200

/books.do and /books.json

No mapping (.do and .json are not in the extension list)

The filter chain continues, Tomcat answers 404 with its own page; Struts is not involved

/index.html

No mapping

Tomcat 404

/nothing

Name nothing

404 from Struts: "There is no Action mapped for namespace [/] and action name [nothing] associated with context path [/bookshelf]."

/books!list

Name books!list, because dynamic method invocation is off and nothing splits at !

The same Struts 404, with the whole books!list as action name

/books.action?action:other and /books.action?method:list

Plain request parameters, the prefixes are off

200, the books page; the parameters are ignored

/health

Excluded by struts.action.excludePattern

Served by Tomcat, ok

/static/utils.js

Static content loader (before the mapper)

200, text/javascript

/struts/utils.js

Not a static path in 7.4.0

404

Replacing the Mapper

The mapper is a bean of type org.apache.struts2.dispatcher.mapper.ActionMapper; a constant chooses which named bean is used. The Core Developer Guide shows the registration and says that a custom mapper must implement the interface and have a default constructor. For the experiment the mapper extends DefaultActionMapper and only adds a log line.

Registering a mapper (struts.xml)
<bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="traceMapper"
      class="org.example.bookshelf.TraceActionMapper"/>
<constant name="struts.mapper.class" value="traceMapper"/>
A mapper that reports what it decides
package org.example.bookshelf;

import jakarta.servlet.http.HttpServletRequest;
import org.apache.struts2.config.ConfigurationManager;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;

public class TraceActionMapper extends DefaultActionMapper {
    private static final System.Logger LOG = System.getLogger("bookshelf.trace");

    @Override
    public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) {
        ActionMapping mapping = super.getMapping(request, configManager);
        LOG.log(System.Logger.Level.INFO, "mapper         {0} -> {1}", request.getRequestURI(),
                mapping == null ? "no mapping, the filter chain continues"
                        : "namespace=" + mapping.getNamespace() + " name=" + mapping.getName()
                        + " method=" + mapping.getMethod());
        return mapping;
    }
}

Because the mapper is also used in the other direction, a page that renders <s:url action="books"/> asks the same bean for the URL; in the Bookshelf the page printed /bookshelf/books.action, using the first extension of the list. A custom mapper that maps /person/1 to an action must therefore also implement getUriFromActionMapping, or the generated links will not match the URLs the mapper understands.

Struts ships four more mappers as beans: composite (tries several mappers in the order given by struts.mapper.composite), prefix (selects a mapper by URL prefix with struts.mapper.prefixMapping, used together with the prefix action proxy factory), and restful and restful2. The two RESTful mappers are deprecated in 6.12.0 and 7.4.0 in favour of the REST plugin (removal is tracked as WW-5708), which has its own mapper; the REST Plugin page covers it.

Stage 3: Dispatcher, ActionProxy and ActionInvocation

With a mapping in hand the filter calls Dispatcher.serviceAction. The Dispatcher is the main point of control of the framework, created once by the filter’s init. For each request it creates the context map (request, session, application, parameters), copies the existing value stack if the request is a forward, asks the ActionProxyFactory for an ActionProxy for the namespace, name and method, and calls proxy.execute(). If the mapping carries a result of its own (a mapper may map a URL straight to a Result) that result is executed instead and no action runs.

The three objects have distinct jobs, which the official page sums up as: the proxy encapsulates how an action can be obtained, the invocation encapsulates how it is executed.

Table 6. ActionProxyFactory, ActionProxy and ActionInvocation
Object Job Notes

ActionProxyFactory

Creates proxies (createActionProxy in three overloads) and their invocations

An interface in 7.4.0, selected by the bean name in struts.actionProxyFactory; the beans are struts (StrutsActionProxyFactory, default) and prefix (PrefixBasedActionProxyFactory)

ActionProxy

Knows the namespace, action name, method and the ActionConfig from struts.xml; execute() binds the invocation’s ActionContext to the thread and calls invocation.invoke()

One per executed action; getAction() returns the action instance, getExecuteResult() says whether the result runs

ActionInvocation

Holds the action, the value stack, the interceptor iterator, the result code and the pre-result listeners; invoke() is the command pattern

DefaultActionInvocation; one per request. init pushes the action on the value stack and builds the context

When Something Goes Wrong Here

An action name that is mapped but not configured makes the proxy throw a ConfigurationException. serviceAction catches it, logs it and reports HTTP 404 through the DispatcherErrorHandler; any other exception becomes HTTP 500 (the default struts.handle.exception=true) or is rethrown as a ServletException. The default handler uses HttpServletResponse.sendError, so the container’s error pages (web.xml, see Servlet Fundamentals — Error Pages) apply. A custom handler is a bean of type DispatcherErrorHandler selected with struts.dispatcher.errorHandler.

Observing the error path (struts.xml and a handler that logs and delegates)
<bean type="org.apache.struts2.dispatcher.DispatcherErrorHandler" name="traceErrors"
      class="org.example.bookshelf.TraceErrorHandler"/>
<constant name="struts.dispatcher.errorHandler" value="traceErrors"/>
package org.example.bookshelf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.struts2.dispatcher.DefaultDispatcherErrorHandler;

public class TraceErrorHandler extends DefaultDispatcherErrorHandler {
    private static final System.Logger LOG = System.getLogger("bookshelf.trace");

    @Override
    public void handleError(HttpServletRequest request, HttpServletResponse response, int code, Exception e) {
        LOG.log(System.Logger.Level.INFO, "error handler  HTTP {0}: {1}", code, e.getMessage());
        super.handleError(request, response, code, e);
    }
}

For GET /bookshelf/nothing the log printed the line below, and the browser received the 404 page with the same message.

mapper         /bookshelf/nothing -> namespace=/ name=nothing method=null
error handler  HTTP 404: There is no Action mapped for namespace [/] and action name [nothing] associated with context path [/bookshelf].

Stage 4: The Interceptors

ActionInvocation.invoke() is a recursion. Each call takes the next interceptor from the list and calls its intercept(invocation); an interceptor that wants the rest of the stack to run calls invocation.invoke() itself and gets back the result code when everything inside it, the action and the result included, has finished. When the list is exhausted the invocation calls the action. That is why the figure draws nested frames, and why the official page on writing interceptors warns that invoke returns only after the result has been called (after the JSP has been rendered), which makes interceptors a good place for open-session-in-view logic and a poor place to modify the response after the page is written.

An interceptor can also stop the chain: if it returns a result code without calling invoke(), nothing inside it runs and that code selects the result. Validation and workflow do exactly that when input is invalid.

The Default Stack

An action that does not name an interceptor stack gets defaultStack, set as default-interceptor-ref of the struts-default package. The list below was read from struts-default.xml in the 7.4.0 jar: 24 interceptors, in this order, taken from the 38 interceptors that the file declares.

Table 7. The defaultStack of Struts 7.4.0, outermost first
# Name Role (details on Interceptors)

1

exception

Maps an exception thrown further in to a result (see the exception handling page)

2

alias

Lets parameter names be aliased to other names (used with chained actions)

3

servletConfig

Hands the servlet objects to actions that declare the Aware interfaces

4

httpMethod

Restricts an action to the HTTP methods allowed for it

5

i18n

Chooses the locale, for example from the request_locale parameter, and keeps it in the session

6

csp

Content-Security-Policy header (parameters disabled and enforcingMode, report-only by default)

7

prepare

Calls prepare() on Preparable actions

8

chain

Copies properties from the previous action in a chain

9, 10

scopedModelDriven, modelDriven

Use a model object instead of the action as the parameter target

11

actionFileUpload

File upload for UploadedFilesAware actions

12, 13, 14

checkbox, datetime, multiselect

Merge or default form values that browsers omit or submit in parts

15

staticParams

Sets the <param> values of the action from struts.xml

16

actionMappingParams

Sets parameters taken from the ActionMapping

17

params

Sets the request parameters on the action (subject to the OGNL and @StrutsParameter rules)

18

conversionError

Remembers type-conversion failures as field errors

19, 20, 21

coep, coop, fetchMetadata

Cross-origin isolation headers and Fetch Metadata resource isolation; in the 7.4.0 stack coep is report-only, coop is same-origin, fetchMetadata enabled

22

validation

Runs the validators (excluded methods input,back,cancel,browse)

23

workflow

Returns the input result when validation produced errors

24

debugging

Serves the debugging page when the development mode asks for it

One Action Per Request, One Interceptor For All

The proxy copies the interceptor list from the ActionConfig into an iterator for every request, but the interceptor objects in that list were built once when the configuration was loaded. So an action is a new object for each request and needs no thread safety, while an interceptor is shared by all requests and must be thread safe (fields set by parameters in struts.xml are configuration, not per-request state). The experiment below prints the identity of each object to prove it.

Code Between the Action and the Result

A PreResultListener runs after the action has returned its result code and before the result is executed. It can be added by an action or, more usually, by an interceptor. In DefaultActionInvocation.invoke the listeners run in the order they were added, which is the order of the interceptors that registered them (outer first).

A tracing interceptor with a pre-result listener, and a stack that uses it twice (struts.xml)
<package name="default" extends="struts-default" namespace="/">
    <interceptors>
        <interceptor name="trace" class="org.example.bookshelf.TraceInterceptor"/>
        <interceptor-stack name="tracedStack">
            <interceptor-ref name="trace">
                <param name="label">outer</param>
            </interceptor-ref>
            <interceptor-ref name="defaultStack"/>
            <interceptor-ref name="trace">
                <param name="label">inner</param>
            </interceptor-ref>
        </interceptor-stack>
    </interceptors>
    <default-interceptor-ref name="tracedStack"/>

    <action name="books" class="org.example.bookshelf.BookAction">
        <result>/WEB-INF/views/books.jsp</result>
    </action>
</package>
package org.example.bookshelf;

import org.apache.struts2.ActionInvocation;
import org.apache.struts2.interceptor.AbstractInterceptor;

public class TraceInterceptor extends AbstractInterceptor {
    private static final System.Logger LOG = System.getLogger("bookshelf.trace");
    private String label = "trace";

    public void setLabel(String label) {
        this.label = label;
    }

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        LOG.log(System.Logger.Level.INFO, "{0} before    action={1}, interceptor instance {2}", label,
                invocation.getProxy().getActionName(), Integer.toHexString(System.identityHashCode(this)));
        invocation.addPreResultListener((inv, resultCode) ->
                LOG.log(System.Logger.Level.INFO, "{0} pre-result, result code {1}", label, resultCode));
        String resultCode = invocation.invoke();
        LOG.log(System.Logger.Level.INFO, "{0} after     result code {1}", label, resultCode);
        return resultCode;
    }
}

Stages 5 and 6: The Action and the Result

When the interceptor list is exhausted, invokeActionOnly calls the action method named by the mapping, or the one configured in struts.xml, or execute. The action is a new instance built by the ObjectFactory for this request (the next-but-one section) and is pushed on the value stack by the invocation, which is how the view later reads its properties by name. The returned string is the result code; ActionSupport defines the usual constants (SUCCESS, INPUT, ERROR, LOGIN, NONE). The invocation looks up the <result> of that name (a <result> without a name is success), builds the result through the ObjectFactory, and executes it. A result is itself called from inside the innermost invoke(), so by the time the first interceptor’s "after" code runs, the response has been produced.

The Bookshelf action and view that the trace follows
package org.example.bookshelf;

import java.util.List;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.interceptor.parameter.StrutsParameter;

public class BookAction extends ActionSupport {
    private static final System.Logger LOG = System.getLogger("bookshelf.trace");
    private String filter;

    public String getFilter() {
        return filter;
    }

    @StrutsParameter
    public void setFilter(String filter) {
        this.filter = filter;
    }

    @Override
    public String execute() {
        LOG.log(System.Logger.Level.INFO, "action         execute(), filter={0}", filter);
        return SUCCESS;
    }

    public List<String> getTitles() {
        LOG.log(System.Logger.Level.INFO, "result         the JSP reads titles from the action");
        return List.of("Dune", "Neuromancer");
    }
}
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (Struts)</h1>
<p>Filter: <s:property value="filter"/></p>
<ul>
<s:iterator value="titles"><li><s:property/></li></s:iterator>
</ul>
<p>Link built by the ActionMapper: <s:url action="books"/></p>
</body>
</html>

The @StrutsParameter annotation on setFilter is required in Struts 7: the params interceptor sets only the injection points that carry it (the struts.parameters.requireAnnotations default is true in 7.4.0). The view lives under WEB-INF, so a browser can reach it only through the action. The result is a dispatcher result (the default type), which forwards to the JSP through the servlet RequestDispatcher; the JSP’s s:property tags then read filter and titles through the value stack. The parameter and value stack rules have pages of their own.

Worked Example: Tracing a Bookshelf Request

The experiment combines the classes above in one WAR: the mapper, an ObjectFactory that reports the action it builds (shown in the next section), the traced stack, the action and the view. It was deployed as a Maven WAR on Tomcat 11.0.26 and called twice.

$ curl -s "http://localhost:8080/bookshelf/books.action?filter=sci"

<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (Struts)</h1>
<p>Filter: sci</p>
<ul>
<li>Dune</li><li>Neuromancer</li>
</ul>
<p>Link built by the ActionMapper: /bookshelf/books.action</p>
</body>
</html>

The log of the second request (thread names and times removed; each line is one System.Logger call):

One request, ten lines
mapper         /bookshelf/books.action -> namespace=/ name=books method=null
object factory built BookAction@6b0a7653
outer before    action=books, interceptor instance 223a3148
inner before    action=books, interceptor instance 7e96c650
action         execute(), filter=sci
outer pre-result, result code success
inner pre-result, result code success
result         the JSP reads titles from the action
inner after     result code success
outer after     result code success

The first request logged the same ten lines with the same interceptor instance ids (223a3148 and 7e96c650) and a different action (BookAction@d6f8f04).

Table 8. Reading the trace against the figure
Log line What it proves

mapper ... first

The mapper runs before anything of the action exists; the namespace is /, the name books, the method is null (so execute)

object factory built

A new action object is built for the request; the second request had a different identity hash

outer before, inner before

The outer interceptor’s code runs first; the whole defaultStack (24 interceptors, not logged) sits between the two

action execute(), filter=sci

The params interceptor already set filter from the query string (it is annotated); the action runs inside the innermost interceptor

outer pre-result, inner pre-result

Pre-result listeners run in registration order, so the outer one first, and before the view

result the JSP reads titles

The JSP is rendered here, inside the innermost invoke(), and reads getTitles() through the value stack

inner after, outer after

Only now do the interceptors unwind, innermost first, with the result code

Interceptor instance ids repeat across requests

Interceptors are shared between requests; thread safety is the interceptor author’s job

The Internal Container and the ObjectFactory

Most objects named on this page are not created with new by other framework classes. Struts has an internal dependency injection container, built from bean and constant declarations, and each replaceable part of the framework is a bean with a name. The 7.4.0 jar declares the defaults in struts-beans.xml (included by struts-default.xml), the plugins add theirs in struts-plugin.xml, and an application’s struts.xml can add or select its own, always the same way: declare a bean of the extension point’s type under a name, then point the constant at the name.

Table 9. Extension points met on this page, their default beans and the constants that choose them (7.4.0)
Bean type (org.apache.struts2...) Default (bean name struts) Constant

ObjectFactory

ObjectFactory itself (the Spring plugin offers spring)

struts.objectFactory

factory.ActionFactory, ResultFactory, InterceptorFactory, ValidatorFactory, ConverterFactory, UnknownHandlerFactory

DefaultActionFactory, StrutsResultFactory, DefaultInterceptorFactory and so on

struts.objectFactory.actionFactory, .resultFactory, .interceptorFactory, .validatorFactory, .converterFactory, .unknownHandlerFactory

ActionProxyFactory

factory.StrutsActionProxyFactory (also prefix)

struts.actionProxyFactory

dispatcher.mapper.ActionMapper

DefaultActionMapper (also composite, prefix, restful, restful2)

struts.mapper.class

dispatcher.StaticContentLoader

DefaultStaticContentLoader

struts.staticContentLoader

dispatcher.DispatcherErrorHandler

DefaultDispatcherErrorHandler

struts.dispatcher.errorHandler

util.ValueStackFactory

ognl.OgnlValueStackFactory

struts.valueStackFactory

validator.ActionValidatorManager

AnnotationActionValidatorManager (also no-annotations)

struts.actionValidatorManager

UnknownHandlerManager

DefaultUnknownHandlerManager

struts.unknownHandlerManager

The ObjectFactory is the most important of them. The official page states that all objects created by the framework are instantiated by it (actions, results, interceptors, validators, converters, unknown handlers), which is what lets integrations such as the Spring plugin substitute container-managed objects: the Spring plugin’s factory makes the actions Spring beans. Since 2.3.16 the factory delegates each kind of object to a dedicated factory interface, so one part can be changed without replacing the whole class. The experiment’s factory only logs the action it built.

An ObjectFactory that reports the actions it builds
<bean type="org.apache.struts2.ObjectFactory" name="traceFactory"
      class="org.example.bookshelf.TraceObjectFactory"/>
<constant name="struts.objectFactory" value="traceFactory"/>
package org.example.bookshelf;

import java.util.Map;
import org.apache.struts2.ObjectFactory;
import org.apache.struts2.config.entities.ActionConfig;

public class TraceObjectFactory extends ObjectFactory {
    private static final System.Logger LOG = System.getLogger("bookshelf.trace");

    @Override
    public Object buildAction(String actionName, String namespace, ActionConfig config,
                              Map<String, Object> extraContext) throws Exception {
        Object action = super.buildAction(actionName, namespace, config, extraContext);
        LOG.log(System.Logger.Level.INFO, "object factory built {0}@{1}", action.getClass().getSimpleName(),
                Integer.toHexString(System.identityHashCode(action)));
        return action;
    }
}

The official page registers a custom factory with struts.objectFactory=... in struts.properties; a bean plus a constant in struts.xml works the same way and is what the experiment used (the factory printed the line seen in the trace). Dependency injection into actions and the CDI integration have their own page, Dependency Injection and CDI.

Startup: How the Configuration Is Assembled

Everything above rests on the Configuration that the filter’s init builds once. Dispatcher.init loads the sources in a fixed order, and a later source can override a constant or bean of an earlier one. The order below is the order of the calls in the 7.4.0 source.

Table 10. Configuration sources in load order (Dispatcher.init)
# Source Content

1

default.properties

Every framework constant with its default (struts.action.extension=action,, and so on)

2

struts-default.xml, then every struts-plugin.xml on the classpath, then struts.xml

The built-in struts-default package and interceptors, the plugins' contributions, and the application’s configuration; the file list is the default configuration-paths (struts-default.xml,struts-plugin.xml,struts.xml)

3

Java configuration providers

Programmatic configuration through StrutsJavaConfiguration

4

struts.properties

Legacy overrides, still read when present

5

Custom providers from the configProviders filter init-parameter

Any class that implements ConfigurationProvider

6

The filter’s init-parameters

Any other init-param is treated as a constant

7

Standard object aliases, then the deferred struts-deferred.xml files

Late bindings

A ConfigurationProvider is the unit that feeds this process: init(Configuration), register(...) for beans and constants, and loadPackages() for packages, actions and results. The default provider reads struts.xml; a custom one can build the configuration in code. It is registered in web.xml, as the web.xml page documents.

Registering a configuration provider (web.xml fragment)
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
    <init-param>
        <param-name>configProviders</param-name>
        <param-value>org.example.bookshelf.TraceConfigProvider</param-value>
    </init-param>
</filter>

The provider of the experiment implements the interface and only logs its three calls. When the web application started, the log showed them in this order, followed by the first request.

config provider init
config provider register (beans and constants)
config provider loadPackages

How struts.xml itself is written and loaded, the constants, and reloading in development mode are covered by Configuration: struts.xml & Constants.

Static Content: the Requests Struts Answers Without an Action

Struts' own resources (the JavaScript and CSS of the xhtml and css_xhtml themes) live inside the core jar and are served by the StaticContentLoader, step 3 of the filter, before the mapper is asked. The default loader answers URLs that start with /static/ (the constant struts.ui.staticContentPath), searching the static package of the classpath, and the feature is switched off with struts.serve.static=false. The official page recommends extracting the files into the web application and letting the container serve them in production, because the loader is not optimised for static content. If the filter mapping is narrowed (for example *.action), the static path must be mapped explicitly or these requests never reach Struts.

Static content in the Bookshelf (Tomcat 11.0.26, Struts 7.4.0)
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://localhost:8080/bookshelf/static/utils.js
200 text/javascript
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/bookshelf/struts/utils.js
404

Struts 1 Versus Struts 2

Struts 1 is end of life (April 2013) and its documentation is no longer on the project site, but most legacy applications and older books describe it, so the differences matter when reading either. The table follows the categories of the wiki’s "Comparing Struts 1 and 2" page, in condensed form, with the 7.4.0 situation added.

Table 11. Struts 1 and Struts 2 compared
Topic Struts 1 (ActionServlet) Struts 2 (filter plus interceptors)

Entry point

One servlet, ActionServlet, mapped to a pattern such as *.do, with a request processor per module

A servlet filter; the mapper decides per request whether it is an action

Action class

Extends an abstract Action class and receives mapping, form, request and response

Any class with a public no-argument method returning a String; ActionSupport is a convenience, and the Action interface is optional

Threading

Actions are singletons and must be thread safe

A new action per request, so no thread safety needed (verified in the trace above)

Servlet API coupling

The request and response are parameters of execute

The action sees maps, or opts in to the servlet objects with Aware interfaces

Testing

Needs mock servlet objects

Instantiate, set properties, call the method

Input

An ActionForm subclass per form (or DynaBean)

Properties of the action or of a model object, populated by the params interceptor (annotated with @StrutsParameter in Struts 7)

Expression language

JSTL EL

OGNL, evaluated against the value stack

Binding values to views

The standard JSP scopes

The ValueStack

Type conversion

Mostly strings with BeanUtils

OGNL-based converters configured per class or application (see Type Conversion)

Validation

validate on the form, or Commons Validator

validate, XML and annotation validators through the validation and workflow interceptors

Control of execution

One lifecycle per module

A different interceptor stack per action

Status

End of life in April 2013; no security patches

Struts 7.4.0 and 6.12.0 supported

The wiki table dates from 2007, the Struts 2.0 era. Its rows are still a good map, but a few items need the modern reading: the parameter binding now needs @StrutsParameter, the Action classes, interceptors and OGNL support classes are in org.apache.struts2 instead of com.opensymphony.xwork2, and the Struts 1 request flow described by the older books exists only in applications that have not been migrated. Struts 1 (Classic) & Migration covers the legacy flow and the migration steps.

Security Notes on the Request Pipeline

Several of the secure defaults of Struts 7.4 sit in the stages above. They are the reason a fresh Bookshelf rejects URLs that the older books routinely used.

Table 12. Pipeline-level defaults and what they protect
Default (7.4.0) Where it acts Why it matters

struts.enable.DynamicMethodInvocation=false

Mapper

action!method and method: do not select a method; only the configured method (or an allowed-methods entry through a wildcard) runs

struts.mapper.action.prefix.enabled=false

Mapper

The action: parameter prefix cannot redirect a request to another action; the official page links this to bulletin S2-018

Allowed-name regular expressions

Mapper

Namespace, action and method names outside a safe character set fall back to defaults

struts.mapper.alwaysSelectFullNamespace=true and struts.actionConfig.fallbackToEmptyNamespace=false

Mapper and configuration

A URL reaches an action only through the exact namespace; bulletin S2-057 (CVE-2018-11776) was a remote code execution through namespace resolution with the older settings

Views under WEB-INF (the Bookshelf’s practice, not a framework default)

Result

A JSP in WEB-INF/views is reachable only through its action, never directly

struts.parameters.requireAnnotations=true, struts.allowlist.enable=true

Interceptors (params), OGNL

Only annotated injection points are populated, and OGNL may only touch allowlisted classes

struts.devMode=false

Dispatcher, interceptors

Debugging output and the debugging interceptor’s page are off in production

struts.action.excludePattern (unset)

Filter

An exclusion removes a URL from Struts' handling; it is not an authorisation rule

A custom ActionMapper, ActionProxyFactory or ObjectFactory sits in front of or under all these protections, so a replacement should extend the default class and call super, and any URL parsing it adds must be as strict as the default regular expressions. Security Hardening and Security Bulletins list the other settings and the history in full.

Pitfalls and Error Names

Table 13. Request-lifecycle pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26 and Struts 7.4.0)
Symptom or error Cause Fix

"There is no Action mapped for namespace [/] and action name [x] associated with context path [/bookshelf]" (observed)

The URL parsed to an action name that struts.xml does not declare, in that namespace

Fix the name or the <action>, check the package namespace, check for a typo in a link built by hand

Tomcat’s own 404 page for /books.do or /books.json (observed)

The extension is not in struts.action.extension (action,,), so the mapper returns null and Struts steps aside

Use .action or no extension, or add the extension to the constant on purpose

A legacy *.do link or form action fails

Struts 1 habit; do is not a default extension

Change the links, or set struts.action.extension to include do

ClassNotFoundException for a filter class at deployment

web.xml still names FilterDispatcher or a filter class from the old dispatcher.ng package of Struts 2.0 to 2.3; neither exists in struts2-core 7.4.0

Use org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter

ActionContext is null in a filter or listener of your own

The filter runs before Struts creates the context

Place the filter after StrutsPrepareFilter (split registration), or avoid needing the context there

Static files from the Struts jar (/static/...) return 404 when the filter is mapped to *.action

The filter never sees the request

Add a filter mapping for the static path, or extract the files into the web application (observed: the default path is /static, not /struts)

A request is handled by Struts although it should be served by Tomcat

The /* mapping makes Struts examine everything and an extension-less URL maps to an action name

Exclude it with struts.action.excludePattern, or keep non-action URLs under an extension that the mapper does not accept

action!method, method:save or action:other has no effect (observed)

Dynamic method invocation and the prefixes are off by default

Map methods explicitly or with wildcards (see the wildcard mappings page); do not re-enable DMI

An interceptor corrupts data between users

A per-request value kept in a field of an interceptor (shared by all requests)

Keep state in locals, the invocation or the action; fields only for configuration

An interceptor’s "after" code cannot change the page

invoke() returns after the result has been written

Change the result before it runs, with a PreResultListener, or return another result code before calling invoke()

The custom mapper’s links do not match its URLs

Only getMapping was changed; the tags use getUriFromActionMapping

Implement both directions

Custom ObjectFactory ignored

The constant names a bean that was not declared, or the bean is declared with another type

Declare <bean type="org.apache.struts2.ObjectFactory" name="x" class="..."/> and set struts.objectFactory to x

Old docs show com.opensymphony.xwork2.* imports (Struts 7)

Packages moved to org.apache.struts2 in 7.0

Search and replace the package prefix

A filter placed after StrutsPrepareAndExecuteFilter never sees action requests

The action is executed inside the Struts filter; the chain continues only for non-actions

Put the filter before Struts, or use the split registration

java.lang.IllegalStateException: Action has already executed

invoke() was called twice on the same invocation, for example by an interceptor that calls it again

Call invoke() at most once per interceptor

HTTP 500 with the exception in the response during development

struts.handle.exception=true reports the failure through the DispatcherErrorHandler; development mode shows details

Read the log; do not enable development mode in production

What Changed Recently

Table 14. Changes that touch the request lifecycle
Version Change

Struts 7.4.0 and 6.12.0 (2026-10-02)

RestfulActionMapper and Restful2ActionMapper are deprecated in favour of the REST plugin (removal tracked as WW-5708); WW-5706, the missing action-name validation of the RESTful mapper compared with DefaultActionMapper, is listed among the fixes

Struts 7.4.0

The REST plugin’s restDefaultStack and the Bean Validation plugin’s beanValidationDefaultStack now include the coep, coop and fetchMetadata interceptors that the core defaultStack already lists (csp and coep report-only, coop same-origin); struts.csp.nonceSource is honoured next to struts.csp.nonce.source (WW-5669). The core is built against Jakarta EE 10 APIs and runs on a Servlet 6.1 container such as Tomcat 11

Struts 7.0.0

Java 17 minimum; Jakarta EE namespace; all classes of com.opensymphony.xwork2 moved into org.apache.struts2 (ActionInvocation, ActionProxy, ObjectFactory, Interceptor, ActionSupport); stronger defaults: exact namespace matching (struts.mapper.alwaysSelectFullNamespace=true, struts.actionConfig.fallbackToEmptyNamespace=false), parameter injection needs @StrutsParameter

Struts 6.x

The 6.x line still uses the javax.servlet namespace and the com.opensymphony.xwork2 packages, and is maintained alongside 7.x (6.12.0 is current); see the migration guides for the differences

Struts 2.5

Strict method invocation and the <allowed-methods> element replace free dynamic method invocation; the action: prefix had already been disabled by default in 2.3.15.3

Struts 2.3.16

ObjectFactory split into dedicated factories (ResultFactory, ActionFactory, InterceptorFactory, ValidatorFactory, ConverterFactory), UnknownHandlerFactory from 2.3.18

Where the Books Are Outdated

Struts in Action (Manning, 2002) describes Struts 1.0 and 1.1, and Struts 2 in Action (Manning, 2008) describes Struts 2.0; both are listed in the section bibliography and neither is quoted here. The architecture they teach (front controller, mapper, interceptor chain, result) survived; the class names, the filter and several defaults did not.

Table 15. Lifecycle material of the books and its modern equivalent
The book’s era Today (Struts 7.4)

FilterDispatcher plus the optional ActionContextCleanUp filter (Struts 2.0 and 2.1)

StrutsPrepareAndExecuteFilter, or StrutsPrepareFilter and StrutsExecuteFilter; neither old class exists in struts2-core 7.4.0

com.opensymphony.xwork2.* (ActionSupport, ActionInvocation, Interceptor, ObjectFactory)

org.apache.struts2.* since 7.0; in 6.x the old packages remain

The Struts 1 request flow: ActionServlet, RequestProcessor, ActionForm, ActionForward, struts-config.xml

A filter, interceptors and struts.xml; Struts 1 is end of life

A servlet mapping such as *.do

A filter mapping, default extension action,,; *.do does not map unless configured

Free dynamic method invocation (action!method) and the action: and method: prefixes

Off by default; strict method invocation with <allowed-methods>

Any public setter is populated from request parameters

Only @StrutsParameter injection points in Struts 7

web.xml with DTD and servlet 2.3 or 2.4

The Jakarta EE schema, Servlet 6.1 or 6.0

Platform: Servlet 2.4, JSP 2.0, Java 5 to 7

Java 17 or later, Jakarta Servlet 6 or later; Tomcat 10.1 or 11 for 7.x

Static content under /struts/

/static/ (struts.ui.staticContentPath)

The Dojo plugin and the ajax theme of Struts 2.0 and 2.1

Gone; the JSON and REST plugins and plain JavaScript replace them

The official pages carry the same problem in places, and they are worth reading with that in mind: the Core Developer Guide’s "Platform Requirements" section of the nutshell page still says Servlet API 2.4, JSP API 2.0 and Java 7, the big picture page still draws FilterDispatcher and ActionContextCleanUp, the Action Proxy page shows static ActionProxyFactory.setFactory calls that the 7.4.0 interface no longer has, and the writing-interceptors page still imports com.opensymphony.xwork2.interceptor.Interceptor. When a name on those pages disagrees with the 7.4.0 jar, the jar wins.

References