Struts Getting Started

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Struts 7.4.0 (released 2026-10-02), with Jakarta Servlet 6.1, Jakarta Pages 4.0 and Jakarta Expression Language 6.0 as provided by Apache Tomcat 11.0.26, on Java 21 (Struts 7 needs Java 17 or later and Servlet API 6 or later). Everything below was verified on 2026-10-08: the facts against the Struts documentation, default.properties, struts-default.xml and the 7.4.0 artifacts on Maven Central, 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 that run produced. The page builds bookshelf-struts, the second of the two Bookshelf applications of this section (the first, bookshelf-jsp, is built in Getting Started, which also installs the JDK and Tomcat). It follows the two official tutorials "How To Create A Struts Web Application" and "Hello World Using Struts" and adapts them to Struts 7: the project is created by hand because there is no 7.x Maven archetype, struts2-bom manages the versions, and the Hello World model class needs one line of allowlist configuration that the tutorial text does not mention.

What You Build

Struts 7 is an action-based MVC framework that installs itself in a web application as a single servlet filter. A request is matched to an action (a Java class) by name, the action does its work, returns a string, and that string selects a result (usually a JSP). Four things are needed to bring that to life:

  • the Struts jars and their dependencies on the class path (Maven and the BOM do that),

  • the filter declared in web.xml, so that every request passes through Struts,

  • struts.xml on the class path, the file that maps action names to classes and result names to views,

  • the action classes and the view pages themselves.

The Bookshelf of this page is small on purpose: a welcome page, a "hello" action that shows a message produced by a model object, and a "books" action that lists the catalogue and accepts an optional author filter. Later pages of Part II grow it into actions with forms, validation, interceptors and CRUD.

Table 1. The software baseline of this page (verified 2026-10-08)
Component Version Notes

Apache Struts

7.4.0 (struts2-core, struts2-bom)

Current line. 6.12.0 is also maintained (same release day); 2.5.x reached end of life on 2023-10-30

Java

21 (17 is the minimum)

maven.compiler.release 21 in the examples; Struts itself is compiled for release 17

Servlet container

Tomcat 11.0.26

Servlet 6.1, Pages 4.0, EL 6.0. Struts 7 needs Servlet 6 or later, which Tomcat 10.1 (Servlet 6.0) provides and Tomcat 9 (Servlet 4.0) does not

OGNL

3.4.12

Pulled in by struts2-core 7.4.0

FreeMarker

2.3.35

Used by the Struts tag templates, pulled in by struts2-core

Log4j 2

2.26.1

log4j-api and log4j-core, managed by log4j-bom

Maven

3.9.16

maven-war-plugin 3.4.0

flowchart LR B["Browser: GET /books?author=Frank Herbert"] --> F["Servlet filter: StrutsPrepareAndExecuteFilter"] F -->|"action name books"| X["struts.xml: action books"] X --> A["BookListAction.execute returns success"] A -->|"result success"| J["/WEB-INF/views/books.jsp"] J --> R["HTML response"] W["web.xml declares the filter"] -.-> F P["pom.xml: struts2-bom and struts2-core"] -.-> F

The figure shows the whole idea of this page. The dotted lines are the two files that wire Struts into the web application, pom.xml and web.xml; the solid line is what happens to each request, and struts.xml is the table the filter consults.

Before You Start

The tutorial assumes a working knowledge of Java, filters, JSP and tag libraries, JavaBeans, HTML and HTTP, web containers and XML; the Struts site collects the background in its Key Technologies Primer, and this section covers the same ground in Servlet Fundamentals and in Part I. To follow along, the machine needs only what the section Getting Started page installs: a JDK 21, Maven and Tomcat 11 (see Installing Tomcat 11). The commands below assume Tomcat is unpacked in $CATALINA_HOME and listens on port 8080.

A Struts application is a normal WAR. Everything that makes it a Struts application is configuration that Struts reads at start-up, so a mistake in web.xml or struts.xml shows up when the web application is deployed, not when the first request arrives. That is a useful property when debugging, and the later sections show the messages.

Why the Project Is Created by Hand

Struts publishes Maven archetypes (struts2-archetype-blank, struts2-archetype-convention and others), and mvn archetype:generate -Dfilter=org.apache:struts lists them. The archetypes, however, stop at version 6.0.0: the metadata of struts2-archetype-blank on Maven Central names 6.0.0 as both latest and release (last updated 2023-03-15), while struts2-core is at 7.4.0. An archetype-generated project therefore pins a Struts 6.0 baseline and the javax-era conventions of its time, and it still has to be upgraded by hand. Building the project from an empty Maven WAR is only seven small steps, takes less than ten minutes, and teaches where each piece lives; the official tutorial does the same.

The Struts examples repository (apache/struts-examples) holds the finished projects of every Getting Started tutorial. The two used here are basic-struts (the empty application of "How To Create A Struts Web Application") and helloworld (its extension from "Hello World Using Struts"); the other tutorials of the series, from "Using Tags" to "JasperReports", are projects of the same repository and are used by the pages of Part II that cover their topic.

The Project Layout

