Actions

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 (the current 7.x release, verified on 2026-10-08), running on Apache Tomcat 11.0.26 and Java 21; Struts 7 needs Java 17 and Jakarta Servlet 6 at least. The facts were checked against the Struts core-developer guide, the Getting Started tutorials, the 7.4.0 Javadoc and the 7.4.0 jar itself (javap on the classes named below), and every example was built into a Maven WAR with the struts2-bom 7.4.0, deployed to the scratch Tomcat and exercised with curl; the outputs shown are the outputs that run produced. An action is the unit of work of Struts 2: a small Java class, instantiated for each request, whose one method runs the application logic and returns the name of the result that should render the answer. The running scenario is the Bookshelf catalogue that Part II builds as bookshelf-struts: a list of titles, a form that adds a title, a reader who has a session, and a CSV export. The other pages of this part (architecture and request lifecycle, packages and namespaces, wildcard mappings, results, interceptors and the rest) are linked where they come up.

What an Action Is

In the model-view-controller split that Struts follows, the filter StrutsPrepareAndExecuteFilter is the front controller, the action is the piece of code that handles one kind of request, and the result (usually a JSP or a FreeMarker template) is the view. The action has three jobs: it receives the request data (Struts copies request parameters onto the action’s properties), it runs the logic or hands it to a service, and it returns a short string, the result code, that the configuration maps to a result. Everything the view needs is read back from the action’s getters through the value stack, because the action instance is pushed on top of that stack while the result renders.

An action is not a servlet. It does not receive HttpServletRequest and HttpServletResponse as arguments, it is not shared between threads, and it does not have to extend or implement anything. The framework creates one instance for every request (see Thread per Request), so the instance fields are private to that request and the usual servlet worry of shared mutable state does not apply to them. The Struts documentation says it directly: each action caused by an HTTP request is a plain old Java object that is instantiated for each request.

There are three ways to write the class, and the framework treats them alike:

Style What the class looks like When it fits

POJO

A public class with a public no-argument constructor and a public method that returns String. No interface, no base class. If no method attribute is configured, the framework finds a method called execute by reflection

The smallest actions; classes that are unit-tested without the framework (BookListAction below)

Implements Action

The same class declares implements org.apache.struts2.action.Action, so execute() is checked by the compiler and the constants SUCCESS, NONE, ERROR, INPUT and LOGIN are in scope

When the result-code constants help readability and nothing else is needed

Extends ActionSupport

The class extends org.apache.struts2.ActionSupport, which implements Action and adds validation hooks, error and message collections, text lookup and the locale

The usual choice for actions that handle forms (AddBookAction, ReaderAction below)

The Bookshelf Actions Used on This Page

