Configuration: struts.xml & Constants

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, the current line together with 6.12.0), which runs on Jakarta Servlet 6.0 or later and so on Apache Tomcat 11.0.26 with Java 21 (Struts 7 needs Java 17 or later). Everything was verified on 2026-10-08: the facts against the Struts documentation pages named in the references, the default.properties and struts-default.xml that ship inside struts2-core-7.4.0.jar and the 7.4.0 source of the Dispatcher; every example by building it into a Maven WAR (the struts2-bom 7.4.0 and struts2-core, no other Struts artifact) and running it on Tomcat 11.0.26 with curl. The outputs shown are the outputs of that run, and rows marked "observed" were reproduced there. The running scenario is the Bookshelf: an action that lists books grows from one struts.xml into a configuration split over several files, a plugin, a few constants and two implementations of a catalogue bean.

Struts is configured almost entirely by one XML file, struts.xml, plus a flat list of named settings called constants. The same constants can come from five different places and the framework has a fixed order in which it reads them, so the most common configuration bug is not a typo but a value that is set in one place and silently overridden in another. This page is the reference for that machinery. The elements that describe the application itself have pages of their own: packages and namespaces, actions, wildcard mappings, results, interceptors and exception mappings. Here they appear only as the skeleton the configuration files carry.

The Configuration Files

A Struts application needs exactly one configuration file, and it is not a Struts file: web.xml must register the StrutsPrepareAndExecuteFilter (see Servlet Fundamentals for filters and the deployment descriptor). Everything else is optional, because the framework ships with its own configuration and every setting has a default. The documentation lists the files like this.

Table 1. The files that configure Struts (Core Developer Guide, "Configuration Files")
File Optional Location Purpose

web.xml

no

/WEB-INF/

The deployment descriptor: registers the filter, its init-params and its mapping

struts.xml

yes

/WEB-INF/classes/ (the class path)

Main configuration: packages, actions, results, interceptors, includes, beans and constants

default.properties

yes

inside struts2-core.jar

The framework defaults for every constant (see default.properties and struts.properties for what the "optional" really means)

struts-default.xml

yes

inside struts2-core.jar

The configuration Struts supplies: the struts-default package with the result types, interceptors and stacks

struts-plugin.xml

yes

the root of a plugin JAR

Configuration for a plugin, in the same format as struts.xml

struts-deferred.xml

yes

the root of a plugin JAR

Like struts-plugin.xml, but loaded last, which suits extension points

velocity.properties

yes

/WEB-INF/classes/

Overrides the default Velocity configuration

struts-default.vm

yes

/WEB-INF/classes/

Default Velocity macros referenced by velocity.properties

Two more files are loaded from the class path although the table does not list them: struts.properties, the legacy properties file (struts.properties, the Legacy File), and the two files struts-default.xml itself includes, struts-beans.xml (the framework’s own beans) and struts-excluded-classes.xml (the classes and packages OGNL may never touch). The same page describes how static content (the JavaScript and CSS of the UI tags) is served by the filter, which matters for the filter mapping (The Filter and Its Init-Params in web.xml).

Load Order: Who Wins

The Dispatcher (the object the filter creates in its init method) builds the configuration by registering providers one after another. For XML elements that define packages, later and earlier files are merged; for constants, which are plain name and value pairs, the provider registered last wins. The 7.4.0 Dispatcher.init() registers them in this order.

Table 2. The providers of Struts 7.4.0 in registration order (Dispatcher.init, 7.4.0 source)
Step Source What it contributes

1

org/apache/struts2/default.properties (inside the core JAR)

Every constant with its default value

2

struts-default.xml, then every struts-plugin.xml on the class path, then struts.xml

The XML configuration; constants declared with <constant> override step 1. This list is the default of the filter init-param config

3

javaConfigClasses (filter init-param)

Optional StrutsJavaConfiguration classes that declare beans and constants in Java

4

struts.properties on the class path

The legacy properties file; overrides every XML constant

5

configProviders (filter init-param)

Optional ConfigurationProvider classes, comma separated

6

The filter init-params of web.xml

Every init-param is copied into the constants; they override step 4

7

StrutsBeanSelectionProvider

Turns the final constants into bean selections (which ObjectFactory, which FileManager) and applies the devMode switch

8

Every struts-deferred.xml on the class path

Configuration that depends on all of the above, for plugin extension points