Maven’s standard WAR layout is all Struts needs. struts.xml goes in src/main/resources, so that it lands in WEB-INF/classes and is found on the class path; the views go under WEB-INF so that no one can request them directly.

The Bookshelf (Struts) project, as built in this page
bookshelf-struts/
|-- pom.xml
`-- src/main/
    |-- java/org/example/bookshelf/
    |   |-- action/HelloAction.java
    |   |-- action/BookListAction.java
    |   |-- model/MessageStore.java
    |   `-- model/Book.java
    |-- resources/
    |   |-- struts.xml
    |   `-- log4j2.xml
    `-- webapp/
        |-- index.jsp
        `-- WEB-INF/
            |-- web.xml
            `-- views/
                |-- hello.jsp
                `-- books.jsp
flowchart TB subgraph build["Build time: Maven"] POM["pom.xml"] --> WAR["bookshelf-struts.war"] JAVA["src/main/java: actions and model"] --> CLS["WEB-INF/classes"] RES["src/main/resources: struts.xml and log4j2.xml"] --> CLS WEBAPP["src/main/webapp: index.jsp, web.xml, views"] --> WAR CLS --> WAR BOM["struts2-bom, log4j-bom"] --> LIB["WEB-INF/lib: struts2-core, OGNL, FreeMarker, Log4j"] LIB --> WAR end subgraph run["Run time: Tomcat 11"] WAR --> CTX["Context /bookshelf-struts"] CTX --> FILTER["Filter from web.xml"] FILTER --> CONF["Reads struts-default.xml, then struts.xml"] CONF --> ACT["Action mappings: index, hello, books"] end

The second figure follows the files from the build to the running container: what ends up in WEB-INF/classes and WEB-INF/lib, and what Struts reads when the filter starts. The WAR built for this page contains four classes, struts.xml and log4j2.xml in WEB-INF/classes, the two views and web.xml under WEB-INF, index.jsp at the root and fifteen jars in WEB-INF/lib (struts2-core 7.4.0 with ognl 3.4.12, freemarker 2.3.35, javassist, caffeine, commons-lang3, commons-text, commons-io, commons-fileupload2 2.0.0-M5 and webjars-locator-lite, plus log4j-api and log4j-core 2.26.1).

Step 1 — The pom.xml and the Struts BOM

The tutorial adds struts2-core to the dependencies and, optionally, uses the bill of materials so that no Struts or Log4j module needs a version of its own. The BOM is the better way for a new project, because every Struts module (the plugins that later pages add) is then managed to the same version. The Jakarta Servlet and Pages APIs are provided, since Tomcat supplies them.

pom.xml of bookshelf-struts
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>bookshelf-struts</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <struts2.version>7.4.0</struts2.version>
        <log4j2.version>2.26.1</log4j2.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.struts</groupId>
                <artifactId>struts2-bom</artifactId>
                <version>${struts2.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-bom</artifactId>
                <version>${log4j2.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.apache.struts</groupId>
            <artifactId>struts2-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.servlet.jsp</groupId>
            <artifactId>jakarta.servlet.jsp-api</artifactId>
            <version>4.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>bookshelf-struts</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Four details of this file deserve a comment.

  • struts2-core is the only Struts dependency an application needs; it brings OGNL, FreeMarker and the other libraries listed in the previous section as transitive dependencies. Every other struts2-* artifact is a plugin that is added only when a feature needs it (Convention, JSON, REST, Spring, Tiles and others).

  • struts2-bom 7.4.0 lists 24 entries, but four of them — struts2-embeddedjsp-plugin, struts2-gxp-plugin, struts2-osgi-plugin and struts2-plexus-plugin — have no 7.4.0 artifact on Maven Central, and neither have the portlet, DWR, OVal, SiteMesh and pell-multipart plugins. A dependency on one of these without a version resolves to a missing artifact. The plugins that are published at 7.4.0 are async, bean-validation, cdi, config-browser, convention, jasperreports, jasperreports7, javatemplates, jfreechart, json, junit, rest, spring, testng, tiles, velocity and xslt.

  • Struts 7.4.0 is built against the Jakarta EE 10 APIs (Servlet 6.0, Pages 3.1) by default; only a jakartaee11 build profile of Struts itself targets EE 11. The WAR of this page runs unchanged on Tomcat 11, whose Servlet 6.1 is backwards compatible.

  • The tutorial’s pom.xml also configures the Jetty Maven plugin so that mvn jetty:run serves the project. This page deploys to the Tomcat of the previous page of the section instead; the Jetty 12.1 plugin (jetty-ee11-maven-plugin) is covered in Jetty 12.1 and GlassFish 8 as Alternatives.

Logging

The tutorial uses Log4j 2 with a console appender and DEBUG level for the org.apache.struts2 logger, so that "what is happening under the hood" is visible. Put the file in src/main/resources and keep the root logger at warn.

src/main/resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="org.apache.struts2" level="debug"/>
        <Root level="warn">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>

On Tomcat the console appender writes into logs/catalina.out. The debug level is for development only: at debug Struts logs every OGNL member access check, and one request produces dozens of lines. The Struts logging page covers the production configuration; Part II returns to it in the page on debugging and logging.

Step 2 — Putting the Filter in web.xml

Struts 2 and later are filter-based; the ActionServlet of Struts 1 no longer exists. The filter org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter is the single entry point of the framework: when the web application starts, its init method builds the configuration and the dispatcher; for each request its doFilter decides whether the request is an action request. If it is, the filter runs the action and the result and does not call the rest of the filter chain; if it is not, the request is passed on to the next filter and eventually to the container’s default servlet or the JSP servlet.

src/main/webapp/WEB-INF/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">
    <display-name>Bookshelf (Struts)</display-name>

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

    <welcome-file-list>
        <welcome-file>/index</welcome-file>
    </welcome-file-list>
</web-app>

The deployment descriptor uses the Jakarta EE 11 schema (version="6.1"); the tutorial’s file declares version 6.0 and the web-app_6_0.xsd schema, and either is accepted by Tomcat 11. The welcome file /index is explained in the section on the index page below. What the descriptor must contain, and what it may contain, is summarised in the next table.

Table 2. What the official web.xml documentation says about the filter
Item Detail

StrutsPrepareAndExecuteFilter

The recommended single filter. It prepares the request (character encoding, ActionContext, wrapping) and executes the action in one step

StrutsPrepareFilter plus StrutsExecuteFilter

The split form, for integrating other filters between the two phases (the documentation points to the SiteMesh plugin as the case that needs it)

/* as the url-pattern

The preferred mapping: Struts then sees every request, including the ones for the static content it serves from its jars

*.action as the url-pattern

Allowed; only requests ending in .action reach Struts. Static content served by Struts then needs an additional mapping such as /static/*

struts.action.excludePattern

A comma-separated list of regular expressions of request URLs that the filter passes through untouched, set as a constant in struts.xml

init-param named struts.fileManager or struts.fileManagerFactory

Registers a custom file manager early, for application servers with a special file system (the docs mention VFS in JBoss)

init-param named configProviders

A comma-separated list of ConfigurationProvider class names that configure the application programmatically

A taglib element

Neither required nor recommended: the Struts tag library descriptor is inside struts2-core and the container discovers it automatically

A mapping other than /* is where most first-time problems start. With *.action the filter ignores a request for /books, and the extension rules of the next section decide whether the action is still reachable. Keeping /* and excluding the paths that Struts must not touch, with struts.action.excludePattern, is the safer pair. The servlet-level meaning of the filter, its position in the chain and its dispatcher types are explained in Servlet Fundamentals, Filters, and the architecture and request lifecycle page of Part II follows the request through the filter and beyond.

Step 3 — struts.xml

struts.xml is the core configuration file; it must be on the class path of the web application, usually in WEB-INF/classes, which is where Maven puts src/main/resources. The file may include other files, and a JAR can carry its own struts-plugin.xml that is plugged in automatically; the page on configuration files and constants covers load order, include, bean and the constants in detail. For the first application a minimal file is enough.

src/main/resources/struts.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>
    <constant name="struts.devMode" value="${env.STRUTS_DEV_MODE:false}"/>
    <constant name="struts.allowlist.packageNames" value="org.example.bookshelf.model"/>

    <package name="bookshelf" extends="struts-default">
        <default-action-ref name="index"/>

        <action name="index">
            <result>/index.jsp</result>
        </action>
        <action name="hello" class="org.example.bookshelf.action.HelloAction">
            <result>/WEB-INF/views/hello.jsp</result>
        </action>
        <action name="books" class="org.example.bookshelf.action.BookListAction">
            <result>/WEB-INF/views/books.jsp</result>
        </action>
    </package>
</struts>

Read the file from the top.

Table 3. The elements of the first struts.xml
Element Meaning

DOCTYPE with the 6.0 DTD

Struts validates the file against a DTD, and a Struts 7 file keeps the 6.0 one, as the tutorial does (why 6.0). The struts2-core 7.4.0 jar also carries a struts-6.5.dtd, which the struts-examples projects declare, and the parser resolves it from the jar (checked: the application starts with either declaration)

constant struts.devMode

Development mode, explained in its own section below. It is false by default and must be off in production. The value ${env.STRUTS_DEV_MODE:false} reads the environment variable STRUTS_DEV_MODE when the configuration loads and falls back to false, so the same WAR is safe in production and a developer turns the mode on locally (Step 5; the syntax is in Configuration: struts.xml & Constants)

constant struts.allowlist.packageNames

Adds the model package to the OGNL allowlist, so that JSP expressions may call getters of the model classes. The Hello World example needs it (see the first action)

package

A named group of actions that shares a namespace, interceptors and results. Only name is required. extends="struts-default" inherits everything defined in the framework’s own struts-default.xml

default-action-ref

The action that answers requests which match no action name; without it such requests are an error

action

Maps a name (the part of the URL without the extension) to a class. Only name is required; when class is absent the default class is used

result

Maps a result name to a view. With no name the result is named success, and with no type it is of type dispatcher, which forwards to a JSP or other server resource

The Intelligent Defaults of struts-default

Very little of the application’s behaviour is written in the file above, because the package extends struts-default, the base package defined in struts-default.xml inside struts2-core. The file is included automatically before struts.xml. The defaults that the first application relies on were read from the 7.4.0 jar:

  • default-class-ref is org.apache.struts2.ActionSupport; an <action name="index"> without class runs ActionSupport.execute(), which returns success. (The Action Configuration page still names com.opensymphony.xwork2.ActionSupport for this default; that package name disappeared in Struts 7.)

  • the default result type is dispatcher (ServletDispatcherResult), and a result without a name is success;

  • the default interceptor stack is defaultStack, which in 7.4.0 runs, in order, exception, alias, servletConfig, httpMethod, i18n, csp, prepare, chain, scopedModelDriven, modelDriven, actionFileUpload, checkbox, datetime, multiselect, staticParams, actionMappingParams, params, conversionError, coep, coop, fetchMetadata, validation, workflow and debugging. The params interceptor copies request parameters onto the action; validation and workflow run validators and return input when they fail.

That is why the action index needs no class and the action hello needs no interceptor configuration: the framework supplies a convention for every omitted piece. The names struts-default, defaultStack and the defaults above are reused by every later page of Part II.

Step 4 — The First Action and Its JSP

The Hello World tutorial adds three pieces: a model class that holds the message, an action class that creates the model and returns SUCCESS, and a JSP that displays the message. The Bookshelf version keeps the same structure, with different names and the package layout shown above.

src/main/java/org/example/bookshelf/model/MessageStore.java
package org.example.bookshelf.model;

public class MessageStore {

    private final String message;

    public MessageStore() {
        message = "Welcome to the Bookshelf";
    }

    public String getMessage() {
        return message;
    }
}
src/main/java/org/example/bookshelf/action/HelloAction.java
package org.example.bookshelf.action;

import org.apache.struts2.ActionSupport;
import org.example.bookshelf.model.MessageStore;

public class HelloAction extends ActionSupport {

    private MessageStore messageStore;

    @Override
    public String execute() {
        messageStore = new MessageStore();
        return SUCCESS;
    }

    public MessageStore getMessageStore() {
        return messageStore;
    }
}

The action extends org.apache.struts2.ActionSupport, which implements Action and supplies the constants SUCCESS, INPUT, ERROR, NONE and LOGIN, plus validation, localization and message support that later pages use. Note the package: in Struts 7 the classes of the old com.opensymphony.xwork2 package were moved into org.apache.struts2 (with a few sub-packages, such as org.apache.struts2.interceptor.parameter for @StrutsParameter), and code written for Struts 2.5 or 6 that imports com.opensymphony.xwork2.ActionSupport no longer compiles. The framework creates a new instance of the class for each request, calls execute, and reads the string it returns. The getMessageStore getter is how the view reaches the model: the JavaBean convention, a public getX method for property x, is the contract between action and JSP.

src/main/webapp/WEB-INF/views/hello.jsp
<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello</title>
  </head>
  <body>
    <h2><s:property value="messageStore.message"/></h2>
  </body>
</html>

The taglib directive binds the prefix s to the Struts tag library, whose URI is /struts-tags. The tag s:property evaluates its value as an OGNL expression against the value stack, a stack of objects whose top is the action: messageStore.message calls getMessageStore() on the action and then getMessage() on the returned object. s:property escapes HTML by default.

The One Line the Tutorial Leaves Out

With the files above, but without the constant struts.allowlist.packageNames in struts.xml, the request to /hello returns status 200 and an empty heading: <h2></h2>. Nothing fails visibly. The cause is in the log, at WARN level:

WARN  ognl.SecurityMemberAccess - Declaring class [class org.example.bookshelf.model.MessageStore] of member type [public java.lang.String org.example.bookshelf.model.MessageStore.getMessage()] is not allowlisted! Add to 'struts.allowlist.classes' or 'struts.allowlist.packageNames' configuration.

Since Struts 6.4 (and by default from 7.0) the OGNL allowlist is on (struts.allowlist.enable=true): beyond the exclusion list of dangerous classes, the classes taking part in an OGNL expression must also be on a list of allowed classes. The framework’s own classes and the action classes defined in struts.xml are allowed automatically. A separate model class is not, because nothing in the configuration names it. The remedy is the constant of the example, which allows a package and its sub-packages (string prefix match); struts.allowlist.classes allows single classes. The struts-examples helloworld project does exactly this with struts.allowlist.packageNames for its model package, while the tutorial page that explains the project does not mention it, so a reader who types in the tutorial code gets the empty heading.

The books action of the next section returns List<Book> and renders titles without any allowlist entry, and a getter that returns a plain String needs none either; the allowlist bites when a JSP navigates from the action into a separate class of your own. The allowlist is a security feature and should stay enabled: it is one of the layers that limit what an injected OGNL expression can reach. The page on security hardening in Part II returns to the allowlist together with @StrutsParameter.

A Second Action: Listing and Filtering Books

The second action shows request parameters, which is where Struts 7 differs most from what older tutorials show. It lists the catalogue and takes an optional author parameter.

src/main/java/org/example/bookshelf/model/Book.java
package org.example.bookshelf.model;

public record Book(String isbn, String title, String author) {

    public String getIsbn() {
        return isbn;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }
}

The record carries explicit getX methods because the view reads it with bare names inside s:iterator (<s:property value="title"/>). A bare name is looked up on the value stack through JavaBean getters, and a record’s accessor title() is not one, so without the getters the list items would be empty; the allowlist entry alone does not change that. Qualified paths such as top.title would read the component without getters (Java Records in OGNL). A plain class with getters works identically.

src/main/java/org/example/bookshelf/action/BookListAction.java
package org.example.bookshelf.action;

import java.util.List;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import org.example.bookshelf.model.Book;

public class BookListAction extends ActionSupport {

    private static final List<Book> CATALOGUE = List.of(
            new Book("978-0-441-17271-9", "Dune", "Frank Herbert"),
            new Book("978-0-441-56956-4", "Neuromancer", "William Gibson"),
            new Book("978-0-593-13520-4", "Project Hail Mary", "Andy Weir"));

    private String author;
    private List<Book> books;

    @Override
    public String execute() {
        if (author == null || author.isBlank()) {
            books = CATALOGUE;
        } else {
            books = CATALOGUE.stream()
                    .filter(b -> b.author().equalsIgnoreCase(author))
                    .toList();
        }
        return SUCCESS;
    }

    public String getAuthor() {
        return author;
    }

    @StrutsParameter
    public void setAuthor(String author) {
        this.author = author;
    }

    public List<Book> getBooks() {
        return books;
    }
}

The params interceptor of the default stack copies request parameters onto the action by calling setters through OGNL. A parameter named author becomes setAuthor(...). That is exactly the surface through which remote-code-execution vulnerabilities reached Struts for years, so since Struts 6.4 (default in 7.0, struts.parameters.requireAnnotations=true) a setter that should receive request data must be annotated with @StrutsParameter; unannotated setters are not reachable from the request. The annotation has a depth element (default 0, a simple property) for nested objects and collections, which the page on parameter injection covers.

src/main/webapp/WEB-INF/views/books.jsp
<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Books</title>
  </head>
  <body>
    <h1>Books</h1>
    <ul>
      <s:iterator value="books">
        <li><s:property value="title"/> by <s:property value="author"/></li>
      </s:iterator>
    </ul>
    <p><a href="<s:url action='index'/>">Back</a></p>
  </body>
</html>

s:iterator pushes each element onto the value stack in turn, so inside the loop title resolves against the current Book first and the action second. This is the stack at work, and the reason the element’s properties need no prefix. The tag s:url builds the URL of an action by name; the next section shows what it produces.

The Welcome Page and the Action URL

src/main/webapp/index.jsp
<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Bookshelf (Struts) - Welcome</title>
  </head>
  <body>
    <h1>Bookshelf</h1>
    <p><a href="<s:url action='hello'/>">Say hello</a></p>
    <p><a href="<s:url action='books'/>">Browse the books</a></p>
  </body>
</html>

The page links to actions, not to JSP files. Linking to actions is the habit the Struts documentation recommends: the link does not know which page renders the answer, and the action can run before the page does. s:url takes the action name, adds the context path and the default extension, and, on the first request of a browser that has no session cookie yet, the container’s ;jsessionid suffix:

<p><a href="/bookshelf-struts/hello.action;jsessionid=AD464B2ABF837CF2560FC3206DDA27EF">Say hello</a></p>
<p><a href="/bookshelf-struts/books.action;jsessionid=AD464B2ABF837CF2560FC3206DDA27EF">Browse the books</a></p>

(The session identifier is the container’s URL rewriting, explained in Servlet Fundamentals, Sessions.)

Why the welcome file is /index: Struts serves index through the filter, and the index.jsp in the web application root is forwarded to by the action index, never requested directly. If the container’s welcome file is index.jsp, a request to the context root runs the JSP outside any action invocation, and s:url action='hello' fails there. On Struts 7.4.0 this was reproduced: a direct request to /index.jsp answers status 500 with JasperException and the root cause NullPointerException: Cannot invoke "org.apache.struts2.ActionInvocation.getProxy()" because "invocation" is null. The struts-examples projects avoid it with the welcome file /index (or index), which Tomcat maps to the action, and the tutorial text avoids it by telling the reader to open index.action. The tag also has a namespace attribute; with <s:url action='hello' namespace='/'/> a direct request to the JSP works.

Step 5 — Build, Deploy and Run on Tomcat 11

Build the WAR with Maven and copy it to Tomcat’s webapps directory; Tomcat unpacks it and uses the file name, without the extension, as the context path (/bookshelf-struts).

mvn -B -q package
cp target/bookshelf-struts.war "$CATALINA_HOME/webapps/"

Development mode stays off unless the environment of Tomcat’s JVM sets STRUTS_DEV_MODE=true, for example a local run started with STRUTS_DEV_MODE=true "$CATALINA_HOME/bin/catalina.sh" run; the variable is read when the configuration loads, so set it before Tomcat starts. Production simply does not set it. Other ways to deploy (the Tomcat Manager, Cargo, Docker) are in Deploying to Tomcat. The requests below are the ones the verification run answered. The first shows the action hello, the second the action books with the author filter. The verification run deployed the same WAR under another file name, so only the context path in the outputs differs from a deployment of bookshelf-struts.war.

$ curl -s http://localhost:8080/bookshelf-struts/hello
<!DOCTYPE html>

<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello</title>
  </head>
  <body>
    <h2>Welcome to the Bookshelf</h2>
  </body>
</html>
$ curl -s "http://localhost:8080/bookshelf-struts/books?author=William%20Gibson"
<h1>Books</h1>
<ul>
    <li>Neuromancer by William Gibson</li>
</ul>
<p><a href="/bookshelf-struts/index.action;jsessionid=B8F042AD62735FCC3A8F28A6482D20AA">Back</a></p>

Without the author parameter the same action lists the three books. A browser also shows the Struts security headers that the 7.4.0 default stack adds to every action response: Cross-Origin-Embedder-Policy-Report-Only: require-corp, Cross-Origin-Opener-Policy: same-origin and Vary: Sec-Fetch-Dest,Sec-Fetch-Mode,Sec-Fetch-Site,Sec-Fetch-User (the interceptors coep, coop and fetchMetadata). The JSP’s default session="true" also makes the container send a JSESSIONID cookie with the first response, even though nothing stores anything in the session. The page on security headers explains the first three; the second is the page directive of Part I.

What Happens to a Request

The tutorial closes with "How the Code Works". The version below is the same story with the 7.4.0 names, and with the lines that the DEBUG logging of this page produced for GET /hello.

sequenceDiagram autonumber participant B as Browser participant F as StrutsPrepareAndExecuteFilter participant P as ActionProxy participant I as Interceptor stack participant A as HelloAction participant R as ServletDispatcherResult participant J as hello.jsp B->>F: GET /bookshelf-struts/hello F->>F: strip context path and extension, action name hello in namespace / F->>P: create the ActionProxy for hello P->>I: invoke the defaultStack I->>A: execute() A-->>I: success I-->>P: the interceptors unwind P->>R: result success, location /WEB-INF/views/hello.jsp R->>J: forward J-->>B: s:property reads messageStore.message, HTML response
Step What happens

1-2

The container’s chain delivers the request to the filter, which asks the action mapper whether it is an action request. It drops the host, the context path and the extension (.action or none) and keeps the action name and the namespace: here hello in /. The log line is DefaultActionProxy - Creating an DefaultActionProxy for namespace [/] and action name [hello].

3-4

An ActionProxy and an ActionInvocation are created from the configuration read from struts.xml. The invocation runs the interceptors of the default stack in order. The debug log lists them: staticParams, actionMappingParams, params, conversionError, coep, coop, fetchMetadata, validation, workflow and debugging are logged as conditional interceptors, followed by Executing action method = execute.

5-6

A new HelloAction is created for this request and its execute method runs; it creates the model and returns success.

7-8

The interceptors unwind, and the invocation looks up the result named success in the action configuration: type dispatcher, location /WEB-INF/views/hello.jsp. The log lines are ServletDispatcherResult - Processing location: /WEB-INF/views/hello.jsp and Forwarding to location: /WEB-INF/views/hello.jsp.

9

The result forwards to the JSP with the request dispatcher. The JSP runs inside the same request, where s:property evaluates against the value stack whose top is HelloAction. The response is plain HTML.

The same sequence for the architecture, with the action mapper, the container and the configuration providers named, is the subject of the architecture and request lifecycle page of Part II; the mechanics of the dispatcher forward are the ones of Servlet Fundamentals, RequestDispatcher.

Which URLs Reach an Action

The default struts.action.extension is action,, in 7.4.0 (default.properties): the extension .action and the empty extension are both accepted. The old *.do extension of Struts 1 is not. The requests of this table were made against the verification application; /hello and /hello.action reach the same action.

Table 4. Request URLs and their outcome (7.4.0 defaults, observed on Tomcat 11.0.26)
Request Status Reason

/hello and /hello.action

200

Both the empty and the action extension are accepted

/books?author=William%20Gibson

200

Parameter reaches the annotated setter

/books.do

404

do is not in struts.action.extension

/nothing (no default-action-ref, devMode off)

404

Tomcat error page with the text "There is no Action mapped for namespace [/] and action name [nothing] associated with context path [/bookshelf-struts]."

/nothing (no default-action-ref, devMode on with STRUTS_DEV_MODE=true)

200

The Struts "Problem Report" page with the same message and a stack trace

/nothing (with default-action-ref name="index")

200

The index action answers every unmatched URL

/WEB-INF/views/hello.jsp

404

The container never serves anything below WEB-INF to a client; only a forward reaches it

Two rows deserve attention. The status 200 of the devMode problem page is a trap for monitoring tools that expect a 404 for an unknown URL, and the page exposes class names and stack traces; both are reasons to keep struts.devMode off outside development. The default-action-ref makes the application answer any URL with the home page, which can be convenient for a prototype and is a poor choice for a service that should return a 404; each package may have one default action, and only one default action per namespace is meaningful.

Development Mode

struts.devMode is the single constant that most changes how a Struts application behaves while it is being written. The official page on development mode lists what true does, and it was confirmed against the verification application:

  • the resource bundles (message .properties files) are reloaded on every request (struts.i18n.reload);

  • the XML configuration (struts.xml and validation files) is reloaded on every request (struts.configuration.xml.reload);

  • "normally ignorable problems" are raised to errors: a request parameter that cannot be set on the action is logged as an ERROR with the text "Developer Notification (set struts.devMode to false to disable this message)". For example, the author parameter of BookListAction with its @StrutsParameter annotation removed produced Parameter injection for method [setAuthor] on target [org.example.bookshelf.action.BookListAction] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'. while the response was still 200 and listed all three books;

  • unmapped action names show the problem page described in the previous section.

The default is false, "because it has a significant impact on performance, since the entire configuration will be reloaded on every request". The official page opens with the warning that the option must be turned off before deploying to production, because it can expose sensitive data. The constant may also be set in a legacy struts.properties file or as an init-param of the Struts filter in web.xml; default.properties is the framework’s own file inside the core jar, not an application file (Configuration: struts.xml & Constants). The struts.xml form used here is the one the documentation calls preferred. Part II has a page on debugging, development mode and logging.

XML or Annotations: The Convention Plugin

The first tutorial notes that Struts can use an XML file, annotations, or both to relate a URL, a class and a view page. The XML route of this page is the one every other Struts source documents, and it is explicit: the whole mapping is in one file. The annotation route is the Convention plugin (struts2-convention-plugin, in the BOM), which scans the class path for actions, derives action names from class names and lets @Action and @Result override the defaults. It supersedes the Codebehind plugin (removed in 2.5) and the zero-configuration actionPackages of Struts 2.0. The same action of the Bookshelf, written with annotations, needs the plugin as a dependency:

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-convention-plugin</artifactId>
</dependency>

and no entry in struts.xml:

package org.example.bookshelf.action;

import org.apache.struts2.ActionSupport;
import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.Result;

public class AboutAction extends ActionSupport {

    @Action(value = "about", results = @Result(name = SUCCESS, location = "/WEB-INF/views/about.jsp"))
    @Override
    public String execute() {
        return SUCCESS;
    }

    public String getVersion() {
        return "1.0.0";
    }
}

With about.jsp containing <s:property value="version"/>, a request to /about answered <p>Bookshelf version 1.0.0</p> in the verification application, with no action element in struts.xml. The class is found because its package is ...action, one of the package names the plugin scans by default. The page on the Convention plugin and annotations in Part II covers the naming rules, @Namespace, @ParentPackage, @InterceptorRef, @ResultPath and the choice between the two approaches.

Tooling

Any Maven-capable IDE opens the project. For IntelliJ IDEA the original Struts 2 plugin was donated by JetBrains to the Apache Software Foundation in 2024 and is maintained as part of the Struts project; it is installed from the JetBrains Marketplace (release notes and downloads of every version are published in the apache/struts-intellij-plugin repository). The Struts Getting Started index lists the plugin page next to the tutorials. Debugging a WAR from an IDE is outside this page; the Jetty plugin of the section Getting Started page and Tomcat’s remote-debug port are the portable choices, and the IDE guides of this site (issue #116) are the place for IDE-specific steps once they exist.

Pitfalls and Error Messages

Table 5. Getting-started pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26 with Struts 7.4.0)
Symptom or message Cause Fix

<h2></h2>: the page renders, the value is empty, status 200 (observed)

The model class is not on the OGNL allowlist; the log has "is not allowlisted! Add to 'struts.allowlist.classes' or 'struts.allowlist.packageNames' configuration."

Add struts.allowlist.packageNames (or struts.allowlist.classes) in struts.xml; keep the allowlist enabled

A request parameter does not arrive; no error; all rows listed (observed)

The setter has no @StrutsParameter. In devMode the log says "Parameter injection for method [setAuthor] …​ rejected. Ensure it is annotated with @StrutsParameter"

Annotate the setter; use depth for nested objects

Status 500, NullPointerException with the message "invocation is null" on /index.jsp (observed)

A JSP with s:url action=... was requested directly, outside an action invocation

Use the welcome file /index, link to the action, or give s:url a namespace

Status 500, NullPointerException because ActionContext.getContext() returned null, on a JSP that uses a Struts tag (observed, filter removed)

The Struts filter is missing or does not map the JSP, so no ActionContext exists

Declare the filter with url-pattern /* in web.xml

Status 404 on /books when the filter is missing (observed)

Without the filter nothing maps the action name; the container looks for a file called books

Declare the filter

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

Mistyped action name, wrong namespace, or struts.xml not on the class path

Check the name and the package namespace; check that struts.xml is in WEB-INF/classes

/books.do answers 404 (observed)

Struts 1 extension; the default is action,,

Use .action or no extension, or configure struts.action.extension

Every URL answers 404 after deployment; catalina.out has "Dispatcher initialization failed", "Unable to load configuration" with the line and column in struts.xml, and Caused by: java.lang.ClassNotFoundException (observed)

A wrong class attribute in struts.xml: the filter fails to start ("One or more Filters failed to start") and the container stops the context

Read the first exception in catalina.out; fix the fully qualified class name

package com.opensymphony.xwork2 does not exist when compiling

Code from a Struts 2.5 or 6 tutorial

Use org.apache.struts2.ActionSupport and the other org.apache.struts2 packages

Maven cannot resolve a struts2-*-plugin from the BOM

The BOM still lists plugins that have no 7.4.0 artifact

Use only the published plugins listed above

URLs with ;jsessionid=

The first response to a client without a cookie

Harmless; the container stops appending it when the cookie is accepted

struts-7.0.dtd cannot be downloaded (404)

There is no such DTD (struts.xml)

Declare struts-6.0.dtd, as the tutorial does

Everything works in devMode and is slow in production-like tests

devMode reloads the whole configuration on every request

Turn struts.devMode off outside development

What Changed Recently

Table 6. Changes since the tutorials were written that matter for a first Struts application
Version Change Effect on this page

Struts 7.0

The com.opensymphony.xwork2 packages are renamed to org.apache.struts2 (a search and replace, per the migration guide; some classes also change sub-package, for example Action moved to org.apache.struts2.action); the OGNL allowlist (struts.allowlist.enable, available since 6.4) is on by default; @StrutsParameter is required on setters (struts.parameters.requireAnnotations, default true)

ActionSupport import, packageNames constant and the annotation on setAuthor

Struts 7.0

Jakarta EE (jakarta.*) and Servlet 6 are required, Java 17 is the minimum; struts.mapper.alwaysSelectFullNamespace is true; access to classes in the default (empty) package is disallowed

Tomcat 10.1 or 11 only; put classes in a named package

Struts 6.0

The DTD struts-6.0.dtd was introduced and is still the newest published one

The DOCTYPE of every struts.xml

Struts 7.4.0

The default stack contains the csp, coep, coop and fetchMetadata interceptors (read from the 7.4.0 struts-default.xml); Fetch Metadata is documented "since Struts 6.0.0"

Extra response headers in the output above

Struts 7.4.0 (2026-10-02)

Current release; the bulletins S2-075 to S2-078 are fixed in 7.4.0 and 6.12.0

Stay on a patched version; the page on security bulletins lists them

Maven archetypes

No archetype after 6.0.0

The project is built by hand

Struts 2.5.x

End of life on 2023-10-30

Tutorials and answers written for 2.5 are for an unsupported line

Where the Books Are Outdated

The four reference books of the section were written for JSP 1.2 and Servlet 2.3 (the JSP and JSTL books), Struts 1.x (the Struts 1 book) and Struts 2.0 (the Struts 2 book). A reader who starts with them meets the following differences, all checked against the current documentation.

Table 7. Getting-started material from the books versus the current documentation
Book-era practice Today (Struts 7.4, Tomcat 11)

web.xml declares the ActionServlet with *.do mapping and config init parameters (Struts 1)

One filter, StrutsPrepareAndExecuteFilter, mapped to /*; the default extension is action,,

struts-config.xml, form beans, ActionForm, ActionMapping and ActionForward (Struts 1)

struts.xml, actions that are plain classes, results; no form-bean classes. Struts 1 reached end of life on 2013-04-05 (last release 1.3.10)

Struts 2.0: org.apache.struts2.dispatcher.FilterDispatcher, the first filter class

StrutsPrepareAndExecuteFilter, now in org.apache.struts2.dispatcher.filter; FilterDispatcher is not in the 7.4.0 jar

A Struts 2.0 "HelloWorld" with the actionPackages zero-configuration or the Codebehind plugin, struts.xml with the 2.0 DTD

Explicit XML as on this page, or the Convention plugin; Codebehind was removed in 2.5

Dojo-based Ajax tags, the s:head theme with JavaScript

Legacy and no longer part of the core; the JSON plugin is the way to serve Ajax responses

Setters are public and every request parameter reaches them

Setters need @StrutsParameter; OGNL access is limited by the allowlist

javax.servlet imports, Servlet 2.3 or 2.4, the taglib element in web.xml

jakarta.servlet, Servlet 6.1; the taglib is discovered automatically

Ant builds and manual copying of jars into WEB-INF/lib

Maven and the BOM; the jar list is generated

Tomcat 4 or 5 and a server.xml Context entry for each application

Tomcat 11: drop the WAR in webapps or deploy with the Manager

struts-default.xml and ActionSupport in com.opensymphony.xwork2

org.apache.struts2.ActionSupport

Where the official pages disagree with the verified behaviour, this page follows the behaviour. The inconsistencies found while writing it are the tutorial’s omission of the allowlist constant, the stale com.opensymphony.xwork2.ActionSupport named by the Action Configuration page, the "struts-core.jar" named by the web.xml page (the artifact is struts2-core), the Servlet API 2.5 baseline that the tutorial gives for Struts 6, and the DTD versions (6.0 on the site, 6.5 in the examples); they are collected in the migration and errata page of the section.

Where to Go Next

The tutorials continue with "Using Tags", "Coding Actions" and "Processing Forms". In this section the next pages of Part II are the architecture and request lifecycle (the filter, the action mapper, the proxy and the interceptors in detail), the configuration page with the table of the struts.* constants, the pages on packages and namespaces, actions, results and interceptors, and later the tag pages and the form-processing and validation pages that grow the Bookshelf into a CRUD application. Each reuses the bookshelf-struts project of this page.