All examples belong to one scratch WAR built from the bookshelf-struts base: struts2-core 7.4.0 from the struts2-bom, log4j-slf4j2-impl, the Servlet and Pages APIs as provided, the filter StrutsPrepareAndExecuteFilter mapped to /* in web.xml, and a package default that extends struts-default in the root namespace. The catalogue itself is a thread-safe static list in a class Catalogue (an in-memory stand-in for the repository that Part II adds later). The default action extension of 7.4.0 is struts.action.extension=action,, (the extension .action or no extension at all), so /books and /books.action both reach the same mapping.

Table 1. The actions of the scratch WAR
Action name Class What it demonstrates

books

BookListAction (POJO)

A POJO with execute, a request parameter, and the custom result code empty

bookCount

BookCountAction (ActionSupport)

The method attribute

newBook, addBook

AddBookAction (ActionSupport)

input, validate, field errors, action messages, getText

reader

ReaderAction

SessionAware and ServletRequestAware, plus the ActionContext helper

export

ExportAction

ServletResponseAware and the result code NONE

instance

InstanceAction

One instance per request

welcome

no class

The default class ActionSupport, and the default action

broken, readerNoServletConfig, unannotated

BrokenAction, ReaderAction, UnannotatedAction

Three misconfigurations that are reproduced in the pitfalls table

How an Action Runs

The wiring behind a single action request is described in Architecture & Request Lifecycle; what an action author needs is the order of events around the action method.

sequenceDiagram participant B as Browser participant F as StrutsPrepareAndExecuteFilter participant M as ActionMapper participant P as ActionProxy and ActionInvocation participant I as Interceptors (defaultStack) participant A as Action instance participant R as Result B->>F: POST /addBook.action (title=Ubik) F->>M: which action does this URL name? M-->>F: ActionMapping for addBook F->>P: create the proxy and the invocation P->>A: new AddBookAction() (one per request) P->>I: invoke() I->>A: servletConfig injects session, request, ... I->>A: params calls setTitle("Ubik") I->>A: validation calls validate() alt validate added a field error I-->>P: workflow returns "input", the action method is skipped else no errors I->>A: execute() A-->>I: result code "success" end I-->>P: result code travels back out through the interceptors P->>R: execute the result mapped to the code R->>A: read properties through the value stack R-->>B: HTML response

The same sequence in words, with the names the Struts 7.4.0 jar uses:

  1. The filter asks the ActionMapper whether the URL names an action. If it does not, the request is passed on to the rest of the filter chain, which is why static files and JSPs keep working next to Struts.

  2. An ActionProxy is created for the mapping (namespace, action name, method) and an ActionInvocation is built. The invocation instantiates the class named in the <action> element, using the Struts object factory. That instance is new for this request.

  3. The invocation runs the interceptors of the action’s stack in order, then the action method, then the result, and unwinds the interceptors in reverse order. In the default stack of 7.4.0 the interceptors that matter to an action author appear in this order: exception, alias, servletConfig (injects the Aware objects), httpMethod, i18n, prepare, chain, scopedModelDriven, modelDriven, actionFileUpload, staticParams, actionMappingParams, params (copies request parameters to setters), conversionError, validation and workflow.

  4. validation calls validate() on an action that implements Validateable (ActionSupport does); workflow checks the error collections, and if any field, action or conversion error exists it returns the result code input without calling the action method. Both interceptors skip the methods input, back, cancel and browse.

  5. If nothing stopped it, the action method (execute by default, or the method attribute) runs and returns a string.

  6. The result code selects a <result> in the mapping, which renders. Properties that the view prints are read from the action instance, which is still on top of the value stack.

The interceptor stack itself, the value stack and the result types each have their own page in this part (interceptors, value-stack-action-context-and-ognl, results-and-result-types); this page stays on the class that the stack wraps.

The Action Mapping

The mapping is the declaration that connects a name in the URL to a class. Only the name attribute is required; the others (class, method, converter) can be omitted, and a mapping can also carry results, exception mappings, an interceptor stack and parameters.

The Bookshelf mappings: the default package of the scratch application (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>
    <package name="default" extends="struts-default" namespace="/">
        <default-action-ref name="welcome"/>

        <action name="welcome">
            <result>/WEB-INF/views/welcome.jsp</result>
        </action>

        <action name="books" class="org.example.bookshelf.BookListAction">
            <result>/WEB-INF/views/books.jsp</result>
            <result name="empty">/WEB-INF/views/empty.jsp</result>
        </action>

        <action name="bookCount" class="org.example.bookshelf.BookCountAction" method="count">
            <result>/WEB-INF/views/count.jsp</result>
        </action>

        <action name="newBook" class="org.example.bookshelf.AddBookAction" method="input">
            <result name="input">/WEB-INF/views/add-form.jsp</result>
        </action>

        <action name="addBook" class="org.example.bookshelf.AddBookAction">
            <result name="input">/WEB-INF/views/add-form.jsp</result>
            <result>/WEB-INF/views/added.jsp</result>
        </action>
    </package>
</struts>

A <result> without a name answers to success. The DOCTYPE is the 6.0 one, which a Struts 7 application keeps (why 6.0).

Action names and extensions

The framework drops the host, the application name and the extension from the requested URL and matches what is left against the name attribute: a request for /g8act/books.action selects the mapping books. In 7.4.0 the extensions accepted are those of struts.action.extension, whose default is action,,: the trailing empty entries mean the empty extension is accepted too. Links to actions are normally produced by a Struts tag (<s:form action="addBook">, <s:url action="books"/>), which adds the extension and the context path for the reader.

The documentation lists three limits that apply to the name itself:

  • The DefaultActionMapper checks the name against a regular expression, [a-zA-Z0-9._!/-]* by default; a name that does not match raises an exception, and the expression can be replaced with the constant struts.allowed.action.names.

  • A name that contains a slash (admin/home) needs struts.enable.SlashesInActionNames set to true, with side effects (validation files then replace the slash with a hyphen).

  • Dots (create.user) have no known side effects, but dashes (my-action) break the JavaScript that some tags and themes generate; camel case (createUser) or underscores (my_action) are the safe choices.

The default class and the default action

If the class attribute is left out, the package’s default-class-ref is used. In struts-default.xml of 7.4.0 that is org.apache.struts2.ActionSupport, whose execute() returns success and whose input() returns input. The welcome action above has no class at all and still renders its JSP, because ActionSupport.execute() returns success:

curl -s http://localhost:18080/g8act/welcome.action
<h1>Bookshelf</h1><p>Welcome, from the default ActionSupport.execute().</p>

<default-action-ref name="welcome"/> names the action that answers when no mapping matches. The same output came back for nothing.action, which has no mapping, so a mistyped URL lands on the welcome page instead of a 404. The documentation’s advice is one default action per namespace: if several packages declare a default action in the same namespace, there is no guarantee which one wins. A trailing wildcard mapping <action name="*"> is the other way to catch unmatched names, and it must be the last mapping in the file.

POJO Actions

The first Bookshelf action is a plain class. It has no base class, it filters the catalogue with a request parameter, and it chooses between two results with its own result code, empty, which is not one of the five standard constants.

BookListAction: a POJO action with a request parameter and a custom result code
package org.example.bookshelf;

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

public class BookListAction {
    private String filter;
    private List<String> titles = List.of();

    public String execute() {
        titles = Catalogue.find(filter);
        return titles.isEmpty() ? "empty" : "success";
    }

    public String getFilter() {
        return filter;
    }

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

    public List<String> getTitles() {
        return titles;
    }
}

With the mapping books shown above, the view books.jsp lists the titles through the <s:iterator value="titles"> tag, and empty.jsp prints the filter:

curl -s http://localhost:18080/g8act/books.action
curl -s "http://localhost:18080/g8act/books.action?filter=dun"
curl -s "http://localhost:18080/g8act/books.action?filter=zzz"
<ul><li>Dune</li><li>Neuromancer</li><li>Hyperion</li></ul>
<ul><li>Dune</li></ul>
<p>No titles match zzz.</p>

The request parameter filter reached setFilter because the params interceptor of the default stack copied it, and the view read titles and filter back through their getters. The annotation @StrutsParameter on the setter is required (see Request Parameters). Because the class does not depend on Struts at all (apart from the annotation), it can be tested with a plain JUnit test, no container and no framework:

A unit test of the POJO action
class BookListActionTest {
    @Test
    void returnsEmptyWhenNothingMatches() {
        BookListAction action = new BookListAction();
        action.setFilter("zzz");

        assertEquals("empty", action.execute());
        assertEquals(0, action.getTitles().size());
    }

    @Test
    void returnsSuccessWhenTitlesMatch() {
        BookListAction action = new BookListAction();
        action.setFilter("dun");

        assertEquals("success", action.execute());
        assertEquals(1, action.getTitles().size());
    }
}

Both tests passed under Maven Surefire (Tests run: 2, Failures: 0, Errors: 0). The limit of this approach is shown in ActionSupport: as soon as the action calls getText, a test needs the framework running. The Struts JUnit plugin (StrutsTestCase) starts the filter and the interceptors in a test, and Testing covers it.

The Action Interface and Result Codes

org.apache.struts2.action.Action (in Struts 6 and earlier com.opensymphony.xwork2.Action) declares one method, String execute() throws Exception, and five constants that name the conventional result codes. A result code is just a string that the mapping resolves; the constants only save typing and typos.

Table 2. The Action constants (read from the 7.4.0 jar with javap -constants)
Constant Value Meaning in the Javadoc and in practice

SUCCESS

success

The execution was successful; show the result view. The result without a name attribute answers to it

NONE

none

The execution was successful, but no view should be shown, because the action has produced the response itself (see ExportAction)

ERROR

error

The execution failed; show an error view

INPUT

input

The action could not execute because the input was missing or invalid; show the input form again. The workflow interceptor returns it when errors exist

LOGIN

login

The action could not execute until the user logs in; the convention for an authentication redirect

Any other string is a valid result code: BookListAction returns empty, and the mapping defines <result name="empty">. The interface is optional; implementing it is useful when the constants help, and it changes nothing else, because the framework calls the method by reflection when the action does not implement it.

If a code is returned that the mapping cannot resolve (no <result> of that name in the action, and none in the package’s global results), nothing renders. The scratch action OddCodeAction returns surprise and has a single unnamed result; Struts 7.4.0 answered with HTTP 404 and the message No result defined for action org.example.bookshelf.OddCodeAction and result surprise. Result lookup, global results and result types are the subject of Results & Result Types.

Answering without a view: NONE

NONE means the action has already written the response, so the framework must not render anything. The export mapping has no <result> at all, and ExportAction writes a CSV document through the servlet response that it received from the ServletResponseAware interface (see Session, Request and Response Access):

curl -s -i http://localhost:18080/g8act/export.action
HTTP/1.1 200
Content-Type: text/csv;charset=UTF-8

title
Dune
Neuromancer
Hyperion

The usual way to send a body that is not HTML is a stream, JSON or plain-text result, which keeps the action free of the servlet API; the NONE form is shown because it is the only case that needs the response object inside the action.

ActionSupport

org.apache.struts2.ActionSupport is the base class that most actions extend. In the 7.4.0 jar it implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider and Serializable, and these are the roles it plays.

classDiagram class Action { +SUCCESS +NONE +ERROR +INPUT +LOGIN +execute() String } class Validateable { +validate() } class ValidationAware { +addActionError(msg) +addActionMessage(msg) +addFieldError(field, msg) +hasErrors() boolean } class TextProvider { +getText(key) String +hasKey(key) boolean } class LocaleProvider { +getLocale() Locale } class ActionSupport { +execute() String +input() String } <<interface>> Action <<interface>> Validateable <<interface>> ValidationAware <<interface>> TextProvider <<interface>> LocaleProvider Action <|.. ActionSupport Validateable <|.. ActionSupport ValidationAware <|.. ActionSupport TextProvider <|.. ActionSupport LocaleProvider <|.. ActionSupport
Table 3. The members of ActionSupport that an action author uses (7.4.0 jar)
Member Role

execute()

Returns success. Override it with the logic of the action

input()

Returns input. Used by a mapping with method="input" to show a form without validation

validate()

Empty by default. Override it for programmatic validation; the validation interceptor calls it before the action method

addFieldError(field, message), getFieldErrors(), hasFieldErrors(), clearFieldErrors()

Errors tied to one input field (a map from field name to a list of messages); rendered by <s:fielderror> and by the field tags of the themes

addActionError(message), getActionErrors(), hasActionErrors(), clearActionErrors()

Errors that belong to the whole action; rendered by <s:actionerror/>

addActionMessage(message), getActionMessages(), hasActionMessages(), clearMessages()

Informational messages (not errors); rendered by <s:actionmessage/>

hasErrors(), clearErrors(), clearErrorsAndMessages()

Aggregates over the three collections

getText(key) and its overloads with default value, String[] or List arguments

Message lookup in the resource bundles; the arguments fill the {0} placeholders

getLocale(), hasKey(key), getTexts()

Current locale and bundle inspection

pause(result)

Stops the invocation at once (by throwing a PauseException) and makes it return the given result, so the invocation can be resumed later

ModelDriven and Preparable are separate interfaces that an action implements next to ActionSupport; they and the CRUD pattern are on ModelDriven, Preparable & CRUD.

A form action: validate, field errors, messages and getText

AddBookAction is the Bookshelf’s "add a title" action. It extends ActionSupport, validates in validate(), and builds its messages with getText so that the wording lives in a properties file, not in Java.

AddBookAction: validate(), field errors, an action message and getText
package org.example.bookshelf;

import org.apache.struts2.ActionSupport;
import org.apache.struts2.interceptor.parameter.StrutsParameter;

public class AddBookAction extends ActionSupport {
    private String title;

    @Override
    public void validate() {
        if (title == null || title.isBlank()) {
            addFieldError("title", getText("book.title.required"));
        } else if (Catalogue.contains(title)) {
            addFieldError("title", getText("book.title.duplicate", new String[] {title}));
        }
    }

    @Override
    public String execute() {
        Catalogue.add(title.trim());
        addActionMessage(getText("book.added", new String[] {title.trim()}));
        return SUCCESS;
    }

    public int getTotal() {
        return Catalogue.size();
    }

    public String getTitle() {
        return title;
    }

    @StrutsParameter
    public void setTitle(String title) {
        this.title = title;
    }
}
src/main/resources/org/example/bookshelf/AddBookAction.properties
book.title.required=Please enter a title
book.title.duplicate=The catalogue already contains "{0}"
book.added=Added "{0}" to the catalogue

The file sits next to the class name in the class path (AddBookAction.properties in the package directory). The lookup order that getText follows, from the Struts documentation (see Localization and i18n), is: the action class’s own bundle, the bundles of its interfaces, the bundles of its base classes, the bundle of the model if the action is model-driven, the package.properties of the class’s package and of every parent package, the key hierarchy itself, and finally the global bundles. That is why the same keys can also be shared from a package.properties.

The workflow interceptor turns the field error into a trip back to the form. The mapping has the result input, which is the form with the error list printed by <s:fielderror fieldName="title"/>. The views of the example use theme="simple" to keep the output short:

curl -s -d title= http://localhost:18080/g8act/addBook.action
curl -s -d title=Dune http://localhost:18080/g8act/addBook.action
curl -s -d title=Ubik http://localhost:18080/g8act/addBook.action
<form id="addBook" name="addBook" action="/g8act/addBook.action" method="post">
<input type="text" name="title" value="" id="addBook_title"/>
<ul class="errorMessage"><li><span>Please enter a title</span></li></ul>
<input type="submit" value="Submit" id="addBook_0"/>
</form>

<form id="addBook" name="addBook" action="/g8act/addBook.action" method="post">
<input type="text" name="title" value="Dune" id="addBook_title"/>
<ul class="errorMessage"><li><span>The catalogue already contains &quot;Dune&quot;</span></li></ul>
<input type="submit" value="Submit" id="addBook_0"/>
</form>

<ul class="actionMessage"><li><span>Added &quot;Ubik&quot; to the catalogue</span></li></ul>
<p>Catalogue now holds: 4</p>

(Whitespace inside the <ul> elements was collapsed for the listing.) Three things in this output are worth reading carefully. The first is that execute() ran only for Ubik: the two invalid requests came back from workflow without reaching it. The second is that the submitted value was put back into the text field, because the title property still holds what params set. The third is that the message after a successful add is an action message rendered by the same request that created it; messages do not survive a redirect unless the message store interceptor copies them, which is the subject of the post-redirect-get material in the results and interceptors pages.

Showing the form: the input method

A request for the form itself must not run validation, otherwise the first visit shows an error. The first attempt in the scratch app used the same mapping, addBook, for the form and the submission, and a plain GET answered with the validation error Please enter a title: the title parameter was missing, so validate() rejected it, and workflow sent the request to input. The fix is a second mapping for the same class with method="input", since the default stack excludes the method input from validation and workflow:

curl -s http://localhost:18080/g8act/newBook.action
<form id="addBook" name="addBook" action="/g8act/addBook.action" method="post">
<input type="text" name="title" value="" id="addBook_title"/><input type="submit" value="Submit" id="addBook_0"/>
</form>

The method name input comes from the excludeMethods parameter of the two interceptors in the default stack of struts-default.xml (input,back,cancel,browse). Another name for the form method would run validation again.

Testing ActionSupport outside a request

getText on an ActionSupport reaches the framework through the current ActionContext. A unit test that creates the action with new AddBookAction() and calls validate() therefore fails before it reaches an assertion. The run of such a test on 7.4.0 reported:

java.lang.NullPointerException: Cannot invoke "org.apache.struts2.ActionContext.getContainer()"
  because the return value of "org.apache.struts2.ActionContext.getContext()" is null

The remedies are to run the test through the Struts JUnit plugin, which sets up the context, or to keep the message construction out of the action (inject a service or keep the check in a plain method that returns a code, as BookListAction does).

The method Attribute: More Than One Entry Point

By default the framework calls execute. Another method can be named with the method attribute, so one class can offer several entry points (the documentation’s example is a CRUD class with create, retrieve, update and delete). The Bookshelf’s BookCountAction has no execute at all:

BookCountAction: an action whose only entry point is count()
public class BookCountAction extends ActionSupport {
    private int count;

    public String count() {
        count = Catalogue.size();
        return SUCCESS;
    }

    public int getCount() {
        return count;
    }
}
<action name="bookCount" class="org.example.bookshelf.BookCountAction" method="count">
    <result>/WEB-INF/views/count.jsp</result>
</action>
curl -s http://localhost:18080/g8act/bookCount.action
<p>Titles in the catalogue: 4</p>

A method named by the method attribute is allowed by definition; the framework’s other ways of choosing a method (wildcard patterns such as method="{1}", dynamic method invocation with ! and method:, and the strict method invocation rules with <allowed-methods> and <global-allowed-methods>) are the subject of Wildcard Mappings & Method Invocation. Two facts matter here. The default struts-default package of 7.4.0 declares <global-allowed-methods>execute,input,back,cancel,browse,save,delete,list,index</global-allowed-methods>, so those names can be called without the method attribute; and dynamic method invocation is off: struts.enable.DynamicMethodInvocation defaults to false in default.properties.

If the class has no execute and the mapping names no other method, the request fails. BrokenAction has only a list method and a mapping without a method attribute; Struts 7.4.0 answered with HTTP 404 and the message Wrong method was defined as an action method: execute. The official page says only that the framework "will throw an exception".

Request Parameters and Properties

Struts fills an action by calling its setters. The params interceptor copies each request parameter whose name matches a property path (filter, title, author.name, books[0].title) through OGNL, converting the string to the type of the property. The action must also authorize the property, and in Struts 7 that is the default (struts.parameters.requireAnnotations=true in default.properties): a setter, or a public field, or a getter for nested objects, needs @StrutsParameter. The annotation exists to stop an attacker from setting a property that the developer added for another reason.

UnannotatedAction has the same property as BookListAction, with the setter lacking the annotation:

curl -s "http://localhost:18080/g8act/unannotated.action?filter=dun"
curl -s "http://localhost:18080/g8act/books.action?filter=dun"
<ul><li>Dune</li><li>Neuromancer</li><li>Hyperion</li></ul>
<ul><li>Dune</li></ul>

The first call returned all three titles: the parameter was dropped silently, no error, no log line at the default level. The second call, with the annotation, filtered. A getter alone never receives data. The parameter handling, the annotation’s depth attribute, nested objects, collections and the OGNL expression limits are on Parameter Injection and @StrutsParameter. An action that must not receive any parameter at all can implement the marker interface org.apache.struts2.action.NoParameters.

Session, Request, Response and Application Access

An action has no request argument, and the servlet scopes are reached through the Aware interfaces: the action declares the object it needs by implementing an interface, and the servletConfig interceptor calls the interface’s method on the new instance before the action runs. The Struts documentation calls this the best way to access the scopes, and it recommends against reaching for ActionContext directly.

Table 4. The Aware interfaces of Struts 7.4.0 (package org.apache.struts2.action; method signatures from the jar)
Interface Method called by the framework Object it provides

SessionAware

void withSession(Map<String, Object> session)

A map backed by the HttpSession; the scope of the user’s visit

ServletRequestAware

void withServletRequest(HttpServletRequest request)

The servlet request; ties the action to the servlet API

ServletResponseAware

void withServletResponse(HttpServletResponse response)

The servlet response; ties the action to the servlet API

ApplicationAware

void withApplication(Map<String, Object> application)

A map backed by the ServletContext; shared by every user

ServletContextAware

void withServletContext(ServletContext context)

The servlet context itself

ParametersAware

void withParameters(HttpParameters parameters)

The request parameters (HttpParameters)

PrincipalAware

void withPrincipalProxy(PrincipalProxy proxy)

The authenticated principal and role checks

CookiesAware

void withCookies(Map<String, String> cookies)

The request cookies, by name

The names with... replaced the older set... names when the interfaces moved from org.apache.struts2.interceptor to org.apache.struts2.action (see What Changed Recently). The interceptor that performs the injection is servletConfig, third in the default stack; the reference page of the interceptor lists the supported interfaces, but its list is stale for 7.x (see Where the Books Are Outdated and the errata notes at the end of this page).

A reader with a session and a request

ReaderAction counts the visits of a reader in the session and records on the request which class served it. It also reads the same session value a second way, through a helper that uses ActionContext, to show what the direct route looks like.

ReaderAction: SessionAware and ServletRequestAware
package org.example.bookshelf;

import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.action.ServletRequestAware;
import org.apache.struts2.action.SessionAware;

public class ReaderAction extends ActionSupport implements SessionAware, ServletRequestAware {
    private Map<String, Object> session;
    private HttpServletRequest request;
    private String requestLine;

    @Override
    public void withSession(Map<String, Object> session) {
        this.session = session;
    }

    @Override
    public void withServletRequest(HttpServletRequest request) {
        this.request = request;
    }

    @Override
    public String execute() {
        int visits = (Integer) session.getOrDefault("visits", 0) + 1;
        session.put("visits", visits);
        request.setAttribute("servedBy", getClass().getSimpleName());
        requestLine = request.getMethod() + " " + request.getRequestURI();
        return SUCCESS;
    }

    public Object getVisitsViaContext() {
        return CurrentReader.attribute("visits");
    }

    public String getRequestLine() {
        return requestLine;
    }
}
CurrentReader: the direct ActionContext route, for code that is not an action
public final class CurrentReader {
    private CurrentReader() {
    }

    public static Object attribute(String key) {
        return ActionContext.getContext().getSession().get(key);
    }
}
reader.jsp: the view reads the scopes through OGNL
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<p>visits=<s:property value="#session.visits"/> servedBy=<s:property value="#request.servedBy"/>
request=<s:property value="requestLine"/> viaContext=<s:property value="visitsViaContext"/></p>

Two requests that share a cookie jar, then one that does not:

curl -s -c jar.txt http://localhost:18080/g8act/reader.action
curl -s -b jar.txt http://localhost:18080/g8act/reader.action
curl -s http://localhost:18080/g8act/reader.action
<p>visits=1 servedBy=ReaderAction request=GET /g8act/reader.action viaContext=1</p>
<p>visits=2 servedBy=ReaderAction request=GET /g8act/reader.action viaContext=2</p>
<p>visits=1 servedBy=ReaderAction request=GET /g8act/reader.action viaContext=1</p>

The counter belongs to the session (the same cookie gives 2, a new visitor starts at 1); #session.visits and #request.servedBy are the OGNL names that the view uses for the two scopes, and #application and #attr complete the set (#attr searches the page, request, session and application scopes in that order). viaContext shows that ActionContext.getContext().getSession() sees the same map: the direct route works during a request, which is why it is acceptable in a utility that has no other way to the session. In an action, the interface is the better form because the dependency is visible in the class signature and the action can be unit-tested by passing a plain HashMap to withSession.

The two scope maps are not the servlet objects. SessionAware hands over a Map, not the HttpSession, so the action stays free of the servlet API, and the session-fixation and cookie controls described on the servlet page still apply to the real HttpSession underneath (Servlet Fundamentals, Sessions).

ActionContext

org.apache.struts2.ActionContext is the per-request, per-thread holder of everything the framework knows about the call. In 7.4.0 it is a fluent class: ActionContext.getContext() returns the context bound to the current thread, the read accessors include getSession(), getApplication(), getParameters(), getLocale(), getActionName(), getValueStack(), getActionInvocation(), getServletRequest(), getServletResponse(), getServletContext() and getConversionErrors(), and the with... methods (withSession, withLocale, …​) configure a context that a test builds with ActionContext.of(...) and bind(). Outside a request, on a thread the framework did not set up, getContext() returns null, which is the cause of the NullPointerException shown above. The Struts documentation’s advice stands: take the scopes from the Aware interfaces, and read ActionContext only where an interface cannot be used.

The interceptor is a requirement

The Aware interfaces work only when the servletConfig interceptor is in the action’s stack. The readerNoServletConfig mapping reuses ReaderAction with <interceptor-ref name="params"/> as its only interceptor (so the stack is replaced, not extended):

curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8act/readerNoServletConfig.action
500
Cannot invoke "java.util.Map.getOrDefault(Object, Object)" because "this.session" is null

The framework does not warn that an Aware interface was ignored; the field stays null and the first use fails. The custom stacks that the interceptor pages describe must include servletConfig (the predefined basicStack does) whenever their actions are Aware.

Thread per Request

A servlet is one instance for the whole life of the application, so its fields are shared state; the servlet page of this section calls them as dangerous as static fields (Servlet Fundamentals, Lifecycle and Threading). A Struts 2 action is the opposite: a new instance for every request, used by one thread, and thrown away after the response. InstanceAction makes that visible: it has an instance counter and a static counter.

InstanceAction: an instance field and a static field, side by side
public class InstanceAction extends ActionSupport {
    private static final AtomicInteger TOTAL = new AtomicInteger();
    private int callsOnThisInstance;
    private int callsOnAllInstances;
    private boolean sameAsContext;

    @Override
    public String execute() {
        callsOnThisInstance++;
        callsOnAllInstances = TOTAL.incrementAndGet();
        sameAsContext = ActionContext.getContext().getActionInvocation().getAction() == this;
        return SUCCESS;
    }

    public int getInstanceId() {
        return System.identityHashCode(this);
    }
    // plus getters for the other three fields
}
curl -s http://localhost:18080/g8act/instance.action
curl -s http://localhost:18080/g8act/instance.action
curl -s http://localhost:18080/g8act/instance.action
<p>instance=2098901688 thisInstance=1 allInstances=1 sameAsContext=true</p>
<p>instance=1002930916 thisInstance=1 allInstances=2 sameAsContext=true</p>
<p>instance=578993255 thisInstance=1 allInstances=3 sameAsContext=true</p>

The identity hash code differs on every request, the instance counter is always 1, and only the static counter grows: three requests, three instances. sameAsContext confirms that the action the invocation holds is the object that execute() runs on. In practice that gives the rules below.

  • Instance fields of an action (the properties that the form fills and the view reads) need no synchronization.

  • Anything the action reaches that outlives the request is shared by all threads: static fields, the application map, the session map (shared by all the requests of one user, which can overlap), singletons, injected services and the catalogue itself. The Catalogue of the scratch app is a CopyOnWriteArrayList for that reason.

  • An action must not be cached or kept in a field of another object. The framework creates it, uses it for one invocation and drops it; the Spring plugin’s rules for actions that are created by a Spring container are on Spring Integration.

Struts 1 was the opposite and the books on Struts 1 stress it: a Struts 1 Action was a singleton shared by all threads, and the book’s thread-safety rules (no instance fields, no per-user state in the action) do not carry over to Struts 2.

Keeping Business Logic Out of Actions

The books' data-services chapters (a JDBC helper, a Lucene index, an RSS reader, an EJB call) all make the same point, and it holds for Struts 7: the action is the adapter between the web and the model, not the model. The Getting Started tutorial describes an action class as code that executes business logic "or calls upon other classes to do that". A class that searches the catalogue, applies a business rule or talks to a database belongs in a plain Java class with no Struts and no Servlet API; the action reads the request properties, calls that class, and chooses the result. Three consequences follow: the rule is tested with an ordinary unit test, no request or ActionContext is needed; the same rule can be reused by a REST controller or a batch job; and a database session or transaction is owned by the service and never leaks into the action instance, which lives for one request only.

CatalogueService.java: the rule, with no framework types
public class CatalogueService {
    private final List<String> titles;

    public CatalogueService(List<String> titles) {
        this.titles = List.copyOf(titles);
    }

    public List<String> find(String filter) {
        if (filter == null || filter.isBlank()) {
            return titles;
        }
        String needle = filter.trim().toLowerCase();
        return titles.stream().filter(t -> t.toLowerCase().contains(needle)).toList();
    }
}
SearchAction.java: the action only adapts
public class SearchAction {
    private final CatalogueService catalogue;
    private String filter;
    private List<String> titles = List.of();

    public SearchAction() {                    // used by the framework
        this(Services.catalogue());
    }

    SearchAction(CatalogueService catalogue) { // used by tests
        this.catalogue = catalogue;
    }

    public String execute() {
        titles = catalogue.find(filter);
        return titles.isEmpty() ? "empty" : "success";
    }

    public String getFilter() { return filter; }

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

    public List<String> getTitles() { return titles; }
}

The framework needs a public no-argument constructor, so the example obtains its service from a small factory class (Services, a stand-in for a container). In a real application that factory is replaced by injection, through the Spring plugin or CDI (see Spring Integration and Dependency Injection and CDI), and the persistence work is done with Hibernate in the service layer (see Getting Started with Hibernate and Hibernate Transactions). The package-private constructor is what lets the test below run without Struts, a server or a request.

SearchActionTest.java: no framework needed
class SearchActionTest {
    private final SearchAction action =
            new SearchAction(new CatalogueService(List.of("Dune", "Emma")));

    @Test
    void successWhenTitlesMatch() {
        action.setFilter("  DU ");

        assertEquals("success", action.execute());
        assertEquals(List.of("Dune"), action.getTitles());
    }

    @Test
    void emptyWhenNothingMatches() {
        action.setFilter("zzz");

        assertEquals("empty", action.execute());
    }
}

Mapped as search (the same two results as the books action), the action answered on Tomcat 11.0.26 with Struts 7.4.0 as follows, and the two tests passed (Tests run: 2, Failures: 0; observed).

Output (curl against the scratch application)
$ curl -s 'http://localhost:18080/g14dact/search.action?filter=dun'
<ul><li>Dune</li></ul>
$ curl -s 'http://localhost:18080/g14dact/search.action?filter=zzz'
<p>No titles match zzz.</p>
$ curl -s 'http://localhost:18080/g14dact/search'
<ul><li>Dune</li><li>Neuromancer</li><li>Hyperion</li></ul>

The Bookshelf End to End: Adding a Title

The worked example ties the pieces together: two mappings for one class (the form and the submission), validation by validate(), messages from a bundle, results by code, and a listing that shows the change. It uses only the files already shown on this page; the scratch app was freshly deployed before the requests, so the catalogue starts with three titles.

flowchart TD new["GET /newBook.action"] --> inp["AddBookAction.input() returns input
(validation and workflow skip input)"] inp --> form["add-form.jsp: the empty form"] form --> post["POST /addBook.action with title"] post --> params["params: setTitle, with the annotation"] params --> val["validate()"] val -->|"blank or duplicate title"| err["addFieldError: workflow returns input"] err --> form val -->|"title is valid"| exec["execute(): add to the catalogue,
addActionMessage, return success"] exec --> added["added.jsp: message and total"] added --> list["GET /books.action lists the new title"]

The configuration is the five mappings of the mapping example; the sequence of requests and the answers:

curl -s http://localhost:18080/g8act/books.action
curl -s http://localhost:18080/g8act/newBook.action
curl -s -d title= http://localhost:18080/g8act/addBook.action
curl -s -d title=Dune http://localhost:18080/g8act/addBook.action
curl -s -d title=Ubik http://localhost:18080/g8act/addBook.action
curl -s "http://localhost:18080/g8act/books.action?filter=u"
curl -s http://localhost:18080/g8act/bookCount.action
<ul><li>Dune</li><li>Neuromancer</li><li>Hyperion</li></ul>
(the empty form, as in the input method section)
(the form again, with "Please enter a title")
(the form again, with the value Dune and "The catalogue already contains "Dune"")
<ul class="actionMessage"><li><span>Added "Ubik" to the catalogue</span></li></ul><p>Catalogue now holds: 4</p>
<ul><li>Dune</li><li>Neuromancer</li><li>Ubik</li></ul>
<p>Titles in the catalogue: 4</p>

Reading the trace: the empty submission and the duplicate stopped in validate() and rendered the input result with a field error, the valid one reached execute(), and the filter=u listing (a case-insensitive substring) now includes Ubik. One real application would finish the success path with a redirect to books (post-redirect-get) so that a browser refresh does not add the title twice; the redirect result and the message store are on Results & Result Types. The form post here is also unprotected against forged requests, which Double-Submit Tokens & Wait Pages and Security Hardening cover.

Pitfalls

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

The form page shows a validation error on the first visit (observed)

The form and the submission use one mapping; with no title parameter validate() fails and workflow returns input

Map the form to the method input (method="input"); the default stack excludes input, back, cancel and browse

A request parameter is silently ignored (observed)

The setter lacks @StrutsParameter, and struts.parameters.requireAnnotations is true in Struts 7

Annotate the setter or field; check the annotation depth for nested objects

NullPointerException on this.session or this.request (observed)

The action implements an Aware interface but its stack has no servletConfig interceptor

Extend defaultStack or basicStack, or add <interceptor-ref name="servletConfig"/>

HTTP 404 with Wrong method was defined as an action method: execute (observed)

The class has no execute and the mapping has no method attribute

Add execute, or set method to the real entry point

A mistyped URL shows the welcome page (observed)

The package has a default-action-ref

Intended; remove the default action if the 404 is wanted, and keep one per namespace

NullPointerException in getText or ActionContext in a unit test (observed)

ActionSupport reaches the text provider through the thread’s ActionContext, which a plain new does not set

Use the Struts JUnit plugin, or keep the logic in methods that do not call getText

A static method in a view expression returns nothing (observed)

OGNL static method access is removed, and static field access is off by default in Struts 7 (struts.ognl.allowStaticFieldAccess=false)

Expose a getter on the action, as getTotal() does

HTTP 404 No result defined for action ... and result surprise (observed)

The action returned a code that has no <result> in the mapping or in the global results

Map the code, or return one that is mapped; none is the only code that needs no result

A property that the view prints shows nothing

The getter is missing, is not public, or the property has a different name; the value stack only sees getters

Name the getter by the JavaBeans rule (getTitle, isActive)

A field set by one user appears for another

The action keeps request data in a static field or in a service singleton

Keep per-request data in instance fields; protect shared data (CopyOnWriteArrayList, atomics, a database)

The action name contains a dash and the generated JavaScript breaks

Dashes are not safe in names that tags turn into identifiers

Use camel case or underscores

ActionContext.getContext() is null in a background thread

The context is bound to the request thread

Pass the values you need into the task; do not call the context from other threads

Confusion between "action" in Struts and jsp:useBean-style "standard actions"

Both are called actions; they are unrelated

A JSP standard action is a tag of the page (see Standard Actions & JavaBeans); a Struts action is a controller class

The JSP standard actions that the last row mentions are explained in JSP Standard Actions and JavaBeans.

What Changed Recently

Table 6. Changes that reach action classes between the books' Struts 2.0 and Struts 7.4
Release Change

Struts 2.3

Strict dynamic method invocation: strict-method-invocation on a package and <allowed-methods> on an action

Struts 2.5

Strict method invocation is on by default in the struts-default package (per-package switch, no global one); <allowed-methods>, <global-allowed-methods> and @AllowedMethods control the callable methods; a custom configuration provider must itself allow methods, because by default only execute is allowed

Struts 6.0

The Aware interfaces move from org.apache.struts2.interceptor (and org.apache.struts2.util for ServletContextAware) to org.apache.struts2.action; the old ones are deprecated and the new methods are named with... (withSession instead of setSession). RequestAware and ParameterAware give way to ServletRequestAware and ParametersAware (migration guide, WW-4963)

Struts 7.0

Java 17 and Jakarta Servlet 6 are the minimum; the package com.opensymphony.xwork2 is renamed to org.apache.struts2, so ActionSupport is org.apache.struts2.ActionSupport and Action is org.apache.struts2.action.Action; @StrutsParameter is required by default; static field access in OGNL is disabled; struts.mapper.alwaysSelectFullNamespace=true and struts.actionConfig.fallbackToEmptyNamespace=false

Struts 7.2 to 7.4

@StrutsParameter authorization extends to chaining (opt-in: only with struts.chaining.requireAnnotations=true, whose default is false; see Action Chaining, Sessions & Scopes), cookies and the JSON and REST plugins (7.2.0), fixes for record-typed REST bodies (7.3.0), and the annotation is honoured on fluent setters (7.4.0)

The two jars give the same story in code: the 2.5.33 SessionAware has the single method setSession, the 6.12.0 version of org.apache.struts2.interceptor.SessionAware still has setSession (and a default withSession) and extends the new org.apache.struts2.action.SessionAware, and the 7.4.0 jar has only the new interface with withSession. An action written for 2.5 therefore needs its imports and its method names changed for 7.

Where the Books Are Outdated

Two of the books of the section’s bibliography deal with actions. Struts 2 in Action (Manning, 2008, chapter 3, Actions) is written for Struts 2.0 on Java 5, and Struts in Action (Manning, 2002, chapters 2 and 8) is about Struts 1, where an Action is a different thing. The concepts carry over, with the exceptions below.

Table 7. Struts 2.0 and Struts 1 material against Struts 7.4
The book’s era Today (Struts 7.4)

Imports from com.opensymphony.xwork2 (ActionSupport, Action, ActionContext)

org.apache.struts2.ActionSupport, org.apache.struts2.action.Action, org.apache.struts2.ActionContext

SessionAware and ServletRequestAware in org.apache.struts2.interceptor, methods setSession and setServletRequest

org.apache.struts2.action.SessionAware and ServletRequestAware, methods withSession and withServletRequest

ActionContext.getContext().getSession() as the normal way to reach the session

Aware interfaces first; the context only where an interface cannot be used. The context is built with ActionContext.of and with... methods

Any public setter fills the action from the request; no annotations

@StrutsParameter is required by default; unannotated setters are skipped silently

Dynamic method invocation action!method shown as a normal technique

Off by default (struts.enable.DynamicMethodInvocation=false); strict method invocation and <allowed-methods> control what can be called

URLs and links ending in .action always

The extension is configurable and the default accepts both .action and none (struts.action.extension=action,,)

Zero configuration and Codebehind in the book’s version of configuration

Replaced by the Convention plugin (@Action, @Result, @Namespace), the subject of Convention Plugin & Annotations

Struts 1 Action.execute(ActionMapping, ActionForm, request, response) returning an ActionForward; perform() before 1.1

A Struts 2 action’s method takes no argument and returns a result string; the request data arrives through setters and the scopes through Aware interfaces. ActionForm and DynaActionForm are replaced by properties on the action or a model object

A Struts 1 Action is a singleton and must be thread-safe

A Struts 2 action is one instance per request

ForwardAction, IncludeAction, DispatchAction, LookupDispatchAction, SwitchAction of Struts 1

A mapping with no class and a result (forward), the method attribute and wildcards (dispatch), the chain result; modules become packages and namespaces. Struts 1 (Classic) & Migration maps them in detail

Notes on the Official Documentation

Reading the official pages against the 7.4.0 jar showed inconsistencies that a reader will meet; they are listed so that a mismatch is not mistaken for a mistake in the example code.

  • Several core-developer pages (action-configuration, localization, default-workflow-interceptor) still name com.opensymphony.xwork2.ActionSupport and com.opensymphony.xwork2.Validateable. In 7.4.0 the classes are org.apache.struts2.ActionSupport and org.apache.struts2.Validateable (the 6.x to 7.x migration page lists the move; the localization page itself suggests org/apache/struts2/ActionSupport.properties in the same paragraph).

  • The accessing-session-request page shows application.set("myKey", "myValue") on a Map; a Map has put, not set.

  • The servlet config interceptor page lists ParameterAware, HttpParametersAware and RequestAware, which are not in the 7.4.0 jar; the interfaces present are ParametersAware, ServletRequestAware, ServletResponseAware, SessionAware, ApplicationAware, ServletContextAware, PrincipalAware and CookiesAware.

  • The action-configuration page says a class without the method "will throw an exception"; 7.4.0 answers with an HTTP 404 and a message naming the missing method.

  • The migration page lists the moved class as org.opensymphony.xwork2.Action; the real old name is com.opensymphony.xwork2.Action.

References