Custom Interceptors
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Struts 7.4.0 (released 2026-10-02) on Apache Tomcat 11.0.26, Jakarta Servlet 6.1 and
Java 21 (Struts 7 needs Java 17 or later and a Servlet 6.0 or later container). Everything was verified on 2026-10-08:
the facts against the Core Developers Guide, the Struts 7.4.0 Javadoc and the struts-default.xml of the 7.4.0 jar, and
every example by building it into a Maven WAR (Struts from the struts2-bom 7.4.0) and calling it with curl on Tomcat
11.0.26, or by running it as a JUnit 6 test; the outputs shown are the outputs of those runs. An interceptor is the
unit of reusable behaviour around an action: almost everything Struts does for you (exception mapping, parameter
binding, validation, file upload, internationalisation) is an interceptor, and the same mechanism is open to
application code. The built-in interceptors and the stacks are covered on the Interceptors page of this section; this page is about writing your own. The running scenario is the
Bookshelf of the Struts half of this section: the books action that lists titles, now with an audit interceptor that
records every invocation, a login-required interceptor that guards the state-changing remove method, and a tracing
interceptor that makes the execution order visible.
What an Interceptor Is and When to Write One
An interceptor is a Java object that the framework calls around the execution of an action. The code before the call to
invoke() runs before the action (and before every interceptor further down the stack); the code after it runs when
the action and its result have completed. An interceptor can also decline to call invoke(): it then returns a result
code of its own, the action never runs, and the framework renders that result. That single contract covers
authentication, auditing, timing, transactions, rate limiting and request enrichment.
Struts has three other places where cross-cutting code can live, and choosing correctly keeps the interceptor small.
| Mechanism | Sees | Choose it when |
|---|---|---|
Servlet |
The raw request and response of every URL, including static files and non-Struts servlets; knows nothing about actions, results or the ValueStack |
The concern is about HTTP, not about actions: character encoding, compression, security headers, CORS, request logging for the whole site. See Servlet Fundamentals |
Interceptor |
The |
The concern is about actions: who may run this method, what result to return instead, what to put on the action before it runs, what to record about the outcome |
|
The same invocation, after the action method has returned and before the result executes |
You must change the result code or the response after the action has decided but before the view is rendered (a response header, a flash message, a result override) |
Action base class or the action itself |
One action |
The behaviour belongs to one action and will not be reused |
The Interceptor Interface
The contract is the Interceptor interface. In Struts 7 it lives in org.apache.struts2.interceptor; the Core
Developers Guide page on writing interceptors still prints the import as com.opensymphony.xwork2.interceptor.Interceptor,
the package that the 6 to 7 migration guide says was renamed to org.apache.struts2 (the XWork code was merged into
the Struts packages in 7.0). The 7.4.0 jar contains only the new names, which is what javap shows:
package org.apache.struts2.interceptor;
public interface Interceptor extends java.io.Serializable {
void destroy();
void init();
String intercept(org.apache.struts2.ActionInvocation invocation) throws Exception;
}
Source: Struts 7 Javadoc — Interceptor
| Method | When it runs and what to do there |
|---|---|
|
Once, after the framework has instantiated the interceptor and set its parameters, and before the first call to |
|
On every request that reaches the interceptor. Do the work; call |
|
Once, when the application shuts down. Release what |
The interface extends java.io.Serializable, so an interceptor must not hold non-serialisable state in instance
fields unless it is marked transient; the interceptors that ship with Struts keep their configuration in plain
fields.
The guide stresses one rule about invoke(): it returns after the result has been executed, so after your JSP has
been rendered. That makes the code after invoke() the right place for cleanup that must cover the view (the classic
open-session-in-view pattern, timing the whole request), and the wrong place for anything that must change the
response, because the response is typically committed by then. Anything that must happen before the result runs
belongs in a PreResultListener, described below.
AbstractInterceptor
Almost every interceptor extends AbstractInterceptor, which supplies empty init() and destroy() so that only
intercept is left to write. In Struts 7.4 it also implements ConditionalInterceptor, which adds
shouldIntercept(ActionInvocation) and a setDisabled(String) setter: an interceptor extending AbstractInterceptor
can be switched off for one action with the parameter disabled (since Struts 6.1.0), described under
Disabling an Interceptor for One Action.
public abstract class AbstractInterceptor implements ConditionalInterceptor {
public void init();
public void destroy();
public abstract String intercept(ActionInvocation invocation) throws Exception;
public void setDisabled(String disabled);
public boolean shouldIntercept(ActionInvocation invocation);
}
A minimal interceptor therefore reads like the guide’s SimpleInterceptor, which sets a date on the action before it
runs. Written for the Bookshelf (and with the Struts 7 package names) it looks like this: an interface on the action
rather than a cast to a concrete action class keeps the interceptor reusable.
import java.time.Instant;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.interceptor.AbstractInterceptor;
public class RequestTimeInterceptor extends AbstractInterceptor {
public interface RequestTimeAware {
void setRequestTime(Instant requestTime);
}
@Override
public String intercept(ActionInvocation invocation) throws Exception {
if (invocation.getAction() instanceof RequestTimeAware aware) {
aware.setRequestTime(Instant.now());
}
return invocation.invoke();
}
}
The five facts to keep in mind about every intercept method are in the next table.
| What the interceptor does | Effect |
|---|---|
Returns |
The request continues down the stack; the action runs when the last interceptor calls |
Returns its own result code without calling |
Nothing below this interceptor runs, including the action and the other interceptors; the framework renders the named result. The interceptors above it still see the code on the way back |
Calls |
The action has run; the original code is replaced before it reaches the framework (the result is looked up for the new code) |
Throws an exception |
The interceptors above it see it as an exception; the |
Calls |
Not supported: the invocation is a one-way chain. Call it at most once |
The ActionInvocation
Every interceptor receives the same ActionInvocation for the whole request. It is the interceptor’s window into the
framework, and the table lists what it exposes in 7.4.0 (from the Javadoc and javap).
| Method | Use |
|---|---|
|
Runs the next interceptor, or the action when none is left, and (when the proxy executes results) the result; returns the result code |
|
The action instance of this request; use |
|
The |
|
The |
|
The ValueStack of this request |
|
Registers a callback that runs after the action method and before the result; listeners run in registration order |
|
Reads the result code, or overrides it between the action and the result (from a listener); setting it after the result has executed throws |
|
True once the action and the result have both run |
|
The |
Two cautions follow from the Javadoc. First, take the session, request and response from
invocation.getInvocationContext() rather than from the thread-local ActionContext.getContext() or the static
ServletActionContext accessors: the first is explicit about what it works on, and it keeps the interceptor testable
(a test hands in its own ActionContext). Second, the interceptor sees the action before the parameters are bound if
it sits above params in the stack, so it must not read action fields that the request is supposed to fill; see the
ordering section.
Thread Safety
The Core Developers Guide states the rule plainly: interceptors must be thread-safe. A Struts action instance is
created for every request and need not be thread-safe, but interceptors are shared between requests (the framework
creates the interceptor objects when it builds the action configuration, not on every request). Every concurrent request
that reaches the same action configuration calls intercept on the same object at the same time.
// WRONG: one counter and one start time shared by all concurrent requests
public class BrokenTimerInterceptor extends AbstractInterceptor {
private long start; // overwritten by the next request
private int calls; // lost updates
@Override
public String intercept(ActionInvocation invocation) throws Exception {
start = System.nanoTime();
calls++;
try {
return invocation.invoke();
} finally {
System.out.println(System.nanoTime() - start); // another request's start
}
}
}
The fix is the structure of the AuditInterceptor below: per-request values live in local variables inside intercept,
configuration set through setters is written once before the first request and only read afterwards, and any shared
counter is an AtomicLong or a LongAdder. A map shared across requests is a ConcurrentHashMap. Per-request data that must travel to a later interceptor or the action goes into the ActionContext,
the request attributes or the action itself, never into a field of the interceptor.
Declaring Interceptors and Stacks
An interceptor class does nothing until it is declared in struts.xml and referenced from an action or a stack. The
declaration has four elements, all children of a package.
| Element | Purpose |
|---|---|
|
Container inside a |
|
Gives a class a name; nested |
|
An ordered list of |
|
Uses an interceptor or a stack, in an action or in a stack; nested |
|
Names the stack that every action of the package (and of the packages extending it) uses when it has no |
The Bookshelf declares three interceptors, builds one stack that puts them in front of the framework’s defaultStack,
and makes that stack the package default. The login global result is what the login-required interceptor returns.
<struts>
<package name="default" extends="struts-default" namespace="/">
<interceptors>
<interceptor name="audit" class="org.example.bookshelf.interceptor.AuditInterceptor"/>
<interceptor name="trace" class="org.example.bookshelf.interceptor.TraceInterceptor"/>
<interceptor name="loginRequired" class="org.example.bookshelf.interceptor.LoginRequiredInterceptor"/>
<interceptor-stack name="bookshelfStack">
<interceptor-ref name="audit"/>
<interceptor-ref name="loginRequired">
<param name="includeMethods">remove</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="bookshelfStack"/>
<global-results>
<result name="login">/WEB-INF/views/login.jsp</result>
</global-results>
<action name="books" class="org.example.bookshelf.BookAction">
<result>/WEB-INF/views/books.jsp</result>
</action>
<action name="book-remove" class="org.example.bookshelf.BookAction" method="remove">
<result type="redirectAction">books</result>
<allowed-methods>remove</allowed-methods>
</action>
</package>
</struts>
Two details of this file were found by running it. The <allowed-methods> element must come after the <result>
elements: the DTD allows only (param|result|interceptor-ref|exception-mapping)* followed by allowed-methods?, and
the other order stops the whole application from starting with The content of element type "action" must match
(the filter fails to initialise and every URL returns 404). And an action that lists its own <interceptor-ref> elements
does not inherit the package default at all: it gets exactly those interceptors, so such an action must list
defaultStack itself if it wants the framework’s behaviour, and it silently loses the audit and login interceptors
of the default stack. The ordered action later on this page shows that: it has no X-Bookshelf-Audit header.
Bookshelf Example: an Audit Interceptor
The first interceptor records every invocation: namespace, action name, method, the result code, and how long the
whole round trip took (including the view). It also reports the outcome to the client in a response header, which
needs a PreResultListener because the header has to be set before the JSP starts writing the body. The header name is
a parameter, set through setHeaderName from the <param> element of the declaration.
package org.example.bookshelf.interceptor;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ActionProxy;
import org.apache.struts2.interceptor.AbstractInterceptor;
import org.apache.struts2.interceptor.PreResultListener;
/** Logs every invocation of an action and reports the outcome in a response header. */
public class AuditInterceptor extends AbstractInterceptor {
private static final Logger LOG = LogManager.getLogger(AuditInterceptor.class);
private String headerName = "X-Bookshelf-Audit";
// Set from <param name="headerName"> in struts.xml, once, before requests arrive.
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
@Override
public String intercept(ActionInvocation invocation) throws Exception {
// All per-request state lives in local variables: this instance is shared by every request.
ActionProxy proxy = invocation.getProxy();
String target = proxy.getNamespace() + proxy.getActionName() + "!" + proxy.getMethod();
long start = System.nanoTime();
invocation.addPreResultListener(new PreResultListener() {
@Override
public void beforeResult(ActionInvocation inv, String resultCode) {
HttpServletResponse response = inv.getInvocationContext().getServletResponse();
if (response != null && !response.isCommitted()) {
response.setHeader(headerName, target + " -> " + resultCode);
}
}
});
String result = null;
try {
result = invocation.invoke();
return result;
} finally {
long micros = (System.nanoTime() - start) / 1_000;
LOG.info("audit {} result={} took={}us", target, result, micros);
}
}
}
Calling the action shows the header and, with a log4j2.xml that enables INFO for the org.example.bookshelf logger,
the log line (the default Log4j2 configuration that Struts ships logs only warnings and errors, so without that file
the LOG.info line is invisible and only the header proves the interceptor ran).
$ curl -s -i http://localhost:8080/bookshelf/books | grep -iE '^HTTP|X-Bookshelf|<li>'
HTTP/1.1 200
X-Bookshelf-Audit: /books!execute -> success
<li>Dune</li><li>Neuromancer</li>
15:51:03 INFO AuditInterceptor - audit /books!execute result=success took=2794us
The finally block is deliberate: if the action or the view throws, the audit line is still written (with a null
result), and the exception continues to the exception interceptor. Logging request parameters in an audit
interceptor needs care: parameter values are attacker-controlled text, so log them encoded or truncated (log
injection through newlines) and never log passwords, tokens or session ids.
PreResultListener
PreResultListener is the interceptor-side hook for "after the action, before the view": a single method,
beforeResult(ActionInvocation invocation, String resultCode). The listener is registered by an interceptor with
invocation.addPreResultListener(...) while the request is going down the stack; the framework calls the listeners when
the action method has returned, in the order they were registered, and then executes the result. The result code passed
in is the one the action returned, and invocation.setResultCode(...) can replace it (the Javadoc says that this is the
intended use of that setter, and that calling it after the result has executed throws IllegalStateException).
The diagram is what the TraceInterceptor of the Bookshelf prints. Each of the two interceptors logs before and
after invoke() and registers a listener that appends its label to a response header:
public class TraceInterceptor extends AbstractInterceptor {
private static final Logger LOG = LogManager.getLogger(TraceInterceptor.class);
private String label = "?";
public void setLabel(String label) {
this.label = label;
}
@Override
public String intercept(ActionInvocation invocation) throws Exception {
LOG.info("before {}", label);
invocation.addPreResultListener((inv, resultCode) -> {
HttpServletResponse response = inv.getInvocationContext().getServletResponse();
String previous = response.getHeader("X-Trace");
response.setHeader("X-Trace", previous == null ? label : previous + "," + label);
});
try {
return invocation.invoke();
} finally {
LOG.info("after {}", label);
}
}
}
<action name="ordered" class="org.example.bookshelf.BookAction">
<interceptor-ref name="trace"><param name="label">A</param></interceptor-ref>
<interceptor-ref name="trace"><param name="label">B</param></interceptor-ref>
<interceptor-ref name="defaultStack"/>
<result>/WEB-INF/views/books.jsp</result>
</action>
$ curl -s -i http://localhost:8080/bookshelf/ordered | grep -iE '^HTTP|X-Trace'
HTTP/1.1 200
X-Trace: A,B
15:50:56 INFO TraceInterceptor - before A
15:50:56 INFO TraceInterceptor - before B
15:50:56 INFO TraceInterceptor - after B
15:50:56 INFO TraceInterceptor - after A
The two log blocks are the whole rule of ordering: "before" code runs top to bottom, "after" code bottom to top, and
the pre-result listeners fire top to bottom in between. One interceptor class declared once can be referenced twice in
a stack with different parameters, as trace is here.
MethodFilterInterceptor
An interceptor often matters for some methods of an action and not others: a login check on remove but not on
execute, validation on save but not on input. MethodFilterInterceptor is the abstract base class for exactly
that. The subclass implements doIntercept(ActionInvocation) instead of intercept, and the base class applies two
bean properties to decide whether to call it at all: includeMethods and excludeMethods, comma-separated method
names. If a method name is in both lists it counts as included: includeMethods takes precedence over
excludeMethods. The built-in interceptors built on it include token, tokenSession, workflow and validation;
the Javadoc lists params, prepare, conversionError and execAndWait among its subclasses as well.
| Member | Purpose |
|---|---|
|
The interceptor logic; called only when the method passes the filter |
|
The |
|
The parsed sets |
|
The filter decision; override to change how methods are matched |
|
The fields behind the properties |
The method that the filter compares against is the one the action is mapped to (ActionProxy.getMethod()), so the
filter works with the method attribute of an action mapping (the example below maps book-remove to remove), the same way the
validation interceptor’s excludeMethods of the default stack does. The default stack itself ships with
excludeMethods set to input,back,cancel,browse on validation and workflow in 7.4.0.
Bookshelf Example: a Login-Required Interceptor
Removing a book is a state-changing operation: it must be reachable only by a signed-in user, while listing books is
public. The interceptor extends MethodFilterInterceptor, reads the user from the session through the invocation
context, and returns the login result without running the action when there is none.
package org.example.bookshelf.interceptor;
import java.util.Map;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.interceptor.MethodFilterInterceptor;
/** Sends anonymous users to the "login" result before the action method runs. */
public class LoginRequiredInterceptor extends MethodFilterInterceptor {
public static final String USER_KEY = "bookshelf.user";
@Override
protected String doIntercept(ActionInvocation invocation) throws Exception {
Map<String, Object> session = invocation.getInvocationContext().getSession();
if (session == null || session.get(USER_KEY) == null) {
return "login"; // the action is never executed
}
return invocation.invoke();
}
}
The BookAction has a public execute and a guarded remove; the declaration above applies the interceptor with
includeMethods set to remove. The LoginAction is a demonstration sign-in: it stores a user name in the session
and, as it should after authentication, starts a new session id. A real application verifies a credential here; no
password or key appears in the example.
public class LoginAction extends ActionSupport implements SessionAware, ServletRequestAware {
private Map<String, Object> session;
private HttpServletRequest request;
private String user;
@StrutsParameter
public void setUser(String user) {
this.user = user;
}
@Override
public void withSession(Map<String, Object> session) {
this.session = session;
}
@Override
public void withServletRequest(HttpServletRequest request) {
this.request = request;
}
@Override
public String execute() {
if (user == null || user.isBlank()) {
return INPUT;
}
request.getSession(true); // changeSessionId() needs an existing session
request.changeSessionId(); // new id after authentication: no session fixation
session.put(LoginRequiredInterceptor.USER_KEY, user);
return SUCCESS;
}
}
Calling it from curl shows both paths. An anonymous POST to book-remove gets the login page and the audit
header shows the short-circuit (remove returned login, and the audit interceptor above it still reported the
outcome); after a sign-in with a cookie jar the same POST reaches the action and the result is a redirect to the list.
$ curl -s -i -X POST http://localhost:8080/bookshelf/book-remove | grep -iE '^HTTP|X-Bookshelf|<h1>'
HTTP/1.1 200
X-Bookshelf-Audit: /book-remove!remove -> login
<h1>Sign in</h1>
$ curl -s -i -c jar -b jar -X POST -d user=ana http://localhost:8080/bookshelf/login | grep -iE '^HTTP|X-Bookshelf|Set-Cookie'
HTTP/1.1 302
Set-Cookie: JSESSIONID=...; Path=/bookshelf; HttpOnly
X-Bookshelf-Audit: /login!execute -> success
$ curl -s -i -c jar -b jar -X POST http://localhost:8080/bookshelf/book-remove | grep -iE '^HTTP|X-Bookshelf|Location'
HTTP/1.1 302
X-Bookshelf-Audit: /book-remove!remove -> success
Location: /bookshelf/books.action
$ curl -s -i http://localhost:8080/bookshelf/books | grep -iE '^HTTP|X-Bookshelf'
HTTP/1.1 200
X-Bookshelf-Audit: /books!execute -> success
Three security points about this interceptor. It authenticates; it does not authorise, so a signed-in user can remove
any book (per-object checks belong in the action or a service; the built-in roles interceptor is the ready-made check against
container roles). It must be placed above params so that an anonymous request is rejected before any request
parameter is bound to the action (see ordering). And remove is a state-changing method reached only by POST in the
example: pair it with the token interceptor, or the httpMethod interceptor of the default stack, for CSRF and
verb restrictions rather than trusting a "login" check alone. A redirect target taken from a request parameter
("return to the page the user came from") must be validated against a fixed list, or it becomes an open redirect.
Ordering: Where the Interceptor Goes in the Stack
An interceptor’s position decides what it can rely on and what is protected by it. The defaultStack of Struts 7.4.0
(the declaration is in struts-default.xml of the 7.4.0 jar) is, from the first to run to the last:
| Position | Interceptors |
|---|---|
1-4 |
|
5-8 |
|
9-14 |
|
15-18 |
|
19-24 |
|
The csp, coep, coop and fetchMetadata entries are the 7.x security-header and Fetch Metadata interceptors, all
declared with disabled set to false in the stack (csp, coep and coop in non-enforcing mode where the stack says
so). A custom interceptor goes before, inside or after this stack, and the rules are these.
| Placement | Effect | Typical use |
|---|---|---|
Before |
Runs before parameters are bound, before validation, outside the |
Audit and timing that must cover everything; authentication and authorisation checks that must reject a request before any request data touches the action |
After |
Runs when parameters are bound and validation has run (a validation failure has already short-circuited with |
Checks that depend on the bound data, such as ownership of the book id in the request |
Between two specific interceptors |
Use two |
A step that needs the result of |
First of everything, even before |
Not possible with |
A custom stack that lists |
The guide warns that "some Interceptors will interrupt the stack/chain/flow … so the order is very important", and that
the order matters most when an interceptor reads parameters that the params interceptor sets: such an interceptor must
be placed after it. The inverse is the security argument of this page: an interceptor that decides whether the request
may proceed belongs above params, so that a rejected request never has its parameters injected into the action. The
Bookshelf stack puts audit first (it measures everything, including the other interceptors), loginRequired second
(rejects before parameters are bound) and defaultStack last.
Interceptor Parameters and Overriding
Properties of an interceptor are set from <param> elements, and the same properties can be overridden where the
interceptor is used. The Core Developers Guide describes three ways, and the third matters when a custom stack wraps the
default one.
| Method | Syntax | Notes |
|---|---|---|
1: copy the stack |
List every |
Verbose; you own the list and must keep it in step with |
2: |
|
The word before the dot is the interceptor name inside the stack, the word after it is the parameter |
3: the same override inside a custom stack |
|
Overrides are not inherited between stack levels: an action that overrides one parameter of an interceptor gets only that parameter, the other parameters set by the parent stack for that interceptor are null |
Disabling an Interceptor for One Action
Since Struts 6.1.0, every interceptor that extends AbstractInterceptor (all the built-in ones do) accepts a disabled
parameter: when it is true, the interceptor is skipped for that invocation (shouldIntercept returns false and the
framework calls invoke() for it). The syntax is the stack-override form interceptor.disabled. The Bookshelf
quiet action runs the same stack as the others but without the audit interceptor, so the header is absent:
<action name="quiet" class="org.example.bookshelf.BookAction">
<interceptor-ref name="bookshelfStack">
<param name="audit.disabled">true</param>
</interceptor-ref>
<result>/WEB-INF/views/books.jsp</result>
</action>
$ curl -s -i http://localhost:8080/bookshelf/quiet | grep -iE '^HTTP|X-Bookshelf|<li>'
HTTP/1.1 200
<li>Dune</li><li>Neuromancer</li>
Disabling is a configuration switch, not a security boundary: it is meant for diagnostics and for the few actions that
legitimately must not run an interceptor. Never disable a security interceptor (a login check, params filtering,
validation) to make an error disappear. Because disabled is a parameter like any other, an interceptor extending
Interceptor directly instead of AbstractInterceptor has no disabled property.
Lazy Parameters
Normally an interceptor’s parameters are fixed when the interceptor is built, so a <param> is a constant. Since Struts
2.5.9 a parameter can be evaluated against the action while the request is running, with an expression such as
${bar} in the <param> value, if the interceptor implements WithLazyParams. The interceptor then cannot hold the
parameters in its own fields (they differ per request); it declares a small InterceptorParams type, and the
framework writes the evaluated values into a fresh holder of that type for each invocation (the Javadoc: never back
onto the shared interceptor) and passes it to the two-argument intercept method.
The Core Developers Guide still prints the pre-7 shape of this example (a non-generic WithLazyParams and a
one-argument intercept with setters on the interceptor); the 7.4.0 interface is generic:
public interface WithLazyParams<P extends InterceptorParams> {
P newLazyParams();
String intercept(ActionInvocation invocation, P params) throws Exception;
}
The page does not run a lazy-parameter example; use the interface above and the guide’s configuration form as the
reference, and remember the guide’s reminder that the order matters when a lazy parameter reads a request parameter:
the params interceptor must already have run. Evaluating an expression from configuration is safe only because the
expression is written by the developer in struts.xml; never build such a <param> value from request data.
Interceptors with Spring, Annotations and the Convention Plugin
The interceptor declarations above are plain Struts configuration. Interceptors that need services (a repository, a
clock, an audit sink) get them through the object factory: with the Spring plugin installed, the class attribute can
be a Spring bean id and the interceptor is wired by Spring; without it, Struts instantiates the class with its
no-argument constructor and the services come from static holders or a lookup. Actions built with the Convention
plugin attach interceptors with @InterceptorRef and @InterceptorRefs on the action class rather than with XML.
Both belong to other pages of this section (Spring Integration and Convention Plugin & Annotations): they are named here so
the extension points are not a surprise, and are not demonstrated on this page.
Testing an Interceptor
An interceptor depends only on the ActionInvocation, so it can be tested without a container. Struts 7.4.0 ships
test doubles in struts2-core itself: org.apache.struts2.mock.MockActionInvocation and MockActionProxy. The
mock invocation’s invoke() returns the result code that the test set with setResultCode, so a test can tell
whether the interceptor called through (the code the "action" returned) or short-circuited (its own code). The three
tests below cover the anonymous, signed-in and non-included-method cases of the Bookshelf login interceptor; they ran
under JUnit 6.1.3 (org.junit.jupiter:junit-jupiter 6.1.3, test scope) with mvn package, three tests, no failures.
class LoginRequiredInterceptorTest {
private MockActionInvocation invocation(Map<String, Object> session, String method) {
MockActionProxy proxy = new MockActionProxy();
proxy.setMethod(method);
MockActionInvocation invocation = new MockActionInvocation();
invocation.setProxy(proxy);
invocation.setInvocationContext(ActionContext.of(new HashMap<>()).withSession(session));
invocation.setResultCode("success"); // what invoke() returns when the action is reached
return invocation;
}
private LoginRequiredInterceptor interceptor() {
LoginRequiredInterceptor interceptor = new LoginRequiredInterceptor();
interceptor.setIncludeMethods("remove");
return interceptor;
}
@Test
void anonymousUserIsSentToLogin() throws Exception {
String result = interceptor().intercept(invocation(new HashMap<>(), "remove"));
assertEquals("login", result);
}
@Test
void signedInUserReachesTheAction() throws Exception {
Map<String, Object> session = new HashMap<>();
session.put(LoginRequiredInterceptor.USER_KEY, "ana");
assertEquals("success", interceptor().intercept(invocation(session, "remove")));
}
@Test
void methodsOutsideIncludeMethodsAreNotChecked() throws Exception {
assertEquals("success", interceptor().intercept(invocation(new HashMap<>(), "execute")));
}
}
The test calls intercept on the interceptor, which is the public entry point of MethodFilterInterceptor; the
filter decides whether to call doIntercept, so the third test exercises the filter and the first two the logic.
Tests that must run the whole stack with the real configuration (that the order is right, that the global login
result resolves) belong to the JUnit plugin’s StrutsTestCase style of integration test, or to a curl check like the
ones on this page against a running container: the unit test cannot see a misspelt interceptor-ref.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Wrong values appear in logs or in responses under load |
State kept in an instance field of the interceptor; the instance is shared between concurrent requests |
Use local variables inside |
The action runs twice, or a short-circuited request still reaches the action |
|
Return the rejection result immediately and call |
The application does not start; every URL returns 404 and the log shows |
|
Put |
A header set after |
The result has already been rendered and the response committed |
Set it before the result, in a |
|
Called after the result has executed |
Call it from a |
The audit and login interceptors are not running for one action |
The action lists its own |
List the full stack in the action, or put the custom stack in the reference ( |
An unauthenticated request still changes an action field |
The login interceptor sits after |
Put authentication before |
The interceptor throws |
|
Test with |
The interceptor’s |
Initialisation was put in the constructor, or the interceptor was expected to start at deployment |
Do it in |
A unit test fails with a null pointer on the session or response |
The interceptor read the thread-local |
Read everything from |
|
|
Call |
An interceptor extending only |
|
Extend |
What Changed Recently
| Version | Change |
|---|---|
2.5.9 |
Lazy parameters through |
6.1.0 |
The |
7.0.0 |
The |
7.4.0 (state verified in the jar) |
|
Source: Struts 6.x.x to 7.x.x migration
Source: Struts Version Notes 7.4.0
Where the Books Are Outdated
The two Struts books consulted for concept coverage predate these names and defaults, and are not reproduced here.
| Book material | Struts 7.4 |
|---|---|
Struts 2 in Action (Struts 2.0): the |
|
The |
|
The interceptor named both |
The name in |
A custom authentication interceptor (Struts 2 in Action chapter 4) |
The session map comes from |
Struts in Action (Struts 1) has no interceptors: its extension points were the RequestProcessor and the
declarative exception handler, which Struts 2 replaced with the interceptor chain; the Struts 1 (Classic) & Migration page
covers the correspondence. The official documentation is itself stale in places: the writing-interceptors page uses
the removed com.opensymphony.xwork2 imports and the interceptors page prints a lazy-parameter example against the
old non-generic interface.
References
-
Struts Core Developers Guide — Writing Interceptors, Interceptor interface
-
Struts Core Developers Guide — Writing Interceptors, Thread Safety
-
Struts Core Developers Guide — Writing Interceptors, AbstractInterceptor
-
Struts Core Developers Guide — Writing Interceptors, Mapping
-
Struts Core Developers Guide — Writing Interceptors, Example
-
Struts Core Developers Guide — Interceptors, Understanding Interceptors
-
Struts Core Developers Guide — Interceptors, Configuring Interceptors
-
Struts Core Developers Guide — Interceptors, Stacking Interceptors
-
Struts Core Developers Guide — Interceptors, The Default Configuration
-
Struts Core Developers Guide — Interceptors, Method Filtering
-
Struts Core Developers Guide — Interceptors, Interceptor Parameter Overriding
-
Struts Core Developers Guide — Interceptors, Interceptor Parameter Overriding Inheritance
-
Struts Core Developers Guide — Interceptors, Lazy parameters
-
Struts Core Developers Guide — Interceptors, Disabling interceptor
-
Struts Core Developers Guide — Interceptors, Order of Interceptor Execution
-
Struts Getting Started — Introducing Interceptors, Create Your Own Interceptor
-
Jakarta Servlet 6.1 Javadoc — HttpServletRequest.changeSessionId