Action Chaining, Sessions & Scopes
|
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 tutorial on the HTTP session, the 7.4.0 Javadoc and the 7.4.0 sources
(ActionChainResult, ChainingInterceptor, ScopeInterceptor, MessageStoreInterceptor and its pre-result listener,
CreateSessionInterceptor, ClearSessionInterceptor, SessionMap), and every example was built into a Maven WAR with
the struts2-bom 7.4.0, deployed to a scratch Tomcat 11.0.26 and exercised with curl; the outputs shown are the outputs
that run produced. A Struts action lives for one request, so everything that must survive from one request to the next, or
from one action to another action in the same request, needs a deliberate carrier: a chain, a redirect with parameters, the
HTTP session, the application scope, or a copy of the messages in the session. This page covers those carriers. The running
scenario is the Bookshelf catalogue of Part II: a reader adds a book, browses a list with a remembered filter, keeps a
reading cart in the session, and is told on the next page whether the form was saved. The pages of this part on actions,
interceptors and results are linked, and so are the pages on the value stack,
parameter injection, validation,
model-driven actions, file upload and
tokens.
Where State Can Live
Before a single line of configuration, a map of the carriers helps, because most mistakes with chaining and sessions come from using the wrong one. The first column is the Bookshelf need; the other columns say how long the data lives and who can see it.
| Carrier | Lives for | Visible to | Typical Bookshelf use |
|---|---|---|---|
Action instance fields |
One request (one action instance per request) |
That action and the view that renders it (through the value stack) |
The submitted title and author of the "add book" form |
Value stack with chained actions |
One request, across several actions |
Every action in the chain and the final view |
A confirmation page that shows what an earlier action computed |
Request parameters of a redirect |
The next request only, in the URL |
Anyone who sees or edits the URL |
|
Request attributes ( |
One request (and its forwards and includes) |
Code running in that request |
The name of the servlet or action that served the page |
HTTP session ( |
Until the session expires or is invalidated (default 30 minutes idle on Tomcat, set in |
One browser (one cookie) |
The reading cart, the logged-in reader, the remembered filter |
Application scope ( |
Until the web application stops |
Every user |
A shared setting such as the page length |
Session-stored messages (store interceptor) |
Until the next request that retrieves them |
One browser |
"Book saved" after a redirect |
Servlet concepts of scope (request, session, application) are the ones of the Servlet fundamentals page; Struts exposes the same three scopes as maps. The JSP implicit objects and scopes page
shows the same scopes from the JSP side with sessionScope and applicationScope.
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 (the descriptor of the Struts getting started page), and a package
default that extends struts-default in the root namespace plus a package admin in the namespace /admin. The views
are plain-text JSPs with session="false" so that a view never creates a session by itself (the reason for that is in the
section on sessions). The default extension of 7.4.0 is struts.action.extension=action,,, so /add-chain and
/add-chain.action reach the same mapping, and the constants struts.parameters.requireAnnotations (request parameters
need @StrutsParameter, see Parameter Injection and @StrutsParameter) and struts.allowlist.enable have their 7.4.0 defaults. The target action of most chain
examples is ConfirmAction; its show.jsp prints the properties it finds on the value stack.
public class AddBookAction extends ActionSupport {
private String title;
private String author;
private String newId;
@Override
public String execute() {
newId = "B-" + (title == null ? 0 : title.length());
addActionMessage("Added " + title);
return SUCCESS;
}
public String getTitle() { return title; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
@StrutsParameter public void setAuthor(String author) { this.author = author; }
public String getNewId() { return newId; }
public List<String> getShelves() { return List.of("Fiction", "Science"); }
}
public class ConfirmAction extends ActionSupport {
private String title;
private String author;
private String newId;
@Override
public String execute() {
addActionMessage("Confirm ran");
return SUCCESS;
}
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getNewId() { return newId; }
public void setNewId(String newId) { this.newId = newId; }
}
AddBookAction has annotated setters because a request parameter must reach it; ConfirmAction has plain setters because
nothing is supposed to reach it from a request. That difference matters in the section on chaining and security.
The classes ActionSupport and StrutsParameter are described on the Actions page.
Action Chaining
Action chaining runs a second action inside the same request, after the first one returned a result code. It has two
parts that the framework combines: a result type, chain, which starts the second action, and an interceptor, chain,
which copies properties from the first action into the second. The Core Developer Guide opens its page with a warning
that the rest of this section repeats with evidence: as a rule, action chaining is not recommended, and the Redirect After
Post technique should be explored first.
The chain Result
The chain result type, registered in struts-default.xml as org.apache.struts2.result.ActionChainResult, "invokes an
entire other action, complete with its own interceptor stack and result". The target is a full action invocation: it runs
its interceptors, its method and its result, and the response is whatever the target’s result writes. The result of the
first action is never rendered. The parameters of the result are:
| Parameter | Meaning |
|---|---|
|
The name of the action to chain to. This is the parameter that the short form |
|
The namespace of the target; when it is not set, the namespace of the current action |
|
A method of the target action to invoke instead of |
|
Comma-separated action names that the infinite-recursion check ignores (explained below) |
The simplest chain uses the default parameter. add-chain runs AddBookAction and, when it returns success, chains to
confirm; the second action uses the default stack, which contains the chain interceptor (see the next section).
<package name="default" extends="struts-default" namespace="/">
<action name="add-chain" class="org.example.bookshelf.AddBookAction">
<result type="chain">confirm</result>
</action>
<action name="confirm" class="org.example.bookshelf.ConfirmAction">
<result>/WEB-INF/views/show.jsp</result>
</action>
</package>
$ curl "http://localhost:18080/g9chain/add-chain?title=Dune&author=Herbert"
title=Dune
author=Herbert
newId=B-4
shelves=[Fiction, Science]
approved=
messages=[Confirm ran]
errors=[] fieldErrors={}
The output shows what chaining does. title and author were set by the params interceptor on AddBookAction and copied
by the chain interceptor onto ConfirmAction; newId was computed by AddBookAction.execute() and copied too. The line
shelves comes from a property that only AddBookAction has: the view found it because the first action stays on the
value stack below the second one. And messages shows only Confirm ran: the message Added Dune that the first action
added is not copied, because the interceptor does not copy messages, errors or field errors by default.
The browser’s URL is still /add-chain; there was one request and one response. That is the first of the problems of
chaining: a reload repeats the first action (for a form that creates a record, a duplicate), and the address bar names an
action whose result the user never sees.
Chaining to Another Namespace or Method
The namespace parameter reaches an action in another package. With the admin package in the namespace /admin, which
has its own confirm action, the first action can chain across namespaces:
<action name="add-chain-ns" class="org.example.bookshelf.AddBookAction">
<result type="chain">
<param name="actionName">confirm</param>
<param name="namespace">/admin</param>
</result>
</action>
<package name="admin" extends="struts-default" namespace="/admin">
<action name="confirm" class="org.example.bookshelf.ConfirmAction">
<result>/WEB-INF/views/show.jsp</result>
</action>
</package>
The run of add-chain-ns?title=Dune&author=Herbert printed the same title, author and newId as the first chain, so the
namespace parameter, like the target’s stack, behaves the same way as in the same namespace. See the
Packages and namespaces page for how a namespace is chosen.
The method parameter invokes another method of the target. In Struts 7 the method must be allowed on the target, as for
any action invocation (the allowed-methods element; the default is execute and the methods are checked at invocation,
not only for the request URL). Without it the chain ends in an error page:
<action name="add-chain-method" class="org.example.bookshelf.AddBookAction">
<result type="chain">
<param name="actionName">method</param>
<param name="method">audit</param>
</result>
</action>
<action name="method" class="org.example.bookshelf.MethodAction">
<result>/WEB-INF/views/show.jsp</result>
<allowed-methods>execute,audit</allowed-methods>
</action>
With the allowed-methods element the run answered messages=[audit() ran] (the method audit of MethodAction
ran, execute did not). Without the element the same configuration answered HTTP 404 with the message
Method audit for action method is not allowed!. See the Wildcard mappings and method invocation
page for allowed-methods.
Chain History and Infinite Recursion
The chain result keeps a history of the actions invoked in the current chain (the key CHAIN_HISTORY of the action
context, a list of namespace/action!method strings). Before it invokes the target it checks whether the same
namespace, action and method is already in the history; if it is, it throws a StrutsException "Infinite recursion detected"
with the history. Two actions that chain to each other end like that:
<action name="loop-a" class="org.example.bookshelf.LoopAction">
<result type="chain">loop-b</result>
</action>
<action name="loop-b" class="org.example.bookshelf.LoopAction">
<result type="chain">loop-a</result>
</action>
A request for loop-a answered HTTP 500 with the message Infinite recursion detected and the history starting
//loop-a!execute. The parameter skipActions is the escape hatch, and its documentation is easy to misread: the Core
Developer Guide calls it the list of "actions that could be chained to", but in the 7.4.0 source it is only a list of action
names that the recursion check does not apply to (the check compares the action name only, not the namespace). A bounded
loop, an action that chains to itself a fixed number of times, needs it:
public class CountAction extends ActionSupport {
private int round;
@Override
public String execute() {
var request = ServletActionContext.getRequest();
Integer n = (Integer) request.getAttribute("round");
round = n == null ? 1 : n + 1;
request.setAttribute("round", round);
return round < 3 ? "again" : SUCCESS;
}
public int getRound() { return round; }
public String getTitle() { return "round " + round; }
}
<action name="count" class="org.example.bookshelf.CountAction">
<result name="again" type="chain">
<param name="actionName">count</param>
<param name="skipActions">count</param>
</result>
<result>/WEB-INF/views/show.jsp</result>
</action>
With skipActions the request to count printed title=round 3 (three rounds in one request); the same mapping without
the parameter, mapped as count-bad, answered HTTP 500 with Infinite recursion detected: [//count-bad!execute, //count-bad,
not in `struts.xml.
Chain Only Runs for the chain Result
The chain interceptor does nothing unless an action is already below the current one on the value stack and the result
of the current invocation is a chain result (or no result yet): in the 7.4.0 source it copies only when the stack holds
more than one object. A request that goes straight to confirm, with no first action, is therefore unaffected, and the
interceptor is safe in every stack, which is why defaultStack and paramsPrepareParamsStack contain it. The stack
basicStack, and any stack the mapping builds without chain, does not copy:
<action name="add-chain-plain" class="org.example.bookshelf.AddBookAction">
<result type="chain">confirm-plain</result>
</action>
<action name="confirm-plain" class="org.example.bookshelf.ConfirmAction">
<interceptor-ref name="basicStack"/>
<result>/WEB-INF/views/show.jsp</result>
</action>
The run printed title=, author= and newId= empty, and shelves=[Fiction, Science] present: the chain happened (the
first action is still on the stack and the view can read shelves from it), but no property was copied, so
The own title of ConfirmAction stayed null and OGNL found that null on the top of the stack before it looked below.
The distinction between "the first action is on the stack" and "its properties were copied" is the one that explains
most surprises with chaining.
The Chaining Interceptor
The chain interceptor (org.apache.struts2.interceptor.ChainingInterceptor) "copies all the properties of every object in
the value stack to the currently executing object, except for any object that implements Unchainable`". The 7.4.0 source
shows the algorithm: take the value stack’s root, drop the top (the target action itself), reverse the rest so that the
oldest object comes first, and copy each object that is not `Unchainable onto the action with the reflection provider.
Later objects overwrite earlier ones. The interceptor works for anything on the stack, not only actions: a model pushed
by ModelDriven is copied as well.
What Is Copied and What Is Not
The copying goes through the setters of the target and the getters of the source. A property that has a getter on the
source and a setter on the target with a compatible name is copied; everything else is skipped silently. Three collections
of a ValidationAware action are excluded by default and three constants switch them on:
| Constant (7.4.0) | Default | Effect |
|---|---|---|
|
|
Copy the action messages of the source actions |
|
|
Copy the action errors |
|
|
Copy the field errors |
The Core Developer Guide names the constants struts.xwork.chaining.copyErrors, struts.xwork.chaining.copyFieldErrors
and struts.xwork.chaining.copyMessages. In 7.4.0 the names are the ones above, struts.chaining.* (the StrutsConstants
class has struts.chaining.copyErrors, struts.chaining.copyFieldErrors and struts.chaining.copyMessages). The old
names are not an error: a WAR with <constant name="struts.xwork.chaining.copyMessages" value="true"/> started and ran, and
add-chain still printed messages=[Confirm ran], so the setting was silently ignored. A WAR with
<constant name="struts.chaining.copyMessages" value="true"/> printed messages=[Added Dune, Confirm ran].
Narrowing the Copy: includes and excludes
The interceptor’s parameters includes and excludes limit the properties that are copied; the page warns that giving both
has undefined results. The stack that carries them is chainStack, whose reference name is chain (so the parameter prefix
is chain.). To copy only the title:
<action name="add-chain-includes" class="org.example.bookshelf.AddBookAction">
<result type="chain">confirm-includes</result>
</action>
<action name="confirm-includes" class="org.example.bookshelf.ConfirmAction">
<interceptor-ref name="chainStack">
<param name="chain.includes">title</param>
</interceptor-ref>
<result>/WEB-INF/views/show.jsp</result>
</action>
The run printed title=Dune and empty author and newId. The opposite, chain.excludes set to author,newId, printed the
same three lines, and one more line that deserves attention: messages=[Added Dune, Confirm ran]. The message copied
although struts.chaining.copyMessages was not set. The reason is in the source: the default exclusion of actionErrors,
actionMessages and fieldErrors is built only when the excludes parameter is null. An explicit excludes list
replaces the built-in one, so a list that does not name the three properties lets messages and errors through.
When excludes is used, add actionErrors, actionMessages and fieldErrors to it unless copying them is the intent.
Unchainable
An object that implements the marker interface org.apache.struts2.Unchainable is never used as a source. A source action
that must not leak its properties declares it:
public class UnchainedAction extends AddBookAction implements Unchainable {
}
Chained from add-unchainable?title=Dune&author=Herbert, the target printed empty title, author and newId; its
messages were again only Confirm ran.
Chaining and Security
Chaining makes the data path into the target implicit: the target is populated from whatever happens to be on the value
stack. Before Struts 7.2, a property of the first action that no request could set, because its setter had no
@StrutsParameter, still reached the target through the chain interceptor, which ignored the annotation. Since 7.2.0 the
annotation extends to chaining, but only if the constant struts.chaining.requireAnnotations is true; its default is
false, so a default application keeps copying every property. The Bookshelf shows the risk with two actions:
public class ApproveAction extends ActionSupport {
private String title;
@Override
public String execute() { return SUCCESS; }
// a public property that a request parameter could never reach, but the chain can
public boolean isApproved() { return true; }
public String getTitle() { return title; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
}
public class SecretAction extends ActionSupport {
private boolean approved;
private String title;
@Override
public String execute() { return SUCCESS; }
public boolean isApproved() { return approved; }
public void setApproved(boolean approved) { this.approved = approved; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
}
<action name="approve-chain" class="org.example.bookshelf.ApproveAction">
<result type="chain">secret</result>
</action>
<action name="secret" class="org.example.bookshelf.SecretAction">
<result>/WEB-INF/views/show.jsp</result>
</action>
A request straight to secret?approved=true&title=Dune printed approved=false and an empty title: the setters have no
@StrutsParameter, so the params interceptor refused them. A request to approve-chain?title=Dune printed
title=Dune and approved=true: the chain copied approved from the first action’s getter to the target’s setter, which a
request could not reach. In an application where the target action trusts approved, that is a privilege path.
The guide’s advice, which the example confirms, is to carry no authorization, trust, identity or approval state across a chain:
re-derive it from the session or the security context inside the target.
Two defences exist, and they combine. The constant struts.chaining.requireAnnotations set to true (a global constant,
there is no per-interceptor override) makes the interceptor copy only the target setters that carry @StrutsParameter:
<constant name="struts.chaining.requireAnnotations" value="true"/>
With the constant, approve-chain?title=Dune printed an empty title and approved=false (nothing was copied, because
SecretAction has unannotated setters), add-chain printed no title, author or newId either (ConfirmAction is not
annotated), and a chain to an annotated target worked: add-chain-annotated?title=Dune&author=Herbert to the action
show-book (which has @StrutsParameter on its setters) printed title=Dune and author=Herbert. The interceptor is
fail-closed: if the target class cannot be introspected, nothing is copied. The rejected properties are logged at WARN.
The second defence is Unchainable on classes that must never be a source, and includes/excludes to copy only what the
target needs.
The remark of the printed newId=B-4 in the annotated run is not a leak: the target has no newId, so the view read
it from the first action that is still on the value stack. Chaining always leaves the whole chain visible to the view,
whatever the interceptor copies.
Redirect After Post Instead of Chaining
The guide’s recommendation, "explore other options, such as the Redirect After Post technique", has a concrete reason in
the Bookshelf. After a POST that adds a book, a chain answers the POST with the confirmation page. The browser’s current
page is the result of a POST, so reload and back-and-forth re-submit the form, and the address bar still shows the
action that changed the data. A redirect after post ends the POST with a 302 to a GET URL; the browser loads the
confirmation page as a new request, and a reload only repeats the GET.
<action name="add-redirect" class="org.example.bookshelf.AddBookAction">
<result type="redirectAction">
<param name="actionName">show-book</param>
<param name="title">${title}</param>
<param name="author">${author}</param>
</result>
</action>
<action name="show-book" class="org.example.bookshelf.ShowBookAction">
<result>/WEB-INF/views/show.jsp</result>
</action>
The first request, add-redirect?title=Dune&author=Herbert, answered 302 with
Location: /g9chain/show-book.action?title=Dune&author=Herbert; following the redirect printed title=Dune and
author=Herbert. The values that cross the redirect are in the URL, so they are visible and editable, and they go through the
params interceptor of the target, which means the target’s setters need @StrutsParameter (ShowBookAction has it).
A first version of this example redirected to ConfirmAction, whose setters are not annotated, and the confirmation page
printed an empty title: the redirect worked and the parameters were silently ignored. See the
Results and result types page for the redirectAction parameters
and the redirect result.
| Aspect | chain |
redirectAction |
dispatcher (a JSP result) |
|---|---|---|---|
Requests |
One |
Two (the second is a GET) |
One |
URL after the response |
The first action’s URL |
The second action’s URL |
The first action’s URL |
Reload after a POST |
Repeats the first action |
Repeats only the GET |
Repeats the first action |
Data that reaches the second step |
Properties copied from the value stack, implicit |
Request parameters in the URL (visible, bounded) and, optionally, messages stored in the session |
Nothing is copied; the view reads the first action |
Second step’s interceptors and result |
Run in full |
Run in full, as a separate request |
Not an action, so none |
Where trust problems arise |
The copy ignores |
Parameters of the URL go through |
None added |
The guide gives one legitimate use of chaining: a lookup list, such as the shelves of a dropdown that a form needs. Even there the guide mentions two alternatives, executing an action from the view with the action tag, and the redirect action result. A shared service called from both actions, or a method of the first action, is usually the cleaner design: the guide asks "could the logic in Action2 be pushed back to a support class or business facade so that Action1 can call it too?" and recommends actions that are as short as possible and only call methods.
Sessions and the SessionMap
A web application recognizes a browser across requests with the HTTP session, a server-side object identified by the
JSESSIONID cookie (or a rewritten URL when cookies are off). The Servlet API’s HttpSession stores attributes by name;
Struts hides it behind a java.util.Map<String, Object> so that an action does not need the servlet API and a unit test can
pass a plain HashMap. The map is an instance of org.apache.struts2.dispatcher.SessionMap, which delegates each
get, put and remove to the HttpSession.
SessionAware and the Other Aware Interfaces
The way to receive the map is the interface org.apache.struts2.action.SessionAware, which has one method,
withSession(Map<String, Object>). The servletConfig interceptor of the default stack calls it before the action runs.
Its siblings do the same for the other scopes: ApplicationAware (withApplication), ServletRequestAware
(withServletRequest) and ServletResponseAware. The Core Developer Guide recommends these interfaces over
ActionContext.getContext().getSession(), which it calls a bad practice.
The Bookshelf’s reading cart is a list in the session. The action, a normal SessionAware class with the four methods the
URL pattern cart-* selects:
public class CartAction extends ActionSupport implements SessionAware {
private Map<String, Object> session;
private String title;
@Override
public void withSession(Map<String, Object> session) {
this.session = session;
}
@SuppressWarnings("unchecked")
private List<String> cart() {
return (List<String>) session.computeIfAbsent("cart", k -> new ArrayList<String>());
}
public String add() {
cart().add(title);
return SUCCESS;
}
@Override
public String execute() { return SUCCESS; }
public String clear() {
session.remove("cart");
return SUCCESS;
}
public List<String> getCart() { return cart(); }
public String getTitle() { return title; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
public String getSessionClass() { return session.getClass().getSimpleName(); }
}
<action name="cart-*" class="org.example.bookshelf.CartAction" method="{1}">
<result>/WEB-INF/views/cart.jsp</result>
<allowed-methods>add,clear,execute</allowed-methods>
</action>
The view reads the session through the OGNL root name #session: <s:property value="#session.cart"/> in cart.jsp. A run
with one cookie jar:
$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-add?title=Dune"
cart=[Dune] mapClass=SessionMap
$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-add?title=Emma"
cart=[Dune, Emma] mapClass=SessionMap
$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-clear"
cart= mapClass=SessionMap
The tutorial page uses an older signature, public void setSession(Map<String, Object> session), in its HelloWorldAction
and describes an interceptor that injects the session "by calling the setSession method". In 7.4.0 the interface has
withSession; a class that still has a setSession method but does not implement the new interface receives nothing.
The Aware interfaces and the 6.0 rename are covered on the Actions page.
The Session Is Created Lazily
The SessionMap does not create an HttpSession when it is built. The servlet session is created on the first put (and
by anything that calls request.getSession()). The Bookshelf shows the two sides. The action no-session (an action that
only reads, rendering a session="false" JSP) answered without a Set-Cookie header; scopes, which calls
session.merge("visits", 1, ...), answered with Set-Cookie: JSESSIONID=...; Path=/g9chain; HttpOnly. A JSP without
session="false" creates a session of its own on the first request, which is the reason the Bookshelf views switch it off:
a page that is only a view must not create a session per visitor (for crawlers, a very large number of them).
The same property decides what happens in the view’s #attr. The Core Developer Guide says that the #attr stack
value searches the page, request, session and application scopes in that order. In the Bookshelf a JSP with
<%@ page session="false" %> printed attr.visits= (empty) for a value that #session.visits printed as 2, while
#attr.servedBy (a request attribute) and #attr.opened (an application attribute) were found; the same view
with the default session="true" printed attr.visits=3. The guide does not mention that a JSP that opted out of the
session cannot see session attributes through #attr. Use #session.name when the scope matters, which it nearly always does.
public class ScopesAction extends ActionSupport implements SessionAware, ApplicationAware, ServletRequestAware {
// withSession, withApplication and withServletRequest store the three scopes
@Override
public String execute() {
session.merge("visits", 1, (a, b) -> (Integer) a + (Integer) b);
application.putIfAbsent("opened", "yes");
request.setAttribute("servedBy", "ScopesAction");
return SUCCESS;
}
}
The Session and Concurrency
The session is shared by every request of one browser: a page that loads three Ajax calls at once runs three actions
with three action instances and one session map. The action instance is per request, the session is not, so the usual
servlet advice applies to anything stored in the session: store immutable values or thread-safe collections, replace values
instead of mutating them in place, and make session objects Serializable so that a server that persists or replicates
sessions can handle them. The cart list above is mutated in place; two simultaneous cart-add requests can interleave,
and a production cart would use a thread-safe list or a service. The scope interceptor of the next section adds a lock around
the session, and no other built-in interceptor does.
Session Lifetime and Fixation
The idle timeout of the session is not a Struts setting; it is session-config in web.xml (the Servlet fundamentals page covers it), with a default of 30 minutes on Tomcat. SessionMap.invalidate() ends the session, and
the next put starts a new one with a new identifier. That is the answer to session fixation: when a reader logs in, the
identifier the browser sent before the login must not remain valid after it. The LoginAction of the scratch app renews the
session when it is asked to:
public class LoginAction extends ActionSupport implements SessionAware, ServletRequestAware {
// fields and the two with-methods omitted
@Override
public String execute() {
if (renew) {
if (session instanceof SessionMap sm) {
sm.invalidate();
}
}
session.put("user", user);
return SUCCESS;
}
}
Two requests to login?user=ann printed the same session id (303C73...), and login?user=ann&renew=true printed a different one
(1577A5...); the user attribute survived because it was put after the invalidation. In a real application the
renewal is done with request.changeSessionId() or by the container’s authentication (the Servlet fundamentals page), and the attribute holds a user id, never a password.
The Exposed Session Trap
The Getting Started tutorial on the HTTP session ends with a security warning that is easy to skip. An action that implements
SessionAware must not have a public getSession(): if the session map is reachable by name from a request, a request
can write into it, because the params interceptor sets nested properties by their path. The tutorial tells the reader to
do two things: omit the getter, and have the action implement ParameterNameAware (refusing names that contain
session or request), or, for a whole package, set params.excludeParams on the default stack. The scratch app reproduces
the dangerous shape with a getter that the annotation allows:
public class ExposedSessionAction extends ActionSupport implements SessionAware {
private Map<String, Object> session;
@Override
public void withSession(Map<String, Object> session) { this.session = session; }
@Override
public String execute() { return SUCCESS; }
// the classic mistake: a getter for the session map, here even annotated for parameter binding
@StrutsParameter(depth = 1)
public Map<String, Object> getSession() { return session; }
// the same map under another name, annotated
@StrutsParameter(depth = 1)
public Map<String, Object> getStore() { return session; }
}
Against a browser that had logged in as ann, the request exposed?session.admin=true did nothing (the default parameter exclusion patterns of the params interceptor are the likely reason, since the same
annotated getter under another name was open), but exposed?store.admin=true
wrote admin=true into the real session: the next request to login printed admin=true. The defence by name covers only
the exact words in the pattern; a getter that returns the map under another name is open. The rule is therefore two parts.
Never expose the session map, or any other scope map, through a public getter, and keep @StrutsParameter away from anything
whose type is a Map of session data. In 7.x the annotation is the second line of defence, because without it the request
parameter does not reach the property at all; the example needed it to be reachable. The Interceptors
page shows how the parameters interceptor’s exclusion patterns are replaced, not extended, when they are overridden.
createSession and clearSession
Two small interceptors, in struts-default.xml but in no built-in stack, manage the session as a whole.
| Interceptor | What it does | When it is used |
|---|---|---|
|
If |
Before a result that writes the response without being able to start a session later, notably a FreeMarker template with the |
|
Calls |
A "log out" or "start over" action; for a real logout also invalidate the session |
<action name="cart-session-created" class="org.example.bookshelf.CartAction">
<interceptor-ref name="createSession"/>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/cart.jsp</result>
</action>
<action name="clear-session" class="org.example.bookshelf.CartAction">
<interceptor-ref name="clearSession"/>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/cart.jsp</result>
</action>
The first request to cart-session-created with no cookie answered Set-Cookie: JSESSIONID=...; Path=/g9chain; HttpOnly
although the action wrote nothing to the session; after two cart-add requests, clear-session printed an empty cart, and
the session identifier was unchanged (the cookie jar kept the same JSESSIONID, and no new Set-Cookie was sent). The guide’s page for cookieProvider repeats the sentence "This interceptor clears the HttpSession" by mistake (an inconsistency noted on the
interceptors page); the sentence is correct only on the clearSession page.
The Scope Interceptor
A session map handled by hand needs the same code in every action: read a value when the action starts, write it back when
it ends. The scope interceptor (ScopeInterceptor) does that for named properties of the action. The guide introduces it
for "wizard-like functionality", and its two motivating cases are application-wide parameters such as the page length of
a list ("pageLen"), and values that must be remembered for the next step: a filter, a sort order, the half-filled form of
a wizard.
How It Works
Before the action runs, the interceptor sets the listed properties of the action from values stored in the session (or in the
application map). After the action has run, and before the result is rendered, it reads the properties back from the value
stack and stores them. It uses a pre-result listener for the second step, so the saved state is the state the view is
about to show, including values changed by interceptors that run later in the stack. The session is locked around the
action so that two requests of the same session do not interleave their reads and writes; the lock is for the session,
not the application map, and the interceptor throws a StrutsException "Deadlock in session lock" if the lock is not obtained
after about 30 seconds of waiting.
Parameters
| Parameter | Meaning (checked against the 7.4.0 source) |
|---|---|
|
Comma-separated action properties to keep in the session |
|
Comma-separated action properties to keep in the application scope (shared by every user) |
|
Prefix of the attribute names. |
|
|
|
The name of the request parameter that behaves like |
|
|
|
Whether to create the session if none exists (the guide gives no default; the source says |
A Remembered Filter and a Shared Page Length
The Bookshelf’s list page has a filter, an orderBy and a pageLen. The reader’s filter and orderBy should survive
from one visit to the list to the next; the page length is a setting that, for the sake of the example, is shared by all
users, so it goes to the application map.
public class BrowseAction extends ActionSupport {
private String filter = "all";
private String orderBy = "title";
private int pageLen = 10;
@Override
public String execute() { return SUCCESS; }
public String getFilter() { return filter; }
@StrutsParameter public void setFilter(String filter) { this.filter = filter; }
public String getOrderBy() { return orderBy; }
@StrutsParameter public void setOrderBy(String orderBy) { this.orderBy = orderBy; }
public int getPageLen() { return pageLen; }
@StrutsParameter public void setPageLen(int pageLen) { this.pageLen = pageLen; }
}
<action name="browse-before" class="org.example.bookshelf.BrowseAction">
<interceptor-ref name="scope">
<param name="session">filter,orderBy</param>
<param name="application">pageLen</param>
<param name="autoCreateSession">true</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/browse.jsp</result>
</action>
The run, with one cookie jar, and the last request from a second browser that has no cookie:
$ curl -c jar -b jar "http://localhost:18080/g9chain/browse-before?filter=sci&orderBy=author&pageLen=25"
filter=sci orderBy=author pageLen=25 session={struts.ScopeInterceptor:class org.example.bookshelf.BrowseActionfilter=sci, ...orderBy=author}
$ curl -c jar -b jar "http://localhost:18080/g9chain/browse-before"
filter=sci orderBy=author pageLen=25 session={...filter=sci, ...orderBy=author}
$ curl "http://localhost:18080/g9chain/browse-before"
filter=all orderBy=title pageLen=25 session={...filter=all, ...orderBy=title}
The second request, without parameters, got sci and author from the session. The third, from a browser with no
session, got the action’s defaults all and title for the session-scoped properties, and 25 for pageLen: that was
stored in the application scope by the first browser and is now the page length of everyone. Application scope has no
per-user separation and no lock; a value there is global state, which is the reason for the guide’s note that application
level consistency is not guaranteed. The keys in the session map are the key prefix and the property name glued together,
with no separator: ...BrowseActionfilter.
Order in the Stack Matters
The placement of scope relative to defaultStack changes the behaviour. The first mapping above puts scope before the
default stack: the interceptor restores the stored values first, then the params interceptor of the default stack applies
the request parameters on top, so a new request parameter wins and is saved at the end. The mapping browse-after puts it
after:
<action name="browse-after" class="org.example.bookshelf.BrowseAction">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="scope">
<param name="session">filter,orderBy</param>
<param name="autoCreateSession">true</param>
</interceptor-ref>
<result>/WEB-INF/views/browse.jsp</result>
</action>
After one request with filter=sci&orderBy=author, a request with filter=newval printed filter=sci: the request parameter was
applied, then the interceptor overwrote it with the stored value. The guide’s first example lists basicStack, hibernate
and scope, in that order, which has the same effect for the parameters in basicStack; the guide does not say so.
Put scope before the interceptor that fills the action from the request when the request should win, which is the
usual case (the reader changes the filter), and after it only when stored values must be authoritative.
Reset and Wizards
A request parameter named session.reset (the sessionReset parameter) skips the restore and saves the action’s current
values, so the stored values return to the defaults. After browse-before?session.reset=1 the output was filter=all
orderBy=title and the following plain request printed the same, so the reset stuck. The parameter type gives the same
mechanism a start and an end, for the multi-step form of a wizard. The Bookshelf’s "add a book in three steps" stores title,
author and shelf across three actions that share the key wizard:
<action name="wizard-1" class="org.example.bookshelf.WizardAction">
<interceptor-ref name="scope">
<param name="key">wizard</param>
<param name="session">title,author,shelf</param>
<param name="type">start</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/wizard.jsp</result>
</action>
<action name="wizard-2" class="org.example.bookshelf.WizardAction">
<interceptor-ref name="scope">
<param name="key">wizard</param>
<param name="session">title,author,shelf</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/wizard.jsp</result>
</action>
<action name="wizard-3" class="org.example.bookshelf.WizardAction">
<interceptor-ref name="scope">
<param name="key">wizard</param>
<param name="session">title,author,shelf</param>
<param name="type">end</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/wizard.jsp</result>
</action>
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-1?title=Dune&author=Herbert&shelf=SF"
title=Dune author=Herbert shelf=SF session={wizardshelf=SF, wizardauthor=Herbert, wizardtitle=Dune, ...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-2"
title=Dune author=Herbert shelf=SF session={wizardshelf=SF, ...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-2?shelf=Classics"
title=Dune author=Herbert shelf=Classics session={wizardshelf=Classics, ...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-3"
title=Dune author=Herbert shelf=Classics session={nonce=...}
$ curl -c jar -b jar "http://localhost:18080/g9chain/wizard-2"
title= author= shelf= session={wizardtitle=NULL, wizardshelf=NULL, wizardauthor=NULL, ...}
Step 1 starts the wizard (the properties are saved but not restored), step 2 reads the earlier values and lets a request
overwrite one of them, step 3 reads them and then removes the session keys (type="end"). The session map after step 3
holds only a nonce attribute that the default stack keeps there, and none of the scope interceptor’s keys; the wizard’s keys are gone. The last
request shows the next surprising detail: running wizard-2 after the end re-created the keys with the value NULL. The
interceptor never skips a null; it stores a placeholder object (NULLClass, whose toString is NULL) so that a
session that gets serialized does not contain null references (the fix of WW-1803), and every action that lists a property
saves it again after each execution. A session map printed in the view shows those placeholders; code that reads the keys of
the scope interceptor directly would see them, which is another reason to read the properties from the action instead.
Sharing a Value Between Two Actions with a Key
The default key is the class of the action, so two actions of different classes do not see each other’s values. The guide’s
second example, with a literal key and a getter on the first action and a setter on the second, passes an arbitrary
object between two actions. The Bookshelf version has ExportAction and BrowseAction, which both list filter under the
key shelf:
<action name="export-key" class="org.example.bookshelf.ExportAction">
<interceptor-ref name="scope">
<param name="key">shelf</param>
<param name="session">filter</param>
<param name="autoCreateSession">true</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/browse.jsp</result>
</action>
<action name="browse-key" class="org.example.bookshelf.BrowseAction">
<interceptor-ref name="scope">
<param name="key">shelf</param>
<param name="session">filter</param>
<param name="autoCreateSession">true</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/browse.jsp</result>
</action>
After export-key?filter=fantasy (which stored shelffilter=fantasy in the session), a request to browse-key printed
filter=fantasy. With the default key, export-nokey stored the value under its own class key and browse-action-key
(with key ACTION) used a third key struts.ScopeInterceptor:/:browse-action-key: none of them saw the others' filter.
Without a key, as the guide says, the interceptor stores the values but does not set them on the other action.
Details the Guide Leaves Out
Three details of the 7.4.0 behaviour were checked against the running scratch app.
-
autoCreateSessionhas no visible effect under the servlet dispatcher. The interceptor decides fromActionContext.getSession(), and under the servlet dispatcher that is always aSessionMap(it creates the servlet session on the firstput), nevernull.browse-nocreatehasautoCreateSessionfalseand a request with no cookie still storedfilter=qin the session and answered with a new session; the defaulttruebehaved the same way. The parameter matters only where the action context has no session map. -
An invalid
typeis silently ignored. The setter throwsIllegalArgumentException("Only start or end are allowed arguments for type")for any other value. A mapping with<param name="type">middle</param>,wizard-bad, deployed and ran, with the interceptor behaving as if the parameter was absent (the properties were restored and saved as for no type). The guide says "any other value throws IllegalArgumentException"; in 7.4.0 the failing setter is caught while the interceptor is built. -
scopedModelDrivenis a related interceptor, not the same one. It stores the model of aScopedModelDrivenaction in a scope under a name; it is indefaultStack. A mapping with<param name="scope">session</param>and<param name="name">bookModel</param>stored aBookFormin the session underbookModel, and a second request with no parameters printed the stored title and author. The interceptor, theModelDrivencontract and the CRUD example belong to ModelDriven, Preparable & CRUD; the point here is only that "what the scope interceptor does" and "where a scoped model is stored" are two mechanisms.
The Message Store Interceptor: Messages Across a Redirect
A redirect after post solves the double submission, and creates a new problem: the action messages ("Book saved") and the
field errors of the first request live in the first action instance, which is gone when the second request starts. The
store interceptor (MessageStoreInterceptor) copies them into the session at the end of one request and puts them back
into the next action, where the view prints them as usual. This is the Struts form of the "flash message" of other
frameworks.
Modes
The interceptor works only with a ValidationAware action (every ActionSupport is) and, as the guide says, only if a session
exists: "if no session exists, nothing will be stored", so the application is responsible for opening the session.
The mode comes from the operationMode parameter:
operationMode |
Behaviour (guide and 7.4.0 source) |
|---|---|
|
The default: the interceptor does nothing |
|
After the action runs, before the result, the action’s errors, messages and field errors go to the session |
|
Before the action runs, the stored errors, messages and field errors are merged into the action and removed from the session |
|
Always retrieves, and stores automatically when the result is a redirect (a result whose class implements |
The parameters are operationMode, allowRequestParameterSwitch (default true) and requestParameterSwitch (default
operationMode, the name of the request parameter that can set the mode). The guide says AUTOMATIC stores when the result
is a ServletRedirectResult; the 7.4.0 pre-result listener tests for the interface Redirectable, which more result types
implement (redirectAction is one).
A Save Form with Validation Errors
The Bookshelf’s form action validates the title. When it fails it must send the reader back to the form with the error, and a success must send the reader to the form with "Saved". Both responses are redirects, so the messages go through the store:
public class SubmitBookAction extends ActionSupport {
private String title;
@Override
public void validate() {
if (title == null || title.isBlank()) {
addFieldError("title", "Title is required");
}
}
@Override
public String execute() {
addActionMessage("Saved " + title);
return SUCCESS;
}
public String getTitle() { return title; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
}
<action name="store-submit" class="org.example.bookshelf.SubmitBookAction">
<interceptor-ref name="store">
<param name="operationMode">STORE</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result name="input" type="redirectAction">store-form</result>
<result type="redirectAction">store-form</result>
</action>
<action name="store-form" class="org.example.bookshelf.FormAction">
<interceptor-ref name="store">
<param name="operationMode">RETRIEVE</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result name="input">/WEB-INF/views/flash.jsp</result>
<result>/WEB-INF/views/flash.jsp</result>
</action>
$ curl -c jar -b jar "http://localhost:18080/g9chain/cart-add?title=x" > /dev/null # opens the session
$ curl -c jar -b jar -L "http://localhost:18080/g9chain/store-submit?title="
messages=[] fieldErrors={title=[Title is required]} title=
$ curl -c jar -b jar "http://localhost:18080/g9chain/store-form"
messages=[] fieldErrors={} title=
$ curl -c jar -b jar -L "http://localhost:18080/g9chain/store-submit?title=Dune"
messages=[Saved Dune] fieldErrors={} title=
The first submit, with an empty title, failed validation, returned input and redirected to store-form, which printed
the stored field error. The next plain request to store-form printed no errors: retrieving removes the stored values, so
they are shown once. The third request redirected after a successful save and printed Saved Dune.
Four details of this configuration matter, and one of them differs from the guide’s example.
-
The target needs an
inputresult. The retrieved field errors go back into the target action, and theworkflowinterceptor ofdefaultStacksees field errors and returnsinput. The guide’s example target,applicationFailed, has only the default result; withdefaultStackthat would give the errorNo result defined for action ... and result input(HTTP 404), which the scratch app produced when the target had only the default result. Either add theinputresult as above or leaveworkflowout of the target’s stack. -
No session, no message.
auto-submit(modeAUTOMATIC) with a first request that had no cookie printed no field errors on the form page: the listener refuses to store whenrequest.getSession(false)isnull. The same mapping withcreateSessionplaced before the store interceptor (auto-submit-session) stored the error and the form page printed it. In a real application the reader has a session already, or the first action opens one. -
AUTOMATICis the least configuration. It retrieves in every action it is in, and stores only when the result is a redirect, so one mapping serves as a source and a target.auto-submitwith an existing session printedfieldErrors={title=[Title is required]}onauto-form, and a second request printed none. -
A request parameter can change the mode. With
allowRequestParameterSwitchat its defaulttrue, a request that addsoperationMode=STOREorRETRIEVEswitches the interceptor on a mapping that does not use it:store-dispatch?title=(AUTOMATICwith a dispatcher result stores nothing) printed its error on its own page, a second request tostore-dispatch?title=&operationMode=STOREstored it, andauto-formprinted the stored error. A request can therefore make an action store or consume messages that the application did not intend; setallowRequestParameterSwitchtofalseunless the parameter is used on purpose.
The message store stores the messages and errors, not the values of the form. A form that must be redisplayed with what the reader typed needs the value in the redirect URL, in the session through the scope interceptor, or a forward instead of a redirect. The validation framework and the input result are covered on the Validation page.
Choosing the Carrier
The sections above are alternatives for the same need, and the Bookshelf picks among them with a short list.
| Need | Use | Why not the others |
|---|---|---|
Show the confirmation after adding a book |
|
A chain repeats the POST on reload and leaves the wrong URL |
List a lookup (the shelves) in a form |
A method of the form action or a service called by it |
A chain to a lookup action couples two actions; the action tag runs an action from the view |
Remember the reader’s filter and sort order |
Scope interceptor with |
Hand-written session code repeated in every action |
A setting shared by all users |
Application map (scope interceptor |
The session is per browser |
The reading cart |
|
Request parameters or hidden fields can be edited by the reader |
A "Saved" flash after a redirect |
Store interceptor, |
A request parameter in the URL is visible and editable |
A multi-step form |
Scope interceptor with a literal key, |
A chain of actions would keep one request and lose the form state on back |
Authorization and identity |
The container or a security framework; re-derived from the session in each action |
Anything copied by a chain or read from a request parameter |
Pitfalls
| Pitfall | What happens | Remedy |
|---|---|---|
A chain used after a POST that changes data |
Reload and the back button repeat the first action; the URL names an action whose page is not shown |
Redirect after post ( |
Chaining carries trust |
A property with a getter on the source reaches a setter on the target that no request could set ( |
Carry no authorization state; |
Constant names from the guide |
Silently ignored in 7.4.0; messages are not copied |
Use |
Explicit |
Messages, errors and field errors are copied, because the built-in exclusion is replaced |
Add |
Properties on the stack but not copied |
The view finds the first action’s properties through the stack, hiding that the target’s own are null or empty |
Print the target’s values in tests; remember the top of the stack wins |
Two actions that chain to each other |
HTTP 500 |
Remove the cycle; |
|
404 |
List the method in the target’s |
A public getter for the session map |
A request can write into the session ( |
No public getter; no |
The old |
Nothing is injected in 7.4.0; a |
Implement |
|
Session attributes are not found |
Use |
A session created by every JSP |
A session per visitor, crawlers included |
|
Mutable objects in the session |
Concurrent requests interleave; a clustered server fails to serialize them |
Immutable or thread-safe |
|
The stored value overwrites the new request parameter ( |
Place |
Application scope for per-user data |
One user’s value is visible to all (the |
Session scope, or no state at all |
|
A key holds an object printed as |
Read the action’s properties, not the session keys |
|
No visible effect for |
Test the wizard’s start and end explicitly |
|
404 |
Add the |
|
Messages are not stored; the form page shows none |
|
|
A request can turn storing and retrieving on or off |
Set it to |
Session fixation |
The identifier a visitor had before login stays valid after login |
Invalidate the session or call |
What Changed Recently
| Version | Change |
|---|---|
Struts 6.0 |
The Aware interfaces move to |
Struts 7.0 |
The |
Struts 7.2 |
The |
Struts 7.4.0 |
The chaining constants are |
Where the Books Are Outdated
The books of the section teach chaining, the session and scopes correctly as ideas; the differences are in names, defaults and security.
| In the books | Struts 7.4.0 |
|---|---|
Struts 1 actions return an |
Struts 1 is end-of-life since 2013 with unfixed vulnerabilities; the Struts 2 counterpart is a |
|
|
|
|
|
Ignored; use |
Chaining shown as the way to pass data to the next screen |
Redirect after post; chaining copies properties implicitly, and in 7.x only annotated setters are meant to be settable from outside |
Session objects as a convenient place for user and cart, with passwords or privileges stored in the clear |
Store identifiers; authentication through the container or a security framework; the session map never exposed through a getter |
Scope interceptor and |
Behaviour above: |
|
Removed or deprecated for a long time; |
Inconsistencies in the Official Documentation
-
The Chaining Interceptor page names the constants
struts.xwork.chaining.copyErrors,copyFieldErrorsandcopyMessages; the 7.4.0 constants arestruts.chaining.*, and the old names are ignored (verified). -
The Chain Result page describes
skipActionsas the "list of comma separated action names for the actions that could be chained to"; in the 7.4.0 source the names are the actions that the infinite-recursion check does not apply to. The Javadoc of the setter says the list is "separated by a white space", while the code splits on commas. -
The Chaining Interceptor page says an explicit
includesorexcludeslist "controls how and which parameters are copied" but not that anexcludeslist replaces the default exclusion of errors and messages (verified). -
The Scope Interceptor page says any
typeother thanstartorendthrows anIllegalArgumentException; the setter does, but the exception did not reach the client in 7.4.0 (verified). The page gives no default forautoCreateSession, and the parameter has no visible effect under the servlet dispatcher (verified). -
The Message Store Interceptor page says
AUTOMATICstores for aServletRedirectResult; the 7.4.0 listener tests the interfaceRedirectable. Its first example has<interceptor-ref name="store"/>followed by a stray<param>and an unclosed</interceptor-ref>, and its target action example has noinputresult. -
The HTTP Session tutorial still shows
setSession(Map)and a prose "setSession method", the interface of Struts 2.x and 5.x; the interface the 7.4.0 jar has isorg.apache.struts2.action.SessionAwarewithwithSession. The page’s link to the "SessionAware interface" page (core-developers/session-aware-interface) answers 404. -
The Accessing Application, Session, Request Objects page says
#attrsearches the session scope; withsession="false"on the JSP it does not (verified).
Summary
Chaining runs a second action in the same request and copies properties from the first one onto it, silently, from
everything on the value stack; the guide, and this page, prefer a redirect after post, with the messages carried by the
store interceptor. The session is a map delivered by SessionAware.withSession, created on the first write, shared by
all requests of one browser, never to be exposed through a getter, and renewed at login. The scope interceptor moves
named action properties to the session or application map for wizards and remembered filters, and its order in the stack
decides who wins between the request and the stored value. The Struts 7 changes, the annotation requirement and the renamed
Aware interfaces, and the places where the official pages disagree with the 7.4.0 code, are in the sections above.
References
-
Core Developer Guide — Chaining Interceptor, Parameter Authorization
-
Core Developer Guide — Chaining Interceptor, Security Considerations
-
Core Developer Guide — Accessing Application, Session, Request Objects, Accessing from Java
-
Core Developer Guide — Accessing Application, Session, Request Objects, Accessing from the View
-
Getting Started — HTTP Session, Using the HTTP Session Object In The Action Class
-
Getting Started — HTTP Session, Accessing HTTP Session Objects In The View
-
Getting Started — HTTP Session, Best Practices When Using SessionAware
-
Core Developer Guide — Message Store Interceptor, Parameters