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.xmlon 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.
| Component | Version | Notes |
|---|---|---|
Apache Struts |
7.4.0 ( |
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) |
|
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 |
FreeMarker |
2.3.35 |
Used by the Struts tag templates, pulled in by |
Log4j 2 |
2.26.1 |
|
Maven |
3.9.16 |
|
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.
Source: Struts — Maven Archetypes
Source: apache/struts-examples on GitHub
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.
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
Source: Struts — Configuration Files
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.
<?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-coreis the only Struts dependency an application needs; it brings OGNL, FreeMarker and the other libraries listed in the previous section as transitive dependencies. Every otherstruts2-*artifact is a plugin that is added only when a feature needs it (Convention, JSON, REST, Spring, Tiles and others). -
struts2-bom7.4.0 lists 24 entries, but four of them —struts2-embeddedjsp-plugin,struts2-gxp-plugin,struts2-osgi-pluginandstruts2-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
jakartaee11build 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.xmlalso configures the Jetty Maven plugin so thatmvn jetty:runserves 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.
Source: Apache Struts — Plugins
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.
<?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.
Source: Struts — 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.
<?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>
Source: Struts — web.xml, Simple Example
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.
| Item | Detail |
|---|---|
|
The recommended single filter. It prepares the request (character encoding, |
|
The split form, for integrating other filters between the two phases (the documentation points to the SiteMesh plugin as the case that needs it) |
|
The preferred mapping: Struts then sees every request, including the ones for the static content it serves from its jars |
|
Allowed; only requests ending in |
|
A comma-separated list of regular expressions of request URLs that the filter passes through untouched, set as a constant in |
|
Registers a custom file manager early, for application servers with a special file system (the docs mention VFS in JBoss) |
|
A comma-separated list of |
A |
Neither required nor recommended: the Struts tag library descriptor is inside |
Source: Struts — web.xml
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.
<?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.
| Element | Meaning |
|---|---|
|
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 |
|
Development mode, explained in its own section below. It is |
|
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) |
|
A named group of actions that shares a namespace, interceptors and results. Only |
|
The action that answers requests which match no action name; without it such requests are an error |
|
Maps a name (the part of the URL without the extension) to a class. Only |
|
Maps a result name to a view. With no |
Source: Struts — Package Configuration
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-refisorg.apache.struts2.ActionSupport; an<action name="index">withoutclassrunsActionSupport.execute(), which returnssuccess. (The Action Configuration page still namescom.opensymphony.xwork2.ActionSupportfor this default; that package name disappeared in Struts 7.) -
the default result type is
dispatcher(ServletDispatcherResult), and aresultwithout a name issuccess; -
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,workflowanddebugging. Theparamsinterceptor copies request parameters onto the action;validationandworkflowrun validators and returninputwhen 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.
Source: Struts — struts-default.xml
Source: Struts — Interceptors
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.
package org.example.bookshelf.model;
public class MessageStore {
private final String message;
public MessageStore() {
message = "Welcome to the Bookshelf";
}
public String getMessage() {
return message;
}
}
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.
<!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.
Source: Struts — property tag
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.
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.
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.
<!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>
Source: Struts — iterator tag
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
<!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>
Source: Struts — url tag
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.
Source: Struts — url tag, Attributes
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.
| 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 ( |
3-4 |
An |
5-6 |
A new |
7-8 |
The interceptors unwind, and the invocation looks up the result named |
9 |
The result forwards to the JSP with the request dispatcher. The JSP runs inside the same request, where |
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.
| Request | Status | Reason |
|---|---|---|
|
200 |
Both the empty and the |
|
200 |
Parameter reaches the annotated setter |
|
404 |
|
|
404 |
Tomcat error page with the text "There is no Action mapped for namespace [/] and action name [nothing] associated with context path [/bookshelf-struts]." |
|
200 |
The Struts "Problem Report" page with the same message and a stack trace |
|
200 |
The |
|
404 |
The container never serves anything below |
Source: Struts — default.properties
Source: Struts — ActionMapper
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
.propertiesfiles) are reloaded on every request (struts.i18n.reload); -
the XML configuration (
struts.xmland 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
ERRORwith the text "Developer Notification (set struts.devMode to false to disable this message)". For example, theauthorparameter ofBookListActionwith its@StrutsParameterannotation removed producedParameter 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.
Source: Struts — Development Mode
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.
Source: Struts — IntelliJ IDEA plugin
Pitfalls and Error Messages
| Symptom or message | Cause | Fix |
|---|---|---|
|
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 |
A request parameter does not arrive; no error; all rows listed (observed) |
The setter has no |
Annotate the setter; use |
Status 500, |
A JSP with |
Use the welcome file |
Status 500, |
The Struts filter is missing or does not map the JSP, so no |
Declare the filter with |
Status 404 on |
Without the filter nothing maps the action name; the container looks for a file called |
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 |
Check the name and the package |
|
Struts 1 extension; the default is |
Use |
Every URL answers 404 after deployment; |
A wrong |
Read the first exception in |
|
Code from a Struts 2.5 or 6 tutorial |
Use |
Maven cannot resolve a |
The BOM still lists plugins that have no 7.4.0 artifact |
Use only the published plugins listed above |
URLs with |
The first response to a client without a cookie |
Harmless; the container stops appending it when the cookie is accepted |
|
There is no such DTD (struts.xml) |
Declare |
Everything works in |
|
Turn |
What Changed Recently
| Version | Change | Effect on this page |
|---|---|---|
Struts 7.0 |
The |
|
Struts 7.0 |
Jakarta EE ( |
Tomcat 10.1 or 11 only; put classes in a named package |
Struts 6.0 |
The DTD |
The |
Struts 7.4.0 |
The default stack contains the |
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 |
Source: Struts — Version Notes 7.4.0
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.
| Book-era practice | Today (Struts 7.4, Tomcat 11) |
|---|---|
|
One filter, |
|
|
Struts 2.0: |
|
A Struts 2.0 "HelloWorld" with the |
Explicit XML as on this page, or the Convention plugin; Codebehind was removed in 2.5 |
Dojo-based Ajax tags, the |
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 |
|
|
Ant builds and manual copying of jars into |
Maven and the BOM; the jar list is generated |
Tomcat 4 or 5 and a |
Tomcat 11: drop the WAR in |
|
|
Source: Struts — web.xml
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.