flowchart TD dp["default.properties
(core JAR)"] --> sd["struts-default.xml
(includes struts-beans.xml)"] sd --> sp["struts-plugin.xml
(every plugin JAR)"] sp --> sx["struts.xml
(+ include files)"] sx --> jc["javaConfigClasses
(optional)"] jc --> pr["struts.properties"] pr --> cp["configProviders
(optional)"] cp --> ip["web.xml filter init-params"] ip --> bs["bean selection and
devMode switch"] bs --> df["struts-deferred.xml
(plugin JARs)"] df --> end1["The one runtime configuration"]

The documentation page "Constant Configuration" gives a shorter list ("struts-default.xml, struts-plugin.xml, struts.xml, default.properties, web.xml, struts-deferred.xml") and places default.properties after struts.xml; the "Plugins Architecture" page gives the XML part correctly (default, plugins, struts.xml, deferred). The 7.4.0 source and the experiment below show that the defaults file is read first, which is why a value in struts.xml can override it.

Seeing the order in the Bookshelf

The same constant is defined in the plugin JAR, in struts.xml, in struts.properties and as a filter init-param, and an action reads the effective value through the container.

ConfigAction: a Struts container injects constants into actions with @Inject
package org.example.bookshelf;

import java.util.List;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.inject.Inject;

public class ConfigAction extends ActionSupport {
    @Inject("struts.devMode")
    private String devMode;

    @Inject("bookshelf.pageSize")
    private String pageSize;

    @Inject("bookshelf.order")
    private String order;

    @Inject("sorted")
    private Catalog catalog;

    public String getDevMode() { return devMode; }
    public String getPageSize() { return pageSize; }
    public String getOrder() { return order; }
    public List<String> getTitles() { return catalog.titles(); }
}

The action is mapped to a plain-text JSP (/config) that prints the four properties with <s:property value="order"/>. Adding one source at a time gave these results.

Table 3. Which value wins (observed on Tomcat 11.0.26, Struts 7.4.0)
Where the constant bookshelf.pageSize / bookshelf.order is set pageSize order

struts-plugin.xml (value 5, plugin) and struts.xml (value 20, struts.xml)

20

struts.xml

plus struts.properties (value 50, struts.properties)

50

struts.properties

plus filter init-params in web.xml (value 7, init-param)

7

init-param

an application file default.properties with bookshelf.pageSize=99, no struts.properties

20 (ignored)

struts.xml

an application file struts.properties with bookshelf.pageSize=99, no init-params

99

struts.xml

The plugin, struts.xml, struts.properties, init-param order of precedence is the order of the table in the previous section, and the fourth row is the one that surprises people (default.properties and struts.properties).

struts.xml

struts.xml is the main file. It must be on the class path (in a Maven project, src/main/resources/struts.xml, which ends up in WEB-INF/classes), and it begins with a DOCTYPE that names a Struts DTD. The DTD is validating: the parser rejects an element in the wrong place when the application starts.

Table 4. The DTD of Struts 7.4: top-level elements of struts (struts-6.0.dtd)
Element Meaning

<package>

Groups actions, results, interceptors and exception mappings; name is required, extends, namespace, abstract and strict-method-invocation are optional (see Packages and Namespaces)

<include file="...">

Loads another file in struts.xml format (Include Configuration)

<bean>

Registers a class in the internal container (Bean Configuration)

<constant name="..." value="...">

Sets one constant (Constants)

<bean-selection>

Names a BeanSelectionProvider that chooses among registered beans

<unknown-handler-stack>

Lists the UnknownHandler beans to try, in order, for unknown actions, results and methods (Unknown Handlers)

The content model is (package|include|bean|constant)*, bean-selection?, unknown-handler-stack?: packages, includes, beans and constants may be mixed in any order, but bean-selection and unknown-handler-stack must come last. The root element also takes an optional order attribute (Include Configuration).

The newest DTD is struts-6.0.dtd, and it is the right DOCTYPE for Struts 7.4: a request for https://struts.apache.org/dtds/struts-7.0.dtd returns 404 (checked 2026-10-08), and the file shipped inside struts2-core-7.4.0.jar (struts-default.xml) itself declares Struts Configuration 6.0.

The Bookshelf struts.xml: constants, beans, includes, a package and an unknown-handler stack
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
        "https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
    <constant name="struts.devMode" value="${env.STRUTS_DEV_MODE:false}"/>
    <constant name="struts.configuration.xml.reload" value="true"/>
    <constant name="bookshelf.pageSize" value="20"/>
    <constant name="bookshelf.order" value="struts.xml"/>

    <bean type="org.example.bookshelf.Catalog" name="inMemory" class="org.example.bookshelf.InMemoryCatalog"/>
    <bean type="org.example.bookshelf.Catalog" name="sorted" class="org.example.bookshelf.SortedCatalog"/>

    <bean type="org.apache.struts2.UnknownHandler" name="shelf" class="org.example.bookshelf.ShelfUnknownHandler"/>
    <include file="bookshelf/struts-*.xml"/>

    <package name="default" extends="struts-default" namespace="/">
        <action name="books" class="org.example.bookshelf.BookAction">
            <result>/WEB-INF/views/books.jsp</result>
        </action>
        <action name="config" class="org.example.bookshelf.ConfigAction">
            <result>/WEB-INF/views/config.jsp</result>
        </action>
    </package>

    <unknown-handler-stack>
        <unknown-handler-ref name="shelf"/>
    </unknown-handler-stack>
</struts>

The package extends struts-default, the abstract package of struts-default.xml that supplies the result types and the interceptor stacks (struts-default.xml); an action in a package that does not extend it has no defaultStack.

Include Configuration

The "divide and conquer" strategy for large configurations is the <include> element. The file attribute is a class path resource name, resolved from the root of the class path (not relative to the including file), and each included file must have the same format as struts.xml, including the DOCTYPE. In a team, each module owns its file.

Splitting the Bookshelf: struts.xml includes one file per module
<include file="bookshelf/struts-*.xml"/>
src/main/resources/bookshelf/struts-admin.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
        "https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
    <package name="admin" extends="struts-default" namespace="/admin">
        <action name="stats" class="org.example.bookshelf.BookAction">
            <result>/WEB-INF/views/books.jsp</result>
        </action>
    </package>
</struts>
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:18080/g8conf/admin/stats
200

The documentation shows only literal file names; the 7.4.0 XmlConfigurationProvider also accepts a pattern with *, as the example does, and resolves it against the class path when the file loads. Four behaviours of includes follow from the 7.4.0 source; the last was also observed.

  • A file is loaded once: a second <include> of the same name is ignored (source).

  • A missing file is skipped without an error (the provider logs "Ignoring file that does not exist" at debug level), so a misspelled include silently drops a whole module. Check that the module’s actions answer.

  • The order attribute of the root <struts order="10"> is read as an integer, and documents are sorted by it before their includes are expanded; a missing or invalid value counts as Integer.MAX_VALUE, so the default is "last". Use it only to control the loading order between files of the same name, for example the struts-plugin.xml of two plugins.

  • With struts.configuration.xml.reload on, adding a new file that matches the pattern is not noticed until one of the files the framework already tracks changes; after touching a tracked file, the new package answered (observed, Reloading and Development Mode).

Constants

Constants are the framework’s settings. The documentation names two roles: overriding a setting such as the maximum upload size or devMode, and choosing which implementation of a bean type the framework uses. A constant is always a string name and a string value; the name is conventionally struts. plus a dotted path, and Struts exposes every built-in name as a field of the StrutsConstants class.

Table 5. The five places a constant can be set
Place Syntax Notes

struts.xml (and any file in its format)

<constant name="struts.devMode" value="true"/>

The recommended place: typed, validated by the DTD, versioned with the application

struts-plugin.xml, struts-deferred.xml

the same <constant> element

Lets a plugin set defaults; struts.xml overrides them

struts.properties

struts.devMode=true

Legacy file kept "for backward-compatibility with WebWork"; overrides XML (struts.properties, the Legacy File)

web.xml filter init-param

<init-param> with param-name struts.devMode

Every init-param becomes a constant; overrides everything above

a custom ConfigurationProvider or javaConfigClasses entry

Java code

For configuration computed at start-up

A constant in each of the XML forms
<!-- struts.xml -->
<struts>
    <constant name="struts.devMode" value="true"/>
</struts>

<!-- web.xml -->
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
    <init-param>
        <param-name>struts.devMode</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
# struts.properties
struts.devMode=true

A constant whose name is misspelled is accepted without a message: adding <constant name="struts.devmod" value="true"/> to the Bookshelf’s struts.xml reloaded cleanly, the application kept answering and devMode stayed false (observed). The framework looks constants up by name and nobody looks up struts.devmod.

Value Substitution

Since Struts 2.5.6 a constant value in struts.xml can read a Java system property or an environment variable, with an optional default after a colon. The substitution applies only to constants, and only to those two sources.

Environment and system properties in struts.xml
<struts>
    <constant name="os" value="Current OS = ${os.name}"/>
    <constant name="struts.devMode" value="${env.STRUTS_DEV_MODE:false}"/>
</struts>

The Bookshelf uses the second form, so a developer switches devMode on with STRUTS_DEV_MODE=true in the environment of the JVM and production keeps the safe default. The substitution is done by EnvsValueSubstitutor, and calling it directly on the Struts 7.4.0 class path gave these results.

$ java -cp ... Subst
false                              # ${env.STRUTS_DEV_MODE:false}, variable not set
Current user = airurueta           # ${user.name:nobody}
${env.BOOKSHELF_MISSING}           # no variable, no default: left as written
$ STRUTS_DEV_MODE=true java -cp ... Subst
true

Two details matter. A variable with no default and no value is not an error; the literal text stays in the constant, which for a boolean constant is then false. And ${...} in a <constant> is not OGNL: the same characters in a result parameter such as <param name="location">${target}</param> are evaluated against the value stack (see Results & Result Types).

The Constants Worth Knowing

default.properties in the 7.4.0 core JAR has about 75 active entries. These are the ones an application normally touches or must not weaken, with the 7.4.0 default (checked in the JAR). The security rows are the secure defaults: change them only for a stated reason.

Table 6. Frequently used constants and their Struts 7.4.0 defaults
Constant Default Effect

struts.devMode

false

Development mode: reloads resource bundles and XML on change, turns ignorable problems into errors, shows debug output. Never in production (Reloading and Development Mode)

struts.action.extension

action,,

Extensions the DefaultActionMapper accepts; the blank entries allow both /books and /books.action, and *.do does not match

struts.enable.DynamicMethodInvocation

false

The foo!bar and method:bar URL forms; keep off, use wildcards (see Wildcard Mappings & Method Invocation)

struts.mapper.action.prefix.enabled

false

The action: prefix

struts.mapper.alwaysSelectFullNamespace

true

Namespace resolution: use everything before the last slash (see Packages and Namespaces)

struts.actionConfig.fallbackToEmptyNamespace

false

Whether an unmatched namespace falls back to the empty namespace

struts.i18n.encoding

UTF-8

Encoding for request parameters and resource bundles

struts.locale

not set

A fixed locale; if unset the request locale is used

struts.locale.validateRequestLocale

false

Restricts request-derived locales to those the JVM knows

struts.custom.i18n.resources

not set

Extra global resource bundles, comma separated (see Localization and i18n)

struts.multipart.parser

jakarta

The multipart parser (jakarta, jakarta-stream)

struts.multipart.maxSize

2097152

Largest accepted request, in bytes (2 MiB)

struts.multipart.maxFiles, struts.multipart.maxParameterCount

256 each

Limits on files and on non-file fields in a multipart request

struts.multipart.maxStringLength

4096

Longest string value in a multipart request

struts.multipart.saveDir

empty

Temporary directory; empty means the servlet container’s temporary directory

struts.ui.theme

xhtml

Default theme of the UI tags (see Themes & Templates)

struts.ui.templateSuffix

ftl

Template engine of the UI tags

struts.serve.static

true

Serve the framework’s own JavaScript and CSS from the JAR

struts.ui.staticContentPath

/static

URL prefix of that static content

struts.serve.static.browserCache

true

Send caching headers for it

struts.allowlist.enable

true

The OGNL allowlist; "strongly recommended to keep enabled"

struts.parameters.requireAnnotations

true

Only members annotated with @StrutsParameter receive request parameters (see Parameter Injection and @StrutsParameter)

struts.parameters.requireAnnotations.transitionMode

false

Drops the annotation requirement on simple setters while migrating; "set to false as soon as possible"

struts.ognl.allowStaticFieldAccess

false

Static fields in OGNL expressions

struts.ognl.disallowCustomOgnlMap

true

Only the standard map classes can be created from OGNL

struts.ognl.expressionMaxLength

256

Maximum length of an OGNL expression; see What Changed Recently for the guide that says 150

struts.disallowProxyObjectAccess, struts.disallowDefaultPackageAccess

true

Block OGNL access to proxy objects and to classes in the default package

struts.handle.exception

true

The Dispatcher calls sendError for unexpected exceptions; false rethrows them as ServletException

struts.el.throwExceptionOnFailure

false

Throw when a property is missing in an expression

struts.configuration.xml.reload

not set (false)

Reload XML configuration when a file changes (Reloading and Development Mode)

struts.i18n.reload

not set (false)

Reload resource bundles on each request

struts.objectFactory

not set

Which ObjectFactory builds actions and interceptors (for example spring)

struts.custom.properties

not set

Additional properties files to load, comma separated

struts.ognl.expressionCacheType, struts.i18n.cacheType, struts.proxy.cacheType

wtlfu

Cache implementations for parsed expressions, localized text and proxy detection (sizes: ...CacheMaxSize, default 10000)

struts.freemarker.whitespaceStripping, struts.tag.compress.enabled

true

Template performance settings; compress.maxSize defaults to 10 MiB

struts.url.http.port, struts.url.https.port, struts.url.includeParams

80, 443, none

How the URL tags build absolute URLs

struts.date.formatter

dateTimeFormatter

The java.time based formatter (the other is simpleDateFormatter)

struts.ui.html5.constraints

false

Whether the html5 theme emits constraint attributes derived from validators

struts.ui.checkbox.hiddenPrefix

__checkbox_

Prefix of the hidden field the checkbox tag adds

struts.csp.report.maxSize

8192

Largest CSP violation report accepted

struts.webjars.enabled

true

Serve WebJar assets under the static path; struts.webjars.allowlist restricts the names

The constants are read when the container is built, so changing one means changing the configuration and reloading it. The complete list is the file itself, and StrutsConstants documents each name in Javadoc.

Constants That Choose a Bean

The second role of a constant is selection. When several beans of one type are registered under different names, a constant holds the name of the one to use. The comment in default.properties shows the idea with struts.objectFactory, whose short name spring selects the Spring plugin’s object factory (see Spring Integration); in Struts 7.4.0 struts.date.formatter, struts.url.encoder and struts.proxyService work the same way, and their defaults in the table above are bean names (dateTimeFormatter, strutsUrlEncoder, struts). A bean registered under the name struts is the framework’s default for its type (Bean Configuration).

default.properties and struts.properties

struts.properties, the Legacy File

struts.properties is a plain properties file on the class path (normally WEB-INF/classes), one constant per line. The constant documentation keeps it "for backward-compatibility with WebWork", and it is loaded by the 7.4.0 Dispatcher (step 4 of Load Order: Who Wins); in the Bookshelf run it overrode struts.xml and was overridden by a filter init-param.

struts.properties in the Bookshelf (observed to override struts.xml)
bookshelf.order=struts.properties
bookshelf.pageSize=50

Prefer <constant> in XML for new code: the DTD checks the structure, the file is versioned with the rest of the configuration, and the load order is easier to reason about. The books of the Struts 2.0 era configure almost everything here (Where the Books Are Outdated), and every setting in this form maps one to one onto a <constant>. Struts also reads extra properties files named by struts.custom.properties, and extra resource bundles named by struts.custom.i18n.resources.

default.properties: Not an Application File

The documentation page default.properties says that "to change any of these properties, specify the property key and value in an default.properties file", located anywhere on the class path, and the development-mode page says devMode "can be set to true or false in default.properties". That does not match 7.4.0. The framework reads exactly one resource of that name, org/apache/struts2/default.properties inside struts2-core.jar, as step 1 of the load order, and an application file called default.properties is never read. In the Bookshelf, a WEB-INF/classes/default.properties with bookshelf.pageSize=99 changed nothing, while the same line in struts.properties took effect (row four and five of the table in Load Order: Who Wins, observed). Read default.properties as the reference list of names and defaults, and override in struts.xml, struts.properties or an init-param.

struts-default.xml

The framework’s own configuration is the file struts-default.xml in the core JAR. It is not copied into the application: it is loaded first by the Dispatcher (step 2) and it supplies

  • <include file="struts-excluded-classes.xml"/>, which sets struts.excludedClasses and the related excluded-package constants (the OGNL block list);

  • <include file="struts-beans.xml"/>, which registers the framework beans, each under the name struts (ObjectFactory, ResultFactory, ActionFactory, ConverterFactory, InterceptorFactory, ValidatorFactory, UnknownHandlerFactory, ActionProxyFactory, the FileManager and its factory) and a few others;

  • one abstract package, struts-default, holding the <result-types> (chain, dispatcher as the default, freemarker, httpheader, redirect, redirectAction, stream, plainText, postback), every named <interceptor>, the interceptor stacks (defaultStack among them) and the default interceptor reference.

Your packages extends="struts-default" to inherit all of that. The page that shows the whole file also says how to leave it out: "see the struts.configuration.files setting in default.properties". That name does not exist in 7.4.0: it is not in default.properties, not in StrutsConstants, and not read by the Dispatcher. The working mechanism is the config filter init-param, a comma-separated list of XML files that replaces the default list struts-default.xml,struts-plugin.xml,struts.xml.

Replacing the list of files that are loaded (web.xml init-param config)
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
    <init-param>
        <param-name>config</param-name>
        <param-value>struts-default.xml,struts.xml</param-value>
    </init-param>
</filter>

With that list the Bookshelf’s /books answered 200 and the action of the plugin JAR (/plugin/ping, which answered 204 before) answered 404, because struts-plugin.xml was no longer in the list; with only struts-default.xml in the list, /books answered 404 too (observed). A file whose name does not end in .xml is rejected with Invalid configuration file name. struts-deferred.xml is always loaded afterwards.

Bean Configuration

Struts has its own small dependency injection container (the documentation compares it with Google Guice). It builds the key framework objects, which is how any part of the framework can be replaced, extended or removed, and how plugins add Spring or SiteMesh support. Most applications never touch it, but <bean> is also the way to register your own service so the framework can inject it into actions (see Dependency Injection and CDI).

Table 7. Attributes of the bean element
Attribute Required Description

class

yes

The class to instantiate or manipulate

type

no

The primary interface the class implements; the container selects among beans of one type by name

name

no

A name unique among beans of the same type; the framework default is the name struts

scope

no

default, singleton, request, session or thread

static

no

Inject static methods and fields instead of creating instances (do not combine with type)

optional

no

Whether the bean is optional

A bean is used in one of two ways: created by the container and injected into other objects (usually with type), or, with static="true", only having values injected into its static members so that objects the container does not create can still receive constants.

The Bookshelf registers two implementations of a catalogue interface and picks one by name.

A bean type with two named implementations
package org.example.bookshelf;

import java.util.List;

public interface Catalog {
    List<String> titles();
}

public class InMemoryCatalog implements Catalog {
    public List<String> titles() { return List.of("Neuromancer", "Dune"); }
}

public class SortedCatalog implements Catalog {
    public List<String> titles() {
        return new InMemoryCatalog().titles().stream().sorted().toList();
    }
}
<bean type="org.example.bookshelf.Catalog" name="inMemory" class="org.example.bookshelf.InMemoryCatalog"/>
<bean type="org.example.bookshelf.Catalog" name="sorted" class="org.example.bookshelf.SortedCatalog"/>

ConfigAction (above) asks for the sorted one with @Inject("sorted"), and the response confirms the choice.

$ curl -s http://localhost:18080/g8conf/config
devMode=false
pageSize=50
order=struts.properties
titles=[Dune, Neuromancer]

titles is sorted, so the container injected SortedCatalog, and the other values are the effective constants of the fourth step of Load Order: Who Wins (this run used the WAR with its struts.properties).

The injected field can be private. A constructor with parameters is not supported for an action: the first attempt to inject the same four values through an @Inject constructor made the whole application fail to start with Unable to load configuration and Caused by: Action class [org.example.bookshelf.ConfigAction] does not have a public no-arg constructor (observed); every URL of the context then answered 404.

The documentation’s own bean example registers com.opensymphony.xwork2.ObjectFactory. In Struts 7 the package was renamed (the 6 to 7 migration guide: "the com.opensymphony.xwork2 package has been renamed/migrated to org.apache.struts2`"), and in 7.4.0 the type is `org.apache.struts2.ObjectFactory; use the org.apache.struts2 names in every type= attribute.

Unknown Handlers

An UnknownHandler is a bean the framework calls when an action, a result or a method is not found. Several can be stacked with <unknown-handler-stack>, which must be the last child of <struts>, and they are tried in the order listed until one handles the request. The default manager is DefaultUnknownHandlerManager, replaceable with the constant struts.unknownHandlerManager. The documentation page still shows the old com.opensymphony.xwork2.UnknownHandler name; in 7.4.0 it is org.apache.struts2.UnknownHandler.

An unknown handler that gives the Bookshelf a /bestsellers page without declaring an action
public class ShelfUnknownHandler implements UnknownHandler {
    @Override
    public ActionConfig handleUnknownAction(String namespace, String actionName) {
        if (!"/".equals(namespace) || !"bestsellers".equals(actionName)) {
            return null; // not ours: let the next handler, or the 404, take over
        }
        return new ActionConfig.Builder("default", actionName, BookAction.class.getName())
                .addResultConfig(new ResultConfig.Builder("success",
                        "org.apache.struts2.result.ServletDispatcherResult")
                        .addParam("location", "/WEB-INF/views/books.jsp").build())
                .build();
    }

    @Override
    public Result handleUnknownResult(ActionContext ctx, String actionName, ActionConfig cfg, String code) {
        return null;
    }

    @Override
    public Object handleUnknownActionMethod(Object action, String methodName) {
        return null;
    }
}
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:18080/g8conf/bestsellers
200
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:18080/g8conf/nothere
404

The 404 body names the problem exactly: There is no Action mapped for namespace [/] and action name [nothere] associated with context path [/g8conf]. For a simple fallback page, a default-action-ref in the package is the lighter tool (see Packages and Namespaces); an unknown handler is for logic, such as mapping legacy URLs.

Plugin Configuration: struts-plugin.xml and struts-deferred.xml

A plugin is a JAR on the class path. If it contains struts-plugin.xml at its root, Struts loads it with the same parser and format as struts.xml, before struts.xml, so a plugin can add packages, results, interceptors and actions, override constants and register beans. A struts-deferred.xml is loaded after everything else, which is where a plugin declares an extension point that must see the final configuration. Because struts.xml comes later, the application always has the last word over the plugin’s constants.

A tiny Bookshelf plugin JAR: struts-plugin.xml at the root
<struts>
    <constant name="bookshelf.order" value="plugin"/>
    <constant name="bookshelf.pageSize" value="5"/>
    <package name="bookshelf-plugin" extends="struts-default" namespace="/plugin">
        <action name="ping">
            <result type="httpheader"><param name="status">204</param></result>
        </action>
    </package>
</struts>

Put into WEB-INF/lib/bookshelf-plugin.jar, this file added the /plugin/ping action with no change to the application’s struts.xml (curl answered 204), and its constants lost to the application’s (first row of the table in Load Order: Who Wins). The same mechanism is what the Convention, JSON, Spring, Tiles and other official plugins use; the developer guide to writing a plugin, extension points and the plugin registry is the "Plugins Architecture" page.

Reloading and Development Mode

Two constants decide whether configuration changes need a restart.

Table 8. The reload switches
Constant Effect

struts.configuration.xml.reload

Re-reads the XML configuration (and validation files) when a file the framework tracks changes; "not recommended for production use"

struts.i18n.reload

Reloads resource bundles on every request

struts.devMode

Turns both of the above on if they are not set explicitly, and raises ignorable problems (an unknown request parameter, for instance) to errors

The development-mode page lists the same three effects, and warns that the option "can expose sensitive data of your application". In 7.4.0 the StrutsBeanSelectionProvider does the switching: when devMode is true it sets struts.i18n.reload, struts.configuration.xml.reload and a zero FreeMarker cache delay only if the application has not set them. The reload is conditional: the ConfigurationManager asks every provider needsReload() and rebuilds only when a tracked file changed, so the cost is a file-timestamp check per request, not a rebuild.

The Bookshelf sets struts.configuration.xml.reload to true explicitly, and the reload behaviour was verified in the exploded WAR without restarting Tomcat.

$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:18080/g8conf/admin/export
404
$ # add <action name="export" .../> to WEB-INF/classes/bookshelf/struts-admin.xml
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:18080/g8conf/admin/export
200

The same run showed three limits.

  • A new file matching the include pattern is not picked up until a tracked file changes (Include Configuration).

  • A reload with an error in a file does not give a clear page. With struts-defualt in an included package, the request failed with HTTP 500 and the unhelpful NoSuchMethodException: ...BookAction.execute() rather than the configuration error, and the application recovered as soon as the typo was fixed (observed). The clear message appears when the application starts: Dispatcher initialization failed with Caused by: Parent package is not defined: struts-defualt, and every URL of the context answers 404.

  • After a configuration reload, the framework’s own static files stopped being served: before the reload GET /g8conf/static/utils.js answered 200, after touching struts.xml it answered 500 with Cannot invoke "java.util.List.iterator()" because "this.pathPrefixes" is null until the application was redeployed (observed, Struts 7.4.0 with struts.configuration.xml.reload=true). Treat reload as a development aid and test static assets after it.

Production: leave both reload constants off, devMode false. The ${env.STRUTS_DEV_MODE:false} form of Value Substitution keeps that safe by default.

The Filter and Its Init-Params in web.xml

The documentation names two ways to enable Struts: the single StrutsPrepareAndExecuteFilter, or the pair StrutsPrepareFilter and StrutsExecuteFilter when another filter (SiteMesh) has to sit between the prepare and execute phases. Both live in org.apache.struts2.dispatcher.filter. The Bookshelf’s web.xml is the Servlet 6.1 form (version="6.1", https://jakarta.ee/xml/ns/jakartaee).

The Bookshelf web.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>

The init-params the 7.4.0 Dispatcher reads, besides every constant, are these.

Table 9. Filter init-params with a special meaning
Init-param Meaning

config

Comma-separated XML files to load instead of struts-default.xml,struts-plugin.xml,struts.xml (struts-default.xml)

configProviders

Comma-separated ConfigurationProvider class names, for configuration built in code

javaConfigClasses

Comma-separated StrutsJavaConfiguration classes (beans, constants and an unknown-handler stack in Java); present in the 7.4.0 source, described by Javadoc but not by the documentation pages reviewed, and not exercised here

packages

Extra packages searched for static content

struts.fileManager, struts.fileManagerFactory

A custom FileManager or factory, which must be registered at this early point

Mapping the filter to /* is "the preferred way". With a narrower mapping such as *.action the filter no longer sees the requests for its static files and a second <filter-mapping> for /static/* is needed, and the constant struts.action.excludePattern (a comma-separated list of regular expressions, for example .*unfiltered.*,.*\.nofilter) makes the filter pass chosen URLs through untouched. A <taglib> entry in web.xml for the Struts tags is "neither required nor recommended": the TLD in the core JAR is discovered by the container.

Static content has a path mismatch in the documentation. The Configuration Files page says requests that start with /struts/ are served from org.apache.struts2.static and template; in 7.4.0 the prefix is struts.ui.staticContentPath, default /static, and GET /g8conf/struts/utils.js answered 404 while /g8conf/static/utils.js answered 200 (observed).

From struts-config.xml to struts.xml

Applications and books of the Struts 1 era configure with struts-config.xml, a *.do servlet mapping and a plug-in element. The elements have direct, simpler equivalents in Struts 7. Struts 1 reached end of life on 2013-04-05 (see introduction and history), and its documentation is no longer online.

Table 10. Struts 1 configuration and its Struts 7 equivalent
Struts 1 (struts-config.xml, web.xml) Struts 7

ActionServlet, *.do mapping, init-param config

One filter on /*; the default extension is action,, and *.do does not match (verified: /config.do answered 404, /config.action and /config answered 200)

<action-mappings> / <action path=...>

<action name=...> in a <package>

<form-beans> (ActionForm)

No form class: properties on the action, or a model object with ModelDriven (see ModelDriven, Preparable & CRUD)

<global-forwards>, <forward>

<global-results> and <result> (see Results & Result Types)

<global-exceptions>

<global-exception-mappings> and <exception-mapping> (see Exception Handling and Global Results)

<controller>

Constants and interceptors

<message-resources>

Resource bundles; struts.custom.i18n.resources (see Localization and i18n)

<plug-in>

A plugin JAR with struts-plugin.xml, or a <bean> (Plugin Configuration: struts-plugin.xml and struts-deferred.xml)

<data-sources>

A container-managed DataSource (JNDI); no framework element

Modules and SwitchAction

Packages and namespaces (see Packages and Namespaces)

Struts Blank application

No archetype for Struts 7: the maven archetypes stop at 6.0.0 (see Struts Getting Started)

The two configuration systems share nothing at run time: Struts 1 is not part of Struts 7, and the migration guides are the reference.

Pitfalls

Table 11. Configuration pitfalls, with the symptom and the fix (rows marked "observed" were reproduced on Tomcat 11.0.26 with Struts 7.4.0)
Symptom or error Cause Fix

The whole context answers 404 and the log shows Dispatcher initialization failed (observed)

A configuration error at start-up: Parent package is not defined: struts-defualt or a wrong class name

Read the first Caused by line in the log; fix the file named in it

Unable to load configuration with does not have a public no-arg constructor (observed)

An action class with only a constructor with parameters

Give the action a public no-arg constructor and inject with fields or setters

A value in struts.xml seems to be ignored

The same constant is set in struts.properties or as a filter init-param, which are read later (observed)

Search the application for the name; keep each constant in one place

default.properties under WEB-INF/classes has no effect (observed)

Only the core JAR’s copy is read

Use struts.xml, struts.properties or an init-param

<constant name="struts.devmod" ...> does nothing and nothing is logged (observed)

Constants are matched by name; an unknown name is accepted

Copy names from default.properties or StrutsConstants

<include> of a module that does not exist gives no error

A missing included file is skipped, logged at debug level only

Request an action of every module in a smoke test

A new file matching an include pattern is not loaded after a reload (observed)

The reload is triggered by a change in an already tracked file

Touch a tracked file, or restart

HTTP 500 with a misleading NoSuchMethodException after editing an XML file while reload is on (observed)

The reload failed because of an error in the edited file

Fix the file; check the log for the real configuration error

Struts static files (/static/utils.js) answer 500 after a configuration reload (observed)

Reload in 7.4.0 left the static content loader without its path prefixes

Dev only: redeploy; keep reload off in production

SAXParseException: "The content of element type "action" must match ..."

Elements in the wrong order for the DTD (result, interceptor-ref, exception-mapping, then allowed-methods; unknown-handler-stack last)

Follow the DTD content model; keep the DOCTYPE 6.0

*.do request answers 404 (observed)

struts.action.extension is action,, in 7.4.0

Link without an extension or with .action; or set the constant if URLs must stay

devMode is on in production

The constant was left true or a struts.properties from a developer machine was packaged

${env.STRUTS_DEV_MODE:false}, and check the effective value with an injected constant

The OGNL, allowlist or annotation constants were switched off "to make it work"

Migration from an older application

Fix the cause (annotate the setter, add the class to the allowlist); keep the secure defaults of The Constants Worth Knowing

struts.configuration.files or /struts/ static URLs from the documentation do not work

Stale documentation (struts-default.xml, The Filter and Its Init-Params in web.xml)

Use the config init-param and /static/

What Changed Recently

Table 12. Changes that affect configuration, with the version
Version Change

2.5.6

${env.NAME:default} and system-property substitution in <constant> values (the Constant Configuration page)

6.0

The Velocity support moved out of the core into a plugin; the DTD is struts-6.0.dtd and still is for 7.x

7.0

The com.opensymphony.xwork2 package was renamed to org.apache.struts2: every type= and class name in struts.xml, in beans and in configuration providers changes. Stricter defaults: exact namespace matches (struts.mapper.alwaysSelectFullNamespace=true, struts.actionConfig.fallbackToEmptyNamespace=false), no OGNL static field access, only standard maps from OGNL, a shorter OGNL expression limit, Java 17 and Servlet 6 required

7.2.0 and later

Performance constants (struts.freemarker.whitespaceStripping, struts.tag.compress.enabled, .maxSize, .log.maxLength) in the Constant Configuration page

7.4.0

Present in default.properties: struts.locale.validateRequestLocale, struts.chaining.requireAnnotations, struts.validators.skipValidatorsOnConversionError, struts.ui.html5.constraints, struts.ui.checkbox.hiddenPrefix, the WebJars switches and struts.csp.report.maxSize. The 7.4.0 build is made against the Jakarta EE 10 APIs by default and runs on Tomcat 11

The 6 to 7 migration guide says the OGNL limit "has been reduced from 200 to 150", but the 7.4.0 default.properties has struts.ognl.expressionMaxLength=256 and the security guide also says 256; this page uses 256 because that is what the JAR contains.

Where the Books Are Outdated

Struts in Action (Manning, 2002) configures Struts 1 with struts-config.xml; its chapters on first application and configuration are covered by the table in From struts-config.xml to struts.xml. Struts 2 in Action (Manning, 2008, both listed in the section bibliography) is the Struts 2.0 book and its chapters on plugins and configuration still describe the right ideas and the wrong details.

Table 13. Configuration material in the books and its Struts 7.4 equivalent
The book’s era Today (Struts 7.4)

struts-config.xml, *.do

struts.xml, extensionless or .action URLs (From struts-config.xml to struts.xml)

struts.properties as the normal place for settings

<constant> in struts.xml; struts.properties still loads (struts.properties, the Legacy File)

A DOCTYPE for DTD 2.0 or 2.1

The 6.0 DTD (struts.xml); there is no 7.0 DTD

com.opensymphony.xwork2.* class names in type= and class=

org.apache.struts2.* since 7.0

The Struts 2 plugin chapter’s list (SiteMesh, Tiles, JFreeChart, and others)

17 plugins are published at 7.4.0; several of the old ones are deprecated or removed (see Other Plugins)

Settings for features that were later removed (struts.devMode is unchanged; the Codebehind and Struts 1 plugins are gone)

Check every name in default.properties

A loose default for every security-sensitive constant

Secure defaults in 7.x (The Constants Worth Knowing)

Reloading configuration "just by editing"

struts.configuration.xml.reload or devMode, development only (Reloading and Development Mode)

The official pages need the same care: the pages quoted above still show com.opensymphony.xwork2 names, the Java EE 3.1 namespace in the web.xml examples, struts.configuration.files, the /struts/ static prefix and the "default.properties in your application" advice, and none of those work as written in 7.4.0.