What’s Changed & Migration

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 was verified on 2026-10-10 against Jakarta Pages 4.0, Jakarta Expression Language 6.0, Jakarta Standard Tag Library (Jakarta Tags) 3.0, Jakarta Servlet 6.1, Apache Tomcat 11.0.26, Apache Struts 7.4.0 (released 2026-10-02) and 6.12.0, Java 21, Spring Framework 7.0.9 and Spring Boot 4.1.1. It is the section’s collection point: it says what changed between the platform of the four reference books (Servlet 2.3 and 2.4, JSP 1.2 and 2.0, JSTL 1.0, Struts 1 and Struts 2.0, Tomcat 4 to 6, the javax namespace) and the platform the rest of the section teaches, and how to move an old application across the gap. The running scenario is the Bookshelf: a catalogue of books with a list page, an "add a book" form and a lend action. Here it is first an application of the javax era (a servlet, a filter, a tag handler, a tag library descriptor and a JSP using JSTL 1.2), which is converted three ways, and then a Struts application that crosses the 2.3, 2.5, 6 and 7 borders one change at a time. Every example that runs was built into a WAR and deployed on Tomcat 11.0.26 with Java 21 and requested with curl; the "before" snippets that cannot run on a current container (Servlet 2.3, Struts 2.3) are marked as such. The repository’s Bookshop legacy-migration scenario (a Struts 1 and JSP monolith moved to a modern stack) is linked in Modernisation Paths; the Faces-based path is covered by the JSF (JavaServer Faces) section. Pages of this section that explain the topics summarised below are linked from the tables, and the books themselves appear only in the bibliography and in the "where the books are outdated" parts, never as a source of text.

The Version Map

The platform moved in generations. Each generation changed the Java package of the APIs, the version of the descriptor and, for the later ones, the minimum Java version, and a book written for one generation describes the others only by accident. The timeline puts the specification generations, Tomcat and Struts on one axis; the dates are the dates of the JCP records, the Jakarta specification pages and the Struts announcements that Introduction and History lists with their sources.

timeline title Platform generations behind the Bookshelf 2001 : Servlet 2.3 and JSP 1.2 : Tomcat 4 : Struts 1 released 2002 : JSTL 1.0 with twin EL and RT libraries 2003 : JSP 2.0 and Servlet 2.4 : EL becomes part of JSP : JSTL 1.1 2006 : JSP 2.1 : Tomcat 6 : JSTL 1.2 : Struts 2.0 beta 2009 : Servlet 3.0 and JSP 2.2 : EL 2.2 method calls : Tomcat 7 2013 : EL 3.0 with lambdas and streams : Struts 1 end of life 2016 : Struts 2.5 2017 : Servlet 4.0 and JSP 2.3 : Tomcat 9 : last javax generation 2020 : Servlet 5.0 and Pages 3.0 : javax becomes jakarta : Tomcat 10.0 2022 : Jakarta EE 10 : Servlet 6.0 and Pages 3.1 : Tags 3.0 : Tomcat 10.1 : Struts 6.0.0 2024 : Jakarta EE 11 : Servlet 6.1 and Pages 4.0 and EL 6.0 : Tomcat 11 : Struts 7.0.0 2026 : Struts 7.4.0 and 6.12.0 : Jakarta EE 12 in progress

The table gives the generation behind each Tomcat line, taken from the Tomcat version page. It answers the first question of every migration: which specifications does the container you have, or the container you want, implement?

Tomcat Servlet Pages EL Namespace and Java Status on 2026-10-10

11.0.x (11.0.26)

6.1

4.0

6.0

jakarta.*, Java 17 or later

Current focus of development; the runtime of this section

10.1.x (10.1.60)

6.0

3.1

5.0

jakarta.*, Java 11 or later

Supported

10.0.x

5.0

3.0

4.0

jakarta.*

Superseded, end of life 2022-10-31

9.0.x (9.0.122)

4.0

2.3

3.0

javax.*, Java 8 or later

Supported; end of support "no earlier than 31 March 2027", then an extended-support 9.1.x branch

8.0.x and 8.5.x

3.1

2.3

3.0

javax.*

End of life

7.x

3.0

2.2

2.2

javax.*

End of life

6.x

2.5

2.1

(within JSP 2.1)

javax.*

End of life

4.x

2.3

1.2

none

javax.*

End of life; the platform of the Servlet and JSP books

The specification histories behind those columns are short enough to put in one table. The right-hand column says what a migration across that border has to touch; the sections that follow take the borders one at a time.

Border Date What a migration across it touches

Servlet 2.3 and JSP 1.2 to JSP 2.0 and Servlet 2.4

2003-11-24

EL in JSP pages, tag files and SimpleTag, the web.xml schema in place of the DTD; EL is ignored in pages of a Servlet 2.3 application, see JSP 1.2 to 4.0

JSTL 1.0 to 1.1

2003-11-24

The tag library URIs lose the twin _rt libraries, see JSTL 1.0 to 3.0: the Library URIs

JSP 2.0 to 2.1, JSTL 1.1 to 1.2

2006-05-11

#{...} is reserved for deferred expressions, with a translation error in template text

Servlet 2.5 to 3.0, JSP 2.1 to 2.2

2009-12-10

Annotations (@WebServlet, @WebFilter, @WebListener), web fragments, programmatic registration, EL 2.2 method calls with parameters

EL 2.2 to 3.0

2013-05-22

Lambdas, collection literals, streams, the assignment, semicolon and string-concatenation operators, static fields and methods

Servlet 4.0 to 5.0, Pages 2.3 to 3.0, EL 3.0 to 4.0, Tags 1.2 to 2.0

2020

Only the namespace: javax.* becomes jakarta.*, see From javax to jakarta, with the Tomcat Migration Tool

Servlet 5.0 to 6.0, Pages 3.0 to 3.1, EL 4.0 to 5.0, Tags 2.0 to 3.0

2022

Removal of the APIs deprecated before; jakarta.tags.* library URIs, see Jakarta Servlet: What Changed and What Was Removed and JSTL 1.0 to 3.0: the Library URIs

Servlet 6.0 to 6.1, Pages 3.1 to 4.0, EL 5.0 to 6.0

2024

Removal of the APIs deprecated in 3.1 and 5.0, isThreadSafe and jsp:plugin removed, records in EL, see JSP 1.2 to 4.0 and EL 2.2 to 6.0

The decision that opens every migration is what you have: the source code, only the WAR, or a library you cannot touch. The flow chart is the whole of From javax to jakarta, with the Tomcat Migration Tool in one picture.

flowchart TD A[javax application] --> B{Source code available?} B -->|yes| C[Change imports and dependencies to jakarta] B -->|no| D{Tomcat 10.1 or 11 deployment?} C --> E[Update descriptors, TLDs and JSTL URIs] D -->|yes| F[Drop the WAR in webapps-javaee] D -->|ahead of time| G[Run the migration tool and ship the result] F --> H[Converted WAR in webapps] G --> H E --> I[Recompile against Servlet 6.1 and Pages 4.0] I --> J{Removed APIs left?} J -->|yes| K[Replace SingleThreadModel, HttpUtils, getValue and friends] J -->|no| L[Deploy on Tomcat 11] K --> L H --> M{Libraries still javax?} M -->|yes| N[Upgrade to jakarta releases or convert the jars] M -->|no| L N --> L

From javax to jakarta, with the Tomcat Migration Tool

Jakarta Servlet 5.0 "is only a change of namespaces", in the words of the specification’s compatibility appendix, and migrating a Servlet 4.0 project to 5.0 or later "requires the replacement of the namespace javax.* with jakarta.*`". Pages 3.0 lists the same move first among its changes (`javax.servlet.jsp to jakarta.servlet.jsp), Jakarta Tags 3.0 says that applications using the Java EE package "will need to change this to the Jakarta EE package", and the Tomcat migration guide calls it "a significant breaking change between Tomcat 9.0.x and Tomcat 10.0.x". The Introduction and History page has the table of renamed packages; this page shows what happens to a real WAR.

The Bookshelf as a javax WAR

The WAR was built for this page with Java 17, javax.servlet-api 4.0.1 and javax.servlet.jsp-api 2.3.3 as provided dependencies and JSTL 1.2 (javax.servlet:jstl) in WEB-INF/lib. It has a servlet registered with @WebServlet, a filter declared in web.xml, a SimpleTag handler with its tag library descriptor, and a JSP that uses JSTL through the java.sun.com library URIs.

The servlet and the filter of the javax era
// BooksServlet.java
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/books")
public class BooksServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setAttribute("books", Arrays.asList(
                new Book(1, "Dune", "Frank Herbert", 1965),
                new Book(2, "Neuromancer", "William Gibson", 1984)));
        req.getRequestDispatcher("/WEB-INF/views/books.jsp").forward(req, resp);
    }
}

// AuditFilter.java
public class AuditFilter implements javax.servlet.Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        ((HttpServletResponse) res).setHeader("X-Bookshelf-Audit", "javax");
        chain.doFilter(req, res);
    }
}
The descriptors and the JSP of the javax era
<!-- WEB-INF/web.xml -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <filter>
        <filter-name>audit</filter-name>
        <filter-class>org.example.bookshelf.AuditFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>audit</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

<!-- WEB-INF/tlds/bookshelf.tld -->
<taglib xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
        version="2.1">
    <tlib-version>1.0</tlib-version>
    <short-name>bs</short-name>
    <uri>http://example.org/bookshelf</uri>
    <tag>
        <name>badge</name>
        <tag-class>org.example.bookshelf.BadgeTag</tag-class>
        <body-content>empty</body-content>
        <attribute>
            <name>text</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
    </tag>
</taglib>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib uri="http://example.org/bookshelf" prefix="bs" %>
<h1>Bookshelf <bs:badge text="javax"/></h1>
<c:forEach var="b" items="${books}">
  <li>${fn:escapeXml(b.title)} by ${fn:escapeXml(b.author)}</li>
</c:forEach>

What Happens Without Migration

Dropped into the webapps directory of Tomcat 11.0.26 as it is, the WAR does not start. The filter named in web.xml extends an interface that the container no longer has, so the context fails, and every URL of it answers 404:

$ curl -s -o /dev/null -w "GET /bookshelf-javax/books -> %{http_code}\n" http://localhost:18180/bookshelf-javax/books
GET /bookshelf-javax/books -> 404

SEVERE [Catalina-utility-2] org.apache.catalina.core.StandardContext.filterStart Exception starting filter [audit]
	java.lang.NoClassDefFoundError: javax/servlet/Filter
	Caused by: java.lang.ClassNotFoundException: javax.servlet.Filter

A servlet that is registered only by annotation fails more quietly: the annotation scan looks for jakarta.servlet.annotation.WebServlet, finds nothing, and the application deploys cleanly and answers 404 without a message. The failing "before" case of that variant, with its two servlets, is on the Introduction and History page.

Conversion at Deployment: legacyAppBase

The first remedy needs no tool of your own. Each Tomcat host has a legacyAppBase directory, webapps-javaee by default, and the Host documentation says that Java EE applications placed there "will be converted to Jakarta EE using the Apache Tomcat Migration Tool for Jakarta EE", with the result placed in the appBase. On 11.0.26 the default works without any configuration: the instance used for this page has no legacyAppBase attribute in server.xml, and copying bookshelf-javax.war into the empty webapps-javaee directory produced, a few seconds later, a converted application in webapps that answered normally:

$ cp bookshelf-javax.war $CATALINA_BASE/webapps-javaee/
INFO [Catalina-utility-2] org.apache.tomcat.jakartaee.Migration.execute Performing migration from source [.../webapps-javaee/bookshelf-javax.war] to destination [...]
INFO [Catalina-utility-2] org.apache.tomcat.jakartaee.Migration.migrateStream Migration starting for archive [WEB-INF/lib/jstl-1.2.jar] using streaming
INFO [Catalina-utility-2] org.apache.tomcat.jakartaee.Migration.migrateStream Migration finished for archive [WEB-INF/lib/jstl-1.2.jar]
INFO [Catalina-utility-2] org.apache.tomcat.jakartaee.Migration.execute Migration completed successfully in [188] milliseconds
INFO [Catalina-utility-2] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [.../webapps/bookshelf-javax.war]

$ curl -s -D - http://localhost:18180/bookshelf-javax/books
HTTP/1.1 200
X-Bookshelf-Audit: javax
Content-Type: text/html;charset=UTF-8

<h1>Bookshelf <span class="badge">javax</span></h1>
<ul>
  <li>Dune by Frank Herbert (1965)</li>
  <li>Neuromancer by William Gibson (1984)</li>
</ul>
<p>Servlet: jakarta.servlet.http.HttpServletRequest</p>

The last line is the proof that the tool rewrote more than the class files: the scriptlet of the JSP printed javax.servlet.http.HttpServletRequest.class.getName() in the source and jakarta.servlet.http.HttpServletRequest at run time, because the tool also converts text files. The original WAR stayed in webapps-javaee and the converted copy was deployed from webapps. The Host documentation adds the limit of the feature: the conversion "will be performed using default settings", and for an application that needs other settings the migrate.sh and migrate.bat scripts in bin run the tool by hand.

Conversion Ahead of Time: the Migration Tool

The Tomcat migration guide recommends the same tool ahead of time "to benefit from faster deployment time and more precise conversion configuration options". Tomcat 11.0.26 bundles version 1.0.12 as lib/jakartaee-migration-1.0.12-shaded.jar behind bin/migrate.sh; the current release on Maven Central at the time of writing is 1.0.13 (published as a shaded jar, with a bin distribution next to it), and the download page of the Apache Tomcat site lists the same 1.0.13. The shaded jar was downloaded from Maven Central into an empty directory and its SHA-1 checked against the .sha1 file next to it before use.

$ java -jar jakartaee-migration-1.0.13-shaded.jar -profile=EE bookshelf-javax.war bookshelf-ee-profile.war
Performing migration from source [bookshelf-javax.war] to destination [bookshelf-ee-profile.war] with Jakarta EE specification profile [EE]
Migration starting for archive [bookshelf-javax.war] using streaming
Migration starting for archive [WEB-INF/lib/jstl-1.2.jar] using streaming
Migration finished for archive [WEB-INF/lib/jstl-1.2.jar]
Migration finished for archive [bookshelf-javax.war]
Migration completed successfully in [231] milliseconds

$ bin/migrate.sh bookshelf-javax.war bookshelf-tomcat-profile.war
INFO: Performing migration from source [bookshelf-javax.war] to destination [bookshelf-tomcat-profile.war] with Jakarta EE specification profile [TOMCAT]
INFO: Migration completed successfully in [218] milliseconds

The options of the 1.0.13 command line are printed by running the jar without arguments. The ones that matter here:

Option Meaning (from the tool’s own usage text)

-profile=TOMCAT

The default: convert the Java EE APIs that Tomcat provides (Servlet, Pages, EL, WebSocket, Authentication)

-profile=EE

Convert all Java EE APIs, for a WAR that also uses, say, Persistence or Validation

-profile=SERVLET

Convert the Jakarta Servlet API only

-profile=JEE8

Convert back to the old Java EE 8 APIs; the usage text warns that classes using APIs added in Jakarta EE 10 or later will not work

-exclude=<glob>

Skip files that match; repeatable; matched against the file name unless -matchExcludesAgainstPathName is given

-zipInMemory

Process archives in memory when the default streaming fails on an unusual zip structure

-cache, -cacheLocation=<dir>

Cache converted libraries between runs, so unchanged bundled jars are not converted again

What the conversion changed, and what it left alone, was read from the converted WARs of both runs (the TOMCAT and EE profiles gave the same result for this application):

Part of the WAR Converted? Observed

Class files of the application

Yes

javap on AuditFilter shows implements jakarta.servlet.Filter; BooksServlet extends jakarta.servlet.http.HttpServlet

String constants in the JSP

Yes

The scriptlet’s javax.servlet.http.HttpServletRequest printed as jakarta.servlet.http.HttpServletRequest

WEB-INF/lib/jstl-1.2.jar

Yes

29 jakarta/servlet entries and no javax/servlet entry left in the converted jar

web.xml namespace and version

No

Still xmlns="http://xmlns.jcp.org/xml/ns/javaee" and version="4.0"; Tomcat 11 accepts it and the filter ran

TLD namespace and version

No

Still the java.sun.com/xml/ns/javaee schema and version="2.1"

JSTL library URIs in the JSP

No

http://java.sun.com/jsp/jstl/core stays; it still resolves, see JSTL 1.0 to 3.0: the Library URIs

The practical reading: the tool makes a javax application run on a jakarta container, and it is the right tool for a third-party WAR or a deadline, but it does not modernise the application. The descriptors keep naming Java EE 8, and the next change to the application (a new @WebServlet that imports javax, a library bumped to a jakarta release) mixes two namespaces in one application, which is the hard-to-find failure of this kind of migration.

Conversion at the Source

The durable remedy changes the source. For the Bookshelf the whole change was a search and replace of javax.servlet with jakarta.servlet, a new set of dependencies, and three descriptor edits. The result compiled with Java 21 and Servlet 6.1 and ran on Tomcat 11.0.26 with the same output as the converted WAR (the filter header and the badge printed jakarta this time):

The Maven dependencies, before and after
<!-- before -->
<dependency>
    <groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version><scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet.jsp</groupId><artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.3</version><scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId><artifactId>jstl</artifactId><version>1.2</version>
</dependency>

<!-- after -->
<dependency>
    <groupId>jakarta.servlet</groupId><artifactId>jakarta.servlet-api</artifactId>
    <version>6.1.0</version><scope>provided</scope>
</dependency>
<dependency>
    <groupId>jakarta.servlet.jsp</groupId><artifactId>jakarta.servlet.jsp-api</artifactId>
    <version>4.0.0</version><scope>provided</scope>
</dependency>
<dependency>
    <groupId>jakarta.servlet.jsp.jstl</groupId><artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
    <version>3.0.2</version>
</dependency>
<dependency>
    <groupId>org.glassfish.web</groupId><artifactId>jakarta.servlet.jsp.jstl</artifactId>
    <version>3.0.1</version>
</dependency>

The Pages specification page lists its API as jakarta.pages:jakarta.pages-api, which does not exist on Maven Central; the artifact is jakarta.servlet.jsp:jakarta.servlet.jsp-api, as above. The JSTL pair is two artifacts on purpose: the API (jakarta.servlet.jsp.jstl-api) and the implementation (org.glassfish.web:jakarta.servlet.jsp.jstl), which Tomcat does not bundle.

The descriptors, before and after
<!-- web.xml before -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" ... version="4.0">
<!-- web.xml after -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
         version="6.1">

<!-- bookshelf.tld before -->
<taglib xmlns="http://java.sun.com/xml/ns/javaee" ... version="2.1">
<!-- bookshelf.tld after -->
<taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_4_0.xsd"
        version="4.0">
<%-- before --%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%-- after --%>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>

A last distinction worth making is the version of the descriptor. Tomcat 11 starts a web.xml of any version it can parse, and for pages the descriptor version has one documented effect on the language: the Pages specification says that the default mode for JSP pages in an application delivered with a Servlet 2.3 or earlier descriptor is to ignore EL, "for backward compatibility". Four otherwise identical WARs, each with a ${2 + 2} in a JSP, prove it:

Descriptor of the WAR ${2 + 2} in a JSP Annotated servlet

Servlet 2.3 DTD (DOCTYPE web-app)

printed literally, 2+2=${2 + 2}

200

Servlet 2.4 schema, version="2.4"

2+2=4

200

Servlet 2.5 schema, version="2.5"

2+2=4

200

Servlet 6.1 schema, version="6.1"

2+2=4

200

The annotation column is the second surprise: Tomcat 11 honoured @WebServlet in all four, so an old descriptor version does not switch the annotation scan off the way a Servlet 2.5 container of the time would have.

Jakarta Servlet: What Changed and What Was Removed

The Servlet 6.1 specification keeps a change log that, read from the bottom, is the history of the API since the namespace move. The entries that affect a JSP or Struts application are in the table; the "Source" line names the sections.

Border Changes that touch an application

Servlet 4.0 to 5.0

Only the namespace, javax.* to jakarta.*

Servlet 5.0 to 6.0

Java 11 minimum; Cookie moved to RFC 6265 and gained generic attributes (SameSite) through setAttribute; HTTP PATCH; the X-Powered-By recommendation dropped; unique request and connection identifiers; and issue 418: the API classes and methods deprecated in 5.0 and earlier were removed, among them the SingleThreadModel and HttpSessionContext interfaces, the HttpUtils class and a number of deprecated methods

Servlet 6.0 to 6.1

jakarta.servlet.error.query_string and (issue 544) jakarta.servlet.error.method added to the error-dispatch attributes, with error dispatches always made as GET; sendRedirect variants that choose the status code and control the body; Charset overloads of setCharacterEncoding; new response constants for 308, 421, 422 and 426; parameter methods throw a runtime exception when parsing fails; ByteBuffer support in the streams; HTTP/2 server push deprecated and optional; references to the SecurityManager removed

The removed API is the part that stops a build, so it is worth knowing exactly. The members listed below were found by comparing the public and protected members of jakarta.servlet-api 5.0.0 with 6.0.0 and 6.1.0 (javap on both jars) and then checked by compiling the old calls against 6.1.0. The compiler’s answer for a class that uses seven of them:

import jakarta.servlet.SingleThreadModel;
import jakarta.servlet.http.HttpUtils;
// ...
String u = r.encodeUrl("/books");
boolean fromUrl = q.isRequestedSessionIdFromUrl();
Object v = s.getValue("cart");
Throwable t = e.getRootCause();
return BodyTag.EVAL_BODY_TAG;
Removed.java:1: error: cannot find symbol  -- class SingleThreadModel
Removed.java:5: error: cannot find symbol  -- class HttpUtils
Removed.java:11: error: cannot find symbol -- method encodeUrl(String)
Removed.java:12: error: cannot find symbol -- method isRequestedSessionIdFromUrl()
Removed.java:13: error: cannot find symbol -- method getValue(String)
Removed.java:14: error: cannot find symbol -- method getRootCause()
Removed.java:15: error: cannot find symbol -- variable EVAL_BODY_TAG
7 errors
Removed (Servlet 6.0 and Pages 4.0) Replacement Notes

SingleThreadModel

Thread-safe servlets; keep state in locals, request attributes or synchronised services

The isThreadSafe page directive attribute went with it, see JSP 1.2 to 4.0

HttpUtils.getRequestURL, parseQueryString, parsePostData

request.getRequestURL(), request.getParameterMap()

HttpSessionContext and HttpSession.getSessionContext()

none

Session enumeration was never portable

HttpSession.getValue, putValue, removeValue, getValueNames

getAttribute, setAttribute, removeAttribute, getAttributeNames

HttpServletResponse.encodeUrl, encodeRedirectUrl, setStatus(int, String)

encodeURL, encodeRedirectURL, setStatus(int) or sendError(int, String)

The spelling changed, not just the case: Url to URL

HttpServletRequest.isRequestedSessionIdFromUrl(), ServletRequest.getRealPath(String)

isRequestedSessionIdFromURL(), ServletContext.getRealPath(String)

ServletContext.getServlet, getServlets, getServletNames, log(Exception, String)

Hold your own references; log(String, Throwable)

UnavailableException(Servlet, String) and getServlet()

UnavailableException(String)

BodyTag.EVAL_BODY_TAG, JspException.getRootCause() (Pages 4.0)

BodyTag.EVAL_BODY_BUFFERED, getCause()

Verified by compiling the replacements in the same file

jakarta.servlet.jsp.el.ExpressionEvaluator, VariableResolver, FunctionMapper, Expression, ELException, ELParseException; JspContext.getExpressionEvaluator() and getVariableResolver() (Pages 4.0)

JspContext.getELContext() with jakarta.el.ExpressionFactory

The classes were deprecated before the removal

The replacements compile: the same file with encodeURL, isRequestedSessionIdFromURL, getAttribute, getCause and EVAL_BODY_BUFFERED built against jakarta.servlet-api 6.1.0 and jakarta.servlet.jsp-api 4.0.0 without a message.

JSP 1.2 to 4.0

The Pages specification keeps its change log only from JSR 245 (JSP 2.1) on, so the older steps come from the JSR records and the Pages chapters themselves. The table keeps to what the sources say.

Version What it added or changed Source

JSP 1.2 (2001, Servlet 2.3)

The Tomcat 4 era of the JSP books: scriptlets and the standard actions, classic tag handlers, the JSP 1.2 tag library descriptor DTD, no expression language; jsp:plugin for applets. The change from JSP 1.1 that the JSP book’s appendix lists is the one Tomcat’s own version page describes from the other side: Tomcat 4.0 "implements the final released versions of the Servlet 2.3 and JSP 1.2 specifications" and, as the specifications require, still runs applications built for Servlet 2.2 and JSP 1.1 "with no changes"

JSR 53, Tomcat — 4.x

JSP 2.0 (2003, Servlet 2.4)

The expression language becomes part of JSP; tag files and SimpleTag; jsp:attribute, jsp:body, jsp:element, jsp:invoke, jsp:doBody; jsp-property-group; JSTL 1.1 builds on it

JSR 152

JSP 2.1 (2006)

Deferred #{...} expressions, shared with JavaServer Faces (JSR 245’s aim was a better alignment with it), which is why #{ in template text is now a translation error; JSTL 1.2

JSR 245, Pages 4.0 — Backwards Compatibility with JSP 2.0

JSP 2.2 (2009)

Shipped with Servlet 3.0 and EL 2.2 (EL, "currently part of JSR 245, JavaServer Page (JSP) 2.2", before JSR 341 took it over)

JSR 341

JSP 2.3 (2017)

The Java EE 8 version, implemented by Tomcat 9 together with Servlet 4.0 and EL 3.0

Tomcat — Supported Version Details

Pages 3.0 (2020)

API moved from javax.servlet.jsp to jakarta.servlet.jsp; all deprecated methods carry @Deprecated; generics where appropriate

Pages 4.0 — C.3

Pages 3.1 (2022)

errorOnELNotFound (a PropertyNotFoundException for an unknown identifier); the EL environment of a page gets the default imports of the scripting environment; isThreadSafe and jsp:plugin deprecated; getFeatureDescriptors() overrides deprecated

Pages 4.0 — C.2

Pages 4.0 (2024, Jakarta EE 11)

Java 17 minimum; removed: the deprecated jakarta.servlet.jsp.el classes, BodyTag.EVAL_BODY_TAG, JspException.getRootCause, the isThreadSafe attribute, the jsp:plugin action and its related actions; ErrorData gains the query string and the method

Pages 4.0 — C.1

The removals of Pages 4.0 are the ones that can break an old page without a compiler’s help, because JSP errors show up at translation time on the first request. Each was requested on Tomcat 11.0.26 through a servlet that forwards to a page in WEB-INF, so the Bookshelf’s books attribute was available:

<%-- threadsafe.jsp --%>
<%@ page contentType="text/plain;charset=UTF-8" isThreadSafe="false" %>
hello

<%-- plugin.jsp --%>
<jsp:plugin type="applet" code="Clock.class" codebase="/applets" width="200" height="100">
  <jsp:fallback>no applet</jsp:fallback>
</jsp:plugin>

<%-- el-class.jsp --%>
${books[0].class.name}
threadsafe.jsp: HTTP 500  JasperException: /WEB-INF/views/threadsafe.jsp (line: [1], column: [2]) [Page directive] has invalid attribute: [isThreadSafe]
plugin.jsp:     HTTP 500  JasperException: /WEB-INF/views/plugin.jsp (line: [2], column: [5]) Invalid standard action
el-class.jsp:   HTTP 500  JasperException: jakarta.el.ELException: Failed to parse the expression [${books[0].class.name}]
el-class2.jsp:  HTTP 200  org.example.bookshelf.Book      (${books[0].getClass().getName()})

The third case is not a removal of Pages 4.0 but of the EL of the same era, and it appears in so many book examples and error pages (${exception.class.name}) that it belongs here: the class property does not parse on Tomcat 11, the method call getClass().getName() does. The Pages and EL pages of this section explain why class is a Java keyword rather than an EL reserved word. The fix for isThreadSafe is to delete the attribute (the specification removed it together with SingleThreadModel), and for jsp:plugin to delete the action: browsers no longer run applets, the reason the 3.1 change log gives for deprecating it.

For the language itself, the pages of Part I are the reference. The directives and the translation of a page are on Directives, Lifecycle and Translation and JSP Documents and XML Syntax; the scriptlets that every book of the JSP era is full of, and the scripting-invalid switch that turns them off, are on Scripting Elements; the property groups that replace per-page settings are on Configuration and Property Groups; the tag handlers, SimpleTag and tag files that replace the manual EL evaluation of the 2002 tag libraries are on Custom Tags: Classic, Custom Tags: SimpleTag and Tag Files and Layouts.

JSTL 1.0 to 3.0: the Library URIs

JSTL 1.0 (2002) came before the expression language was part of JSP, so the library had to carry its own: each area had a twin, one library whose attributes took JSTL’s EL (http://java.sun.com/jstl/core) and one whose attributes took request-time scriptlet expressions (http://java.sun.com/jstl/core_rt). JSTL 1.1 (the JSR 52 maintenance release of 2003-11-24) dropped the twins, moved the URIs to http://java.sun.com/jsp/jstl/core and added the fn functions, and Jakarta Tags 3.0 renamed the URIs again: its change log lists the item "Rename Tag URIs" (to the jakarta.tags.* form), together with the move to Java 11 and a JPMS module descriptor.

Library URI Generation Request on Tomcat 11.0.26 with JSTL 3.0.1

http://java.sun.com/jstl/core

JSTL 1.0, EL library

Translation error: According to TLD or attribute directive in tag file, attribute [value] does not accept any expressions for ${...}; a literal works

http://java.sun.com/jstl/core_rt

JSTL 1.0, RT library

Works: <c:out value="<%= 1 + 1 %>"/> printed 2

http://java.sun.com/jsp/jstl/core

JSTL 1.1 and 1.2

Works like jakarta.tags.core: Dune

http://xmlns.jcp.org/jsp/jstl/core

Never defined (the Java EE 7 namespace style)

The absolute uri: [http://xmlns.jcp.org/jsp/jstl/core] cannot be resolved in either web.xml or the jar files deployed with this application

jakarta.tags.core

Tags 3.0

Works: Dune

The same applies to the other libraries (fmt, sql, xml, functions). The old URIs are still accepted because the implementation jar org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1 still ships the old descriptors next to the new ones; the specification documents only the jakarta.tags.* names, so a migration should change them even though nothing fails, and the old URIs should never be written as links. On the old attribute spelling, x:parse still accepts the JSTL 1.0 xml attribute on 3.0.1 (the page below used it and printed Dune) next to the current doc attribute, which is the form to write.

<%-- before: JSTL 1.0 --%>
<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jstl/xml" prefix="x" %>
<x:parse var="d" xml="${src}"/>

<%-- after: Tags 3.0 --%>
<%@ taglib uri="jakarta.tags.core" prefix="c" %>
<%@ taglib uri="jakarta.tags.xml" prefix="x" %>
<x:parse var="d" doc="${src}"/>
<x:out select="$d/shelf/book/title"/>

The coordinates moved with the namespace, and the JSTL 3.1 release that is in preparation adds java.time to fmt:formatDate and fmt:parseDate and needs Java 21 (see What Is In Progress for Jakarta EE 12).

Era Maven coordinates

JSTL 1.0 and 1.1

jstl.jar and standard.jar from Jakarta Taglibs

JSTL 1.2

javax.servlet:jstl:1.2 (API and implementation in one jar)

Tags 3.0

jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2 and org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1

The pages that teach the libraries as they are today are JSTL Core, JSTL Iteration, JSTL URLs, Imports and Redirects, JSTL Formatting, JSTL I18n and Messages, JSTL Functions and JSTL SQL and XML (the last one also says why the sql library is for prototypes only).

EL 2.2 to 6.0

EL was a chapter of the JSP specification until JSP 2.1, then an extract of it, and since EL 3.0 (JSR 341, final release 2013-05-22) it is a specification of its own that Pages, Faces and standalone code share. The Pages specification pins the numbers: an EL expression in a JSP page is evaluated by the container’s ELResolver chain, which since Pages 3.1 shares the default imports of the scripting environment. The change log of the EL specification covers the borders after the namespace move:

Border Changes

EL 2.2 to 3.0 (JSR 341, 2013)

Lambda expressions, collection construction ([1, 2, 3], {'a': 1}), collection operations as streams, the string-concatenation operator +=, the assignment operator = and the semicolon operator ;, static fields and methods through class names

EL 3.0 to 4.0

The API moved from javax.el to jakarta.el

EL 4.0 to 5.0 (Java 11)

BeanELResolver considers default methods; a lambda can be coerced to a functional interface; ELResolver.getFeatureDescriptors() deprecated; the misspelt MethodExpression.isParmetersProvided() removed; MethodReference; generics; array coercion; the length special case in ArrayELResolver

EL 5.0 to 6.0 (Java 17)

Deprecated code removed; the java.beans dependency becomes optional; array length property; java.util.Optional support through the new OptionalELResolver (opt-in); module visibility when accessing methods; references to the SecurityManager removed; java.lang.Record support through RecordELResolver (on by default)

A page that uses the features of each generation, with the Bookshelf’s three books in the books attribute, a record Isbn in isbn and an int[] in sizes, printed the following on Tomcat 11.0.26 (the page forwards from a servlet; the expression column is what was written):

method call (EL 2.2):   ${books[0].title.toUpperCase()}
lambda filter (EL 3.0): ${books.stream().filter(b -> b.year lt 1900).map(b -> b.title).toList()}
sorted titles:          ${books.stream().map(b -> b.title).sorted().toList()}
list literal:           ${[1, 2, 3].stream().sum()}
map literal:            ${{'a': 1, 'b': 2}['b']}
string concat (+=):     ${'Book' += ' ' += 'shelf'}
assignment and ;:       ${n = 3; n * 2}
static field (3.0):     ${Boolean.TRUE}
static method (3.0):    ${Integer.parseInt('42') + 1}
record (6.0):           ${isbn.code}
array length (6.0):     ${sizes.length}
method call (EL 2.2):   DUNE
lambda filter (EL 3.0): [Frankenstein]
sorted titles:          [Dune, Frankenstein, Neuromancer]
list literal:           6
map literal:            2
string concat (+=):     Book shelf
assignment and ;:       6
static field (3.0):     true
static method (3.0):    43
record (6.0):           978-0-441-17271-9
array length (6.0):     3

Two things changed from the books' point of view. In JSP 1.2 there was no EL at all, and in a JSTL 1.0 tag the EL lived inside the library and ended at the attribute, which is why the books' claim that EL does not work in template text is true only of that pair; since JSP 2.0 ${...} works anywhere in the page, as the ignored-EL table above shows for the one case (an old descriptor) where it still does not. And the jump from "property access" to "a small expression language" means that the logic a 2003 page put in a scriptlet can often be an expression, which is the direction Lambdas, Collections and Streams takes it; the operators and conversions are on Operators and Type Coercion, the static access, records and Optional on Static Access, Records and Optional, the resolver chain on Configuration and ELProcessor, and the basics on Expression Language Basics.

The EL pitfalls of an upgrade are mostly in the table the earlier pages collected, and the three that bite first are these:

  • ${bean.class.name} stopped parsing (shown above); write ${bean.getClass().getName()}.

  • A record is read by its component name (${isbn.code}), and on Tomcat 11 a bean-style getter on a record is not reachable as a property.

  • Optional is not resolved unless the application registers OptionalELResolver; support for records is on by default.

Tomcat 9 to 10.1 to 11: What the Container Changed

The container is the other half of every migration, and the Tomcat migration guides list the breaking changes. The borders that matter for this section, from the guides for 10.0 and for 11.0:

Border Changes

9.0 to 10.0

Servlet 5.0, Pages 3.0, EL 4.0, WebSocket 2.0 and Authentication 2.0: the Java package of each changes from javax.* to jakarta.*, and applications must be recompiled or converted; the guide names the migration tool and the deployment-time legacyAppBase route for that

10.0 to 10.1

Servlet 6.0, Pages 3.1, EL 5.0; Jakarta EE 10; Java 11 minimum

10.1 to 11.0

Java 17 minimum; Servlet 6.1, Pages 4.0, EL 6.0; every method and class deprecated in the 3.1 Pages API and the 5.0 EL API removed; the support for running under a SecurityManager removed (Jakarta EE 11 dropped it); the FailedRequestFilter removed, because parameter methods can now throw; HTTP/2 server push removed

The notable-changes list of the 11.0 guide has two entries a Struts or JSP application may meet when it moves: the default maxParameterCount of the Connector went from 10,000 to 1,000 in 11.0.0-M5, and the cookie parser treats the quotes of a quoted cookie value as part of the value, as RFC 6265 requires. A form with more than a thousand fields (a Struts action binding a large collection, an editable grid) is the application that notices the first one.

Two behaviours of the container that the older books assume and 11.0.26 does not provide are worth keeping next to the table: the character encoding of requests and responses defaults to UTF-8 through the stock conf/web.xml (the specifications still say ISO-8859-1; see Character Encoding), and Jasper compiles for Java 17 unless compilerSourceVM and compilerTargetVM say otherwise, so Java 21 syntax in a scriptlet needs the two parameters on the jsp servlet (see Jasper Configuration and Precompilation).

The Tomcat distribution still ships an examples web application (servlets, JSP and WebSocket samples, with a web.xml at version="6.1" in 11.0.26), which is a convenient smoke test for a new container but not a style guide: its samples are written for the container, not for an application. The sample application of the Tomcat application developer’s guide is the older, smaller reference for the layout of a WAR.

Struts 2.3 to 2.5 to 6 to 7

Struts changed more between 2.3 and 7.4 than the JSP platform did, and the Struts migration guides on the project wiki are written as a sequence on purpose: 2.3 to 2.5, 2.5 to 6.0.0, then 6.x to 7.x, each one a release line that can be reached and tested before the next. The version table gives the facts that decide the order; the dates are the ones of the end-of-life page and the announcements.

Line Baseline Namespace and packages Status on 2026-10-10

Struts 2.3.x

Java EE era; the 2.3 DTD

javax.*, com.opensymphony.xwork2.*

End of life since 2019-09-12

Struts 2.5.x (GA 2016-05-09)

The 2.3 to 2.5 guide: StrutsPrepareAndExecuteFilter moves to org.apache.struts2.dispatcher.filter; the 2.5 DTD; HTML 5 themes

javax.*, com.opensymphony.xwork2.*

End of life announced 2023-10-30; last release 2.5.33

Struts 6.x (6.0.0 on 2022-06-06)

Java 8 and a Servlet 3.1 container per the 2.5 to 6.0.0 guide; OGNL expressions limited to 256 characters

javax.*, com.opensymphony.xwork2.*

Maintained: 6.12.0 released 2026-10-02

Struts 7.x (7.0.0 on 2024-12-19)

Java 17 and Jakarta Servlet 6 per the 6 to 7 guide

jakarta.*, org.apache.struts2.*

Current: 7.4.0 released 2026-10-02

The wiki’s own documents disagree on the baseline of 6.0.0: the 2.5 to 6.0.0 guide says Java 8 and Servlet API 3.1, while a Getting Started page of the Struts site says Servlet API 2.5 or higher. This page follows the migration guide; a number for a current 6.x build should be read from the Version Notes of that build. The Struts 6 line is the supported intermediate step because it is the one that kept the com.opensymphony.xwork2 names while it already carried most of the security changes, and the jump to the jakarta namespace is the one of 7.0.0, whose Version Notes list "Migrated to JakartaEE", Java 17 as the minimum and the move of all com.opensymphony.xwork classes to org.apache.struts2.

The flow chart is the order of work the guides imply.

flowchart LR A[Struts 2.3] --> B[2.5.33] B --> C[6.12.0] C --> D[7.4.0] B -.- B1[Filter class, DTD, tag attributes, plugins] C -.- C1[Constants, Aware interfaces, no static OGNL, Velocity plugin] D -.- D1[jakarta, org.apache.struts2, StrutsParameter, allowlist, upload]

The sections below take each kind of change in turn. Every "after" that is code was run on Struts 7.4.0 with Tomcat 11.0.26; the "before" lines are Struts 2.3-era text, labelled as such, and their failures were provoked on 7.4.0 where that shows what a half-migrated application does.

The Filter and the Descriptor

The filter changed class three times. Struts 2.0 used FilterDispatcher, optionally with ActionContextCleanUp; Struts 2.3 used org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter; 2.5 moved it to org.apache.struts2.dispatcher.filter, which is the name of 7.4.0, and the 2.3 to 2.5 guide tells to replace it in web.xml. Naming either older class on 7.4.0 gives the same failure as the javax filter above: the context starts, the filter does not, and every URL is 404.

<!-- Struts 2.0 -->
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
<!-- Struts 2.3, the name the 2.3 to 2.5 guide replaces -->
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
<!-- Struts 2.5 to 7.4 -->
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
SEVERE [Catalina-utility-2] org.apache.catalina.core.StandardContext.filterStart Exception starting filter [struts2]
	java.lang.ClassNotFoundException: org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
SEVERE [Catalina-utility-2] org.apache.catalina.core.StandardContext.filterStart Exception starting filter [struts2]
	java.lang.ClassNotFoundException: org.apache.struts2.dispatcher.FilterDispatcher

The descriptor namespace is not part of Struts’s problem. A web.xml that still declares xmlns.jcp.org and version="3.1" (the shape the Struts web-xml documentation page still shows) deployed on Tomcat 11.0.26 with the Struts 7.4.0 filter and answered 200, so the descriptor can be modernised after the filter and the packages, not before.

The DTD of struts.xml is similar. The 2.3 to 2.5 guide says to move from the 2.3 DTD to the 2.5 DTD, and the 2.5 to 6.0.0 guide to the 6.0 one. The jar of 7.4.0 still contains struts-2.0.dtd to struts-6.0.dtd and a struts-6.5.dtd, and a struts.xml that declared the 2.3 DOCTYPE started and served normally (/books?filter=x printed Filter: x). The target is the 6.0 DTD, because no 7.0 DTD exists (Configuration: struts.xml).

<!-- 2.3 -->
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
        "http://struts.apache.org/dtds/struts-2.3.dtd">
<!-- 6.0, still the newest on the Struts site and the one to write for 7.4 -->
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
        "https://struts.apache.org/dtds/struts-6.0.dtd">

Packages, Aware Interfaces and Actions

The 7.0.0 repackaging is the change that breaks the most source files and the one a search and replace mostly fixes: "The com.opensymphony.xwork2 package has been renamed/migrated to org.apache.struts2. A simple search/replace should be all that is necessary", in the words of the migration guide, with the text and locale providers in their own subpackages (org.apache.struts2.text, org.apache.struts2.locale) and Action and ActionChainResult moved to org.apache.struts2.action and org.apache.struts2.result. The guide’s table writes the old package of some entries as org.opensymphony.xwork2; the real old package is com.opensymphony.xwork2.

Struts 6.x and before Struts 7.x

com.opensymphony.xwork2.ActionSupport

org.apache.struts2.ActionSupport

com.opensymphony.xwork2.Action

org.apache.struts2.action.Action

com.opensymphony.xwork2.ActionChainResult

org.apache.struts2.result.ActionChainResult

com.opensymphony.xwork2.interceptor.Interceptor, AbstractInterceptor

org.apache.struts2.interceptor.Interceptor, AbstractInterceptor

com.opensymphony.xwork2.ActionInvocation, ActionContext

org.apache.struts2.ActionInvocation, ActionContext

com.opensymphony.xwork2.ObjectFactory, UnknownHandler

org.apache.struts2.ObjectFactory, UnknownHandler

com.opensymphony.xwork2.TextProvider, LocalizedTextProvider

org.apache.struts2.text.TextProvider, LocalizedTextProvider

com.opensymphony.xwork2.LocaleProvider

org.apache.struts2.locale.LocaleProvider

com.opensymphony.xwork2.validator.validators.ValidatorSupport

org.apache.struts2.validator.validators.ValidatorSupport

com.opensymphony.xwork2.Validateable

org.apache.struts2.Validateable

com.opensymphony.xwork2.util.ValueStack

org.apache.struts2.util.ValueStack

com.opensymphony.xwork2 as a log4j category or in a struts.xml class attribute

org.apache.struts2; no com.opensymphony.xwork2 package exists in the 7.4.0 jar

The Aware interfaces took a second step. The 2.5 to 6.0.0 guide deprecated the interfaces of the old packages in favour of interfaces in org.apache.struts2.action, with these pairs, and 7.4.0 contains only the new ones:

Deprecated in 6.0 (interceptor or util package) Struts 7 (org.apache.struts2.action) Method of the new interface

SessionAware

SessionAware

withSession(Map<String, Object>)

ApplicationAware

ApplicationAware

withApplication(Map<String, Object>)

ParameterAware, HttpParametersAware

ParametersAware

withParameters(HttpParameters)

RequestAware, ServletRequestAware

ServletRequestAware

withServletRequest(HttpServletRequest)

ServletResponseAware

ServletResponseAware

withServletResponse(HttpServletResponse)

ServletContextAware

ServletContextAware

withServletContext(ServletContext)

CookiesAware

CookiesAware

withCookies(Map<String, String>)

PrincipalAware

PrincipalAware

withPrincipalProxy(PrincipalProxy)

(none; the file upload interceptor bound File setters)

UploadedFilesAware

withUploadedFiles(List<UploadedFile>)

The method names were read from the 7.4.0 jar with javap. A class written for Struts 2.3 does not compile against it:

// Struts 2.3 to 6.x -- does not compile on 7.4.0
import com.opensymphony.xwork2.ActionSupport;
import java.util.Map;
import org.apache.struts2.interceptor.SessionAware;

public class LendAction extends ActionSupport implements SessionAware {
    private Map<String, Object> session;

    @Override
    public void setSession(Map<String, Object> session) {
        this.session = session;
    }
}
LegacyAction.java:1: error: package com.opensymphony.xwork2 does not exist
LegacyAction.java:3: error: cannot find symbol  -- class SessionAware, location: package org.apache.struts2.interceptor
LegacyAction.java:5: error: cannot find symbol  -- class ActionSupport
LegacyAction.java:5: error: cannot find symbol  -- class SessionAware
4 errors

The Struts 7 form of the same action, which counts the Bookshelf’s lend operations in the session, built and ran on 7.4.0; three requests with one cookie jar printed lent=1, lent=2 and lent=3:

// Struts 7.x
import java.util.Map;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.action.SessionAware;

public class LendAction extends ActionSupport implements SessionAware {
    private Map<String, Object> session;
    private int lent;

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

    @Override
    public String execute() {
        lent = (int) session.getOrDefault("lent", 0) + 1;
        session.put("lent", lent);
        return SUCCESS;
    }

    public int getLent() {
        return lent;
    }
}

The pages that teach the 7.4.0 side are Actions, Interceptors, Custom Interceptors, Results and Result Types and Value Stack, ActionContext and OGNL. Many pages of the official Struts documentation still show the old package names (the collected list is in Inconsistencies in the Official Documentation), so a class that compiled in a copy of the documentation may not compile in the project.

Tag Attributes and Removed Tags

The 2.3 to 2.5 guide lists four changes that break a JSP, and they fail at translation time, which makes them easy to find. A page for each of them was requested on 7.4.0:

Struts 2.3 Struts 2.5 and later Result on 7.4.0 / Tomcat 11.0.26

<s:url id="u" action="books"/>

<s:url var="u" action="books"/>

JasperException: Attribute [id] invalid for tag [url] according to TLD; the var form printed the URL

<s:set name="x" value="1"/>

<s:set var="x" value="1"/>

According to the TLD or the tag file, attribute [var] is mandatory for tag [set]

<s:property escape="false" .../>

<s:property escapeHtml="false" .../>

Attribute [escape] invalid for tag [property] according to TLD; the escapeHtml form printed the raw <b> it was given

<s:div cssClass="x">hello</s:div>

plain <div class="x">

No tag [div] defined in tag library imported with prefix [s]

The id attribute became var on s:action, s:append, s:bean, s:date, s:generator, s:iterator, s:merge, s:number, s:set, s:sort, s:subset, s:text and s:url, and the guide adds that field names that start with a single lower-case letter (sTrng) need accessors named getsTrng, with the reference WW-3909. The 2.5 to 6.0.0 guide adds the s:date change to DateTimeFormatter patterns, and the same guide notes the FreeMarker auto-escaping that makes ?html in custom templates an error. The tag pages of this section are Generic Tags, UI and Form Tags and Themes and Templates; the 7.0 rename of the FreeMarker template variable parameters to attributes (WW-5465), which affects custom themes and s:component templates, is explained there.

Constants

All XWork constants that had been deprecated were removed in 6.0.0 and replaced by struts.* names; the 2.5 to 6.0.0 guide lists them. A configuration that keeps the old names is not an error, it is silently ignored, which is why the table is worth reading line by line. The row marked "verified" was tested: a WAR with the old name struts.xwork.chaining.copyMessages still did not copy messages, and only the new name did.

Removed in 6.0.0 Replacement

devMode

struts.devMode

reloadXmlConfiguration

struts.configuration.xml.reload

collectionConverter, dateConverter, numberConverter, stringConverter, arrayConverter

struts.converter.collection, .date, .number, .string, .array

logMissingProperties, enableOGNLExpressionCache, enableOGNLEvalExpression

struts.ognl.logMissingProperties, struts.ognl.enableExpressionCache, struts.ognl.enableEvalExpression

allowStaticMethodAccess

struts.ognl.allowStaticMethodAccess, itself without effect since static method access was removed (see below)

ognlExcludedClasses, ognlExcludedPackageNamePatterns, ognlExcludedPackageNames

struts.excludedClasses, struts.excludedPackageNamePatterns, struts.excludedPackageNames

additionalExcludedPatterns, additionalAcceptedPatterns, overrideExcludedPatterns, overrideAcceptedPatterns

struts.additional.excludedPatterns, struts.additional.acceptedPatterns, struts.override.excludedPatterns, struts.override.acceptedPatterns

struts.xwork.chaining.copyErrors, copyFieldErrors, copyMessages (verified)

struts.chaining.copyErrors, struts.chaining.copyFieldErrors, struts.chaining.copyMessages

xwork.autoGrowCollectionLimit

struts.ognl.autoGrowthCollectionLimit

objectFactory.classloader

struts.objectFactory.classloader

xwork.loggerFactory

not used any more

The constants themselves are documented on Configuration: struts.xml and Constants, and a few of the "new constants" of the same guide arrived with their own page: struts.i18n.search.defaultbundles.first (Localization and I18n) and struts.ui.escapeHtmlBody (UI and Form Tags). The 2.3 and 2.5 era constant struts.devMode and the struts.properties file are the same as in the books, with the change that struts.properties is the file that works (an application default.properties is never read) and the <constant> in struts.xml is the form the documentation prefers.

Dynamic Method Invocation and the Method Prefixes

In the books of the Struts 2.0 era, the action!method syntax was on by default. Since 2.5 it is a switch, struts.enable.DynamicMethodInvocation, off by default (7.4.0 default.properties says false), and the replacement is strict method invocation with an <allowed-methods> list. The prefixes method: and action: are separate switches again. The Bookshelf’s BookAction got a lend() method and two mappings, one with a wildcard, and was requested in each combination on 7.4.0:

<package name="default" extends="struts-default" namespace="/">
    <action name="books" class="org.example.bookshelf.BookAction">
        <result>/WEB-INF/views/books.jsp</result>
    </action>
    <action name="book-*" class="org.example.bookshelf.BookAction" method="{1}">
        <result>/WEB-INF/views/books.jsp</result>
        <allowed-methods>lend</allowed-methods>
    </action>
</package>
Request DMI off (default) DMI on, <allowed-methods>lend on books Notes

/books

execute ran, 200

not tested

the default method

/books!lend and /books!lend.action

404, There is no Action mapped for namespace [/] and action name [books!lend]

lend ran, 200

! is part of the action name when DMI is off

/books.action?method:lend

execute ran (the parameter is ignored)

lend ran

the method: prefix needs DMI

/book-lend and /book-lend.action

lend ran, 200

not tested

wildcard method selection with <allowed-methods>

/book-giveBack

404, There is no Action mapped

not tested

the method is not in <allowed-methods>

The change in practice is that a link or a form button written action!method in a 2.0-era page stops working with a 404 that says there is no such action, and the repair is to map the method as an action of its own. In struts.xml, <allowed-methods> must come after the <result> elements; the other order makes the filter fail to start. The issue statement that the action: and redirect: prefixes were removed needs correcting for 7.4.0: method: and action: still exist behind their switches (struts.mapper.action.prefix.enabled is false by default), while the redirect: and redirectAction: prefixes are not in the 7.4.0 DefaultActionMapper. The page on Wildcard Mappings and Method Invocation has the details.

@StrutsParameter, OGNL and the Allowlist

The 6 to 7 guide calls the next group "stronger security", and it is where an application that compiles after the repackaging still misbehaves, because nothing fails: a value is silently not bound or a property silently prints nothing. Three Struts 7.4.0 defaults, read from default.properties, decide it: struts.parameters.requireAnnotations is true, struts.allowlist.enable is true, and struts.ognl.allowStaticFieldAccess is false.

The first is parameter injection. Until 6.x any public setter received a request parameter; in 7.0 each injection point needs @StrutsParameter. The unannotated BookAction below ignored ?filter=x; with the annotation, or with the constant set to false, the value arrived:

// Struts 6.x -- any public setter binds
public void setFilter(String filter) {
    this.filter = filter;
}

// Struts 7.x
@StrutsParameter
public void setFilter(String filter) {
    this.filter = filter;
}
Variant of the Bookshelf’s BookAction on 7.4.0 GET /books?filter=x printed

setFilter without the annotation

`Filter: ` (empty, status 200, no error on the page)

setFilter with @StrutsParameter

Filter: x

setFilter without it and <constant name="struts.parameters.requireAnnotations" value="false"/>

Filter: x

The 6 to 7 guide even gives a regular expression to find the setters and a replacement that adds the annotation to every *Action.java; a review of the result is part of the migration, because the point of the annotation is that someone decides what may be bound. The guide’s constants for the transition are struts.parameters.requireAnnotations and, named in the 7.4.0 Version Notes and present in the 7.4.0 default.properties, struts.parameters.requireAnnotations.transitionMode. Parameter Injection and @StrutsParameter explains the depth and the rest.

The second is the OGNL allowlist. A page that reads a property of a plain Java class outside the action’s own package prints nothing, with a warning in the log, until the class or its package is allowlisted. The Bookshelf action got a getFeatured() returning an object of org.example.bookshelf.model.Featured:

featured=[<s:property value="featured.title"/>]
<constant name="struts.allowlist.packageNames" value="org.example.bookshelf.model"/>
Configuration Output of the page

defaults

featured=[]

struts.allowlist.packageNames naming the model package

featured=[Dune]

The third is what OGNL may call. Static method access was removed in 6.0.0 ("it won’t be possible to access static methods using OGNL expression"), and static field access was turned off in 7.0.0. Both were requested on 7.4.0:

max=[<s:property value="@java.lang.Math@max(1,2)"/>] field=[<s:property value="@java.lang.Integer@MAX_VALUE"/>]
Configuration Output

defaults

max=[] field=[]

struts.ognl.allowStaticFieldAccess=true

max=[] field=[] (the guide’s one-line switch is not enough)

struts.ognl.allowStaticFieldAccess=true and struts.allowlist.classes=java.lang.Integer

max=[] field=[2147483647]

The guide describes the constant as the way to bring static fields back; on 7.4.0 the declaring class must also pass the allowlist, and calls of static methods stay blocked in every configuration tried (the struts.ognl.allowStaticMethodAccess constant of the old books does not exist in 7.4.0). The migration advice of the guide is better than the switch: copy the value into the action, or into a helper that the action exposes. The 6 to 7 guide also changes the limits: the OGNL expression length is limited to 256 characters since 6.0.0, and the 7.0 section of the guide says "reduced from 200 to 150"; the 7.4.0 default.properties and the security guide say 256, which is the value this section uses (see Inconsistencies in the Official Documentation). Namespace matching became exact in 7.0 (struts.mapper.alwaysSelectFullNamespace is true, struts.actionConfig.fallbackToEmptyNamespace is false), proxied objects are blocked (struts.disallowProxyObjectAccess), default-package classes are blocked (struts.disallowDefaultPackageAccess) and custom OGNL maps are blocked (struts.ognl.disallowCustomOgnlMap); Security Hardening and Value Stack, ActionContext and OGNL have the rest.

File Upload

The fileUpload interceptor and its File, xxxContentType and xxxFileName setters are gone. 6.4.0 added the actionFileUpload interceptor with UploadedFilesAware, and 7.0.0 removed the old one; the 6 to 7 guide’s "remove the old interceptor reference" instruction is not optional, because an application that still names it does not start:

<!-- Struts 2.x to 6.3 -->
<interceptor-ref name="fileUpload"/>
<interceptor-ref name="defaultStack"/>

<!-- Struts 6.4 and later -->
<interceptor-ref name="actionFileUpload">
    <param name="maximumSize">1048576</param>
    <param name="allowedTypes">text/plain</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
public class UploadAction extends ActionSupport implements UploadedFilesAware {
    @Override
    public void withUploadedFiles(List<UploadedFile> uploadedFiles) {
        for (UploadedFile f : uploadedFiles) {
            // f.getOriginalName(), f.length(), f.getContentType()
        }
    }
}
$ curl -s -F "upload=@dune.txt;type=text/plain" http://localhost:18180/bookshelf-struts/upload
dune.txt 5 bytes text/plain;

$ curl -s -w "[%{http_code}]" -F "upload=@dune.pdf;type=application/pdf" http://localhost:18180/bookshelf-struts/upload
... No result defined for action org.example.bookshelf.UploadAction and result input [404]

# the old reference on 7.4.0
Caused by: Unable to find interceptor class referenced by ref-name fileUpload - interceptor-ref - file:.../WEB-INF/classes/struts.xml:11:49

The rejected type is the input result of the action, which is mapped to nothing and answers 404; the page File Upload and Download shows the mapping and the S2-066 and S2-067 path-traversal bulletins that are the reason for the replacement.

Plugins and Views

The plugin list is the part of the books that has aged most. The 2.3 to 2.5 guide removes the Dojo, Codebehind, JSF and Struts 1 plugins and keeps a single Tiles plugin (Tiles 3); the 2.5 to 6.0.0 guide removes the SiteGraph plugin and moves Velocity into struts2-velocity-plugin; the 6 to 7 guide removes the Portlet plugins, the Pell Multipart plugin, the DWR plugin and the SiteMesh plugin ("use direct integration with Sitemesh 3"). The plugins Maven Central publishes for 7.4.0 are the 17 in the table; the 7.4.0 BOM still lists four more artifacts that do not exist at that version, which is one of the inconsistencies collected below.

Removed or replaced Replacement and where it is explained

Dojo plugin, sx: tags, the ajax theme (2.5)

Plain fetch or htmx with the JSON and REST plugins, JSON Plugin and Ajax and REST Plugin

Codebehind plugin (2.5)

Convention Plugin and Annotations

Struts 1 plugin (2.5)

Struts 1 (Classic) and Migration

Tiles 2 (struts2-tiles-plugin before 2.5), Apache Tiles itself (retired to the Attic)

struts2-tiles-plugin with Tiles 3, bundled since 6.3.0; the legacy OGNL: evaluator of Tiles attributes is disabled by default in 7.4.0 and throws EvaluationException unless struts.tiles.ognl.legacy.enabled=true; Tiles and Page Layouts

Velocity in core (until 5.x)

struts2-velocity-plugin and packages that extend velocity-default, FreeMarker and Velocity Views

SiteMesh, Portlet, DWR and Pell Multipart plugins (7.0)

No replacement of the same name; SiteMesh 3 is used directly, the others have no successor, Other Plugins

struts.properties for everything

<constant> in struts.xml; struts.properties still works

Spring 2 XML with singleton="false"

Spring Framework 7 with struts2-spring-plugin 7.4.0; scope="prototype", Spring Integration

From 7.0 to 7.4

The lines after 7.0.0 are minor releases that are allowed to break less, and the Version Notes of 7.3.0 and 7.4.0 both have a "Breaking changes" section worth reading before an upgrade of the patch line. The ones a Bookshelf-sized application can meet are in the table (the notes of 7.1.0 and 7.2.0 could not be read for this page, so those two releases are not summarised here):

Release Breaking change

7.3.0

Struts no longer uses SLF4J or java.util.logging internally; annotated wildcard actions are matched most-specific-first; the JSON interceptor uses a fresh reader and writer per request; static content paths are canonicalised; ActionFileUploadInterceptor rejects an upload when a parameter such as maximumSize cannot be applied, so the literal null no longer lifts the size limit

7.4.0

With struts.parameters.requireAnnotations=true, a ModelDriven action’s own members need @StrutsParameter and only the model’s members stay exempt, with struts.parameters.requireAnnotations.transitionMode as the migration path; @StrutsParameter enforcement recognises fluent (non-void) setters; the Tiles OGNL: evaluator is off by default; the REST plugin’s default stacks and the Bean Validation plugin’s stack include the coep, coop and fetchMetadata interceptors, so a REST API for cross-site browser clients must set fetchMetadata.exemptedPaths or disable that reference; the REST plugin rejects bodies longer than struts.rest.content.maxLength (default 2097152); the JasperReports 7 plugin no longer brings jasperreports transitively; five dev-mode setters were removed from SecurityMemberAccess

Both releases are tied to security bulletins, summarised on Security Bulletins: 7.3.0 fixed S2-070 to S2-074 (6.11.0 carries the fixes of S2-072 to S2-074) and 7.4.0 and 6.12.0 fixed S2-075 to S2-078, and the release page marks 7.3.0 and 6.11.0 as releases no longer recommended. That is the last argument for a short upgrade cycle: a Struts application should run the latest release of its line.

From Struts 1

Struts 1 is a different framework, not an older Struts 2, and none of the guides above applies to it. It reached its end of life on 2013-04-05, its last release is 1.3.10 and its bundled Commons BeanUtils had the ClassLoader manipulation vulnerability CVE-2014-0114, which the project would not fix. The concept map from Struts 1 to Struts 7 (and to Spring MVC), the piecemeal migration with both frameworks side by side and the Bookshelf rebuilt as a Struts 7 application are on Struts 1 (Classic) and Migration; the history is on Introduction and History.

Modernisation Paths

A migration does not have to end at "the same application on a newer platform". Three destinations are realistic for a Bookshelf-sized JSP or Struts application, and the choice is the one the repository’s architecture pages already describe for a legacy monolith.

  • Stay on JSP and Struts, current versions. The cheapest path when the application is stable: the steps of this page, in order, then keep the Struts line current. It is the right answer for an internal application that changes little.

  • Struts or JSP to Spring MVC with Thymeleaf (or another template engine), one screen at a time. The strangler-fig path. The Spring MVC page of this section, Spring MVC with JSP, shows the first step (new controllers that still render JSP views, in a WAR), and the Thymeleaf section carries the destination: Thymeleaf: Introduction and Architecture and Thymeleaf: What’s Changed and Migration. The comparison of the web UI options for Spring Boot is Web UI Frameworks, and the javax.* to jakarta.* switch of Spring Boot 3 is in Spring Boot: Getting Started.

  • Faces. A component-based move is a different programming model; it is covered by the JSF: Migrating to Jakarta Faces page and the rest of the JSF (JavaServer Faces) section, and is not described here.

The repository’s Bookshop scenario, a Struts 1 and JSP monolith moved to a modern stack, is the longer story of the first two paths with its phases, risks and decisions, in Migration Path: Legacy to Modern Stack, with the general view of migration strategies in Migration Overview; the Struts 1 specifics (a dual request processor, the concept map) are on Struts 1 (Classic) and Migration.

The mechanical core of the strangler-fig path is routing: the old application and the new code share one WAR and one URL space, and a rule decides who answers. In a Struts application the rule is a constant. With the default struts.action.extension of action,,, the Struts filter claims the extensionless URLs as well, so a new servlet at /api/books was shadowed (Struts answered 404 for an action it did not have). Narrowing the constant to action gave the new code its URLs and kept the old screens:

@WebServlet("/api/books")
public class ApiBooksServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("application/json");
        resp.getWriter().write("[\"Dune\",\"Neuromancer\"]");
    }
}
<constant name="struts.action.extension" value="action"/>
Request on 7.4.0 / Tomcat 11.0.26 Default action,, action

/books (old Struts action)

200

404 (links must say books.action)

/books.action

200

200

/api/books (new servlet)

404 from the Struts filter

200, ["Dune","Neuromancer"]

The price is visible in the second column of the first row: every link and redirect of the legacy screens must carry the extension. The alternative that keeps the extensionless Struts URLs is struts.action.excludePattern, described with the deployment settings on Deployment and Production.

What Is In Progress for Jakarta EE 12

Jakarta EE 12 is under development and the Jakarta specification pages list the next versions of the technologies of this section: Pages 4.1, Tags 3.1, EL 6.1 and Servlet 6.2. None of the pages the section teaches is removed. The Tags 3.1 release page (under development) lists one feature, java.time support in fmt:formatDate and fmt:parseDate, and Java 21 as the minimum; the EL 6.1 page lists pluggable caches and the move of the TCK next to the planned Elvis operator and collection merge with +, and names Java 17 as the minimum (the issue’s appendix says Java 21 for the platform; the EL page says 17, see Inconsistencies in the Official Documentation). These are milestones, not releases, so nothing in this section depends on them; the section’s baseline remains Jakarta EE 11. A project that wants to prepare changes nothing in its code and adds Java 21 to its build matrix, which is also the version the examples of this section use.

What Changed Recently

The changes of the last two years that an application meets first, in the order of the sections above, so that a reader returning to the page after a release can start here:

When Change

Pages 4.0, EL 6.0, Servlet 6.1 (Jakarta EE 11)

isThreadSafe and jsp:plugin removed; the deprecated jakarta.servlet.jsp.el classes removed; records in EL by default; Optional and array length; sendRedirect with a chosen status, Charset overloads, error dispatch as GET with jakarta.servlet.error.method

Tomcat 11.0.x

Java 17 minimum, no SecurityManager, maxParameterCount 1,000, the FailedRequestFilter removed; the migration tool bundled as 1.0.12 in 11.0.26 and 1.0.13 on Maven Central

Struts 7.0.0 (2024-12-19)

jakarta.* and Java 17; org.apache.struts2.*; @StrutsParameter; allowlist, no static field access, proxies blocked; the old fileUpload interceptor and the Portlet, Pell Multipart, DWR and SiteMesh plugins removed

Struts 7.3.0

Wildcard actions of the Convention plugin matched most-specific-first; stricter JSON reader and writer per request; the upload interceptor no longer lets a null parameter lift a limit

Struts 7.4.0 and 6.12.0 (2026-10-02)

Fixes for S2-075 to S2-078; ModelDriven actions need @StrutsParameter for their own members; the REST and Bean Validation stacks add fetchMetadata, coep and coop; the Tiles OGNL: evaluator is off by default; the legacy RESTful action mapper is deprecated

Where the Reference Books Are Outdated

The section was written from the specifications and the official documentation, with four books consulted only to decide which concepts the section must cover: a book on JSP written for JSP 1.2, a book on JSTL written for JSTL 1.0, a Struts 1 book written for the 1.1 beta, and a Struts 2 book written for Struts 2.0. The table is the section’s complete list of what each of them describes that no longer holds, with the current form and the page that teaches it. It repeats the list that the pages of the section carry in their own "where the books are outdated" parts, so that a reader who owns one of the books can read it from one place.

Area Outdated in the books Current form Where

Platform (all four)

J2EE 1.3 and 1.4, Servlet 2.3 and 2.4, JSP 1.2 and 2.0, JDK 1.2 to 5, Tomcat 4 to 6, the javax.* packages

Jakarta EE 11 with Servlet 6.1, Pages 4.0 and EL 6.0 on Java 21 (17 minimum), Tomcat 11, jakarta.*; since Tomcat 10 a javax application must be recompiled or converted with the migration tool or legacyAppBase (From javax to jakarta, with the Tomcat Migration Tool)

Getting Started, Servlet Fundamentals

Scripting (JSP book)

Scriptlet-centric pages as the norm

EL, JSTL and tag files; scriptlets switched off with scripting-invalid

Scripting Elements, Configuration and Property Groups

EL (JSP and JSTL books)

No EL (JSP book), or EL only inside the attributes of JSTL 1.0 tags (JSTL book)

Container EL everywhere since JSP 2.0, and EL as its own specification with lambdas, collection construction, streams, +=, ;, =, static fields and methods and imports, records (6.0, on by default), Optional (6.0, opt-in) and array length (6.0) (EL 2.2 to 6.0)

Expression Language Basics, Lambdas, Collections and Streams, Static Access, Records and Optional

JSTL URIs (JSTL book)

http://java.sun.com/jstl/core and the _rt twin libraries

One library per area with jakarta.tags.core, fmt, sql, xml and functions; the java.sun.com/jsp/jstl/* URIs still resolve on the 3.0.1 implementation; fn: functions exist since JSTL 1.1; x:parse takes doc (JSTL 1.0 to 3.0: the Library URIs)

JSTL Core, JSTL Functions, JSTL SQL and XML

Custom tags (JSP and JSTL books)

Classic Tag and BodyTag only, the JSP 1.2 TLD DTD, manual EL evaluation with a third-party evaluator

SimpleTag, JspFragment, dynamic attributes, tag files and the XML Schema TLD; BodyTag.EVAL_BODY_TAG removed in Pages 4.0

Custom Tags: SimpleTag, Tag Files and Layouts, TLDs and EL Functions

Removed JSP features

jsp:plugin, jsp:params, jsp:fallback and applets; the isThreadSafe page attribute

Both deprecated in Pages 3.1 and removed in 4.0 (verified: Invalid standard action, has invalid attribute: [isThreadSafe]); the deprecated classes of jakarta.servlet.jsp.el and JspException.getRootCause() removed as well (JSP 1.2 to 4.0)

Directives, Lifecycle and Translation

Removed Servlet API

SingleThreadModel, HttpSessionContext, HttpUtils (the JSP book uses HttpUtils.getRequestURL)

Removed in Servlet 6.0; use request.getRequestURL() and thread-safe servlets (Jakarta Servlet: What Changed and What Was Removed)

Servlet Fundamentals, MVC with Servlets and JSP

web.xml (JSP and Struts 1 books)

The Servlet 2.3 DTD, <taglib> directly under <web-app>

The Servlet 6.1 schema (web-app_6_1.xsd, version="6.1"), <jsp-config>, annotations, TLD auto-discovery; EL is ignored under a 2.3 descriptor (verified)

Getting Started, Configuration and Property Groups

Data access in views (JSP and JSTL books)

JDBC in JSPs, sql:* tags, sql:setDataSource with literal passwords, sun.jdbc.rowset.CachedRowSet

Data access in services and repositories (see the Hibernate pages of the repository); sql:* for prototypes only, as the JSTL specification itself says

JSTL SQL and XML

Client formats (JSP book)

WML and WAP, VoiceXML, application/x-msexcel

HTML5, JSON, CSV or XLSX with Content-Disposition

Character Encoding, File Upload and Download

Authentication (JSP and JSTL books)

A hand-made login with passwords in beans, session or tables

Container security (login-config, security-constraint, Jakarta Security) or Spring Security; hashed passwords; placeholders in examples

Security, Forms, Validation and Sessions

Struts 1 (Struts 1 book)

Struts 1.0 and 1.1 beta

End of life since 2013 with unfixed vulnerabilities (CVE-2014-0114); perform() removed in 1.2, ActionError deprecated, data-sources removed, the 1.3 Commons Chain request processor; read it only to migrate (From Struts 1)

Struts 1 (Classic) and Migration

Struts 2 filter (Struts 2 book)

FilterDispatcher and ActionContextCleanUp

org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter (The Filter and the Descriptor)

Architecture and Request Lifecycle

Struts 2 packages (Struts 2 book)

com.opensymphony.xwork2.*

org.apache.struts2.* since 7.0; the Aware interfaces renamed in 6.0 (withSession and the rest) (Packages, Aware Interfaces and Actions)

Actions, Interceptors

Annotations (Struts 2 book)

Zero-configuration actionPackages, Codebehind and @Result(value=...)

The Convention plugin with @Action, @Result(location=...), @Namespace, @ParentPackage, @InterceptorRef and @ResultPath

Convention Plugin and Annotations

Ajax (Struts 2 book)

The Dojo plugin, the ajax theme, s:datetimepicker and sx: tags, a hand-built XStream JSON result

Dojo removed in 2.5; the JSON plugin, the REST plugin, plain fetch or htmx; s:datetextfield; theme="ajax" fails with TemplateNotFoundException

JSON Plugin and Ajax, REST Plugin, Themes and Templates

DMI (Struts 2 book)

action!method on by default

struts.enable.DynamicMethodInvocation is false by default; strict method invocation with <allowed-methods> (2.5). The method: and action: prefixes remain behind switches (action: also needs struts.mapper.action.prefix.enabled), and the redirect: and redirectAction: prefixes are not in 7.4.0 (Dynamic Method Invocation and the Method Prefixes)

Wildcard Mappings and Method Invocation

OGNL (Struts 2 book)

Free %{...} evaluation and @class@method static calls

Static method access removed (6.0), static field access off by default and needing the allowlist (7.0), the 256-character expression limit, excluded classes and packages, the OGNL allowlist on by default (7.0) and the OGNL Guard (@StrutsParameter, OGNL and the Allowlist)

Value Stack, ActionContext and OGNL, Security Hardening

Parameter binding (Struts 2 book)

Any public setter bound from request parameters

@StrutsParameter required on every injection point (struts.parameters.requireAnnotations, opt-in from 6.4, default in 7.0), with depth and allowDynamicKeys

Parameter Injection and @StrutsParameter

File upload (Struts 2 book)

The fileUpload interceptor with File, xxxContentType and xxxFileName setters

actionFileUpload with UploadedFilesAware.withUploadedFiles(List<UploadedFile>) (6.4); the old interceptor removed in 7.0 after S2-066 and S2-067 (File Upload)

File Upload and Download

Plugins (Struts 2 and Struts 1 books)

SiteMesh, Portlet, DWR, Pell Multipart (removed in 7), OVal, Embedded JSP, OSGi, Plexus, GXP (not built for 7), Dojo, Codebehind, the Struts 1 plugin (removed in 2.5)

The 17 plugins published at 7.4.0: async, bean-validation, cdi, config-browser, convention, jasperreports, jasperreports7, javatemplates, jfreechart, json, junit, rest, spring, testng, tiles and velocity, xslt (Plugins and Views)

Other Plugins

Tiles (Struts 1 and Struts 2 books)

Struts Tiles 1, Apache Tiles 2

Apache Tiles is retired (Attic, 2019); its code is bundled in struts2-tiles-plugin since 6.3.0 and the OGNL: evaluator is off by default in 7.4.0 (use S2:); alternatives are tag-file layouts and SiteMesh 3 used directly

Tiles and Page Layouts, Tag Files and Layouts

Velocity (Struts 1 book)

VelocityStruts and VelocityTools

struts2-velocity-plugin, split from core in 6.0, with packages that extend velocity-default

FreeMarker and Velocity Views

struts.properties (Struts 2 book)

Constants in struts.properties

<constant> in struts.xml; constants renamed to struts.* in 6.0 (Constants)

Configuration: struts.xml and Constants

Spring (Struts 2 book)

Spring 2 XML and OpenEntityManagerInViewFilter

Spring Framework 7 with struts2-spring-plugin 7.4.0; struts.objectFactory.spring.autoWire.alwaysRespect is true since 7.2

Spring Integration, Spring MVC with JSP

Beyond the table, the books are narrower than the platform in places where they have nothing wrong to correct: they have no chapter on EL lambdas, collection operations, static access, records and Optional, no tag files or SimpleTag beyond the classic handlers, no property groups in their current schema, no Jasper configuration and precompilation with JspC, no Spring MVC view resolver, no Spring Boot WAR packaging, no Tomcat security advisories and OWASP Java Encoder, and for Struts none of the security model of 6 and 7 (allowlist, excluded patterns, OGNL Guard, strict method invocation, Fetch Metadata, COOP, COEP and CSP interceptors, @StrutsParameter), no testing with the JUnit and TestNG plugins, no Convention naming rules and no REST plugin. Those topics come from the official documentation, and each has its page in the section.

Errata Found in the Books

The books also contain plain errors, found when the examples were compared with the platform they were written for. They are stated here in prose, never reproduced, so that a reader who copies from one of them knows what to expect.

Book Erratum What is right

JSP book

Curly quotes in the code samples of the chapters on syntax, standard actions and beans; typos in tag and class names (CashedRowSet, jsp:setPropety, jsp:sriptlet)

The samples do not compile or translate if copied; the actions are spelled jsp:setProperty and jsp:scriptlet, see Standard Actions and JavaBeans

JSTL book

Appendix A.1 points to chapter 2 for the expression language, which is chapter 3; the statement that EL does not work in template text is true only of JSTL 1.0 on JSP 1.2

Container EL works in template text since JSP 2.0, see EL 2.2 to 6.0 and Expression Language Basics

Struts 2 book

@Result(name="SUCCESS") in the chapter on annotations; the interceptor is written both scoped-model-driven and scopedModelDriven

The constant’s value is lower-case success (Action.SUCCESS); the interceptor name is scopedModelDriven, see ModelDriven, Preparable and CRUD

Struts 1 book

Written against Struts 1.1 beta 2, and defers to an online errata page for the changes of the final release

Struts 1.1 final and later changed details; the 1.3.10 behaviour is the one on Struts 1 (Classic) and Migration

Inconsistencies in the Official Documentation

The official documentation is the source of this section, and it has its own errors and its own age. The pages of the section state the verified value of each fact and say, where it matters, that a source disagrees. The tables collect the cases in one place, grouped by where they came from. "Observed" means what was run on Tomcat 11.0.26 and Struts 7.4.0 in the scratch applications of the section; each row names the page that discusses it with the evidence. The groups are long on purpose: a reader who hits one of these in a real project usually spends an afternoon on it.

Versions, Coordinates and Dates

Source What it says, and what is true Page

struts.ognl.expressionMaxLength: the security guide, default.properties and the 6.0.0 notes

256; the 6 to 7 migration guide says "reduced from 200 to 150" and shows =150. A 323-character expression is refused by default and passes with 400 on 7.4.0

Security Hardening, Configuration

Struts 6 baseline

The 2.5 to 6.0.0 guide says Java 8 and Servlet 3.1; a Getting Started page says Servlet 2.5 and Java 8; the same page uses Struts 7.0.3 and the struts-6.0.dtd

Getting Started (Struts)

Struts DTDs

struts-7.0.dtd and struts-6.5.dtd answer 404 on the Struts site (the index lists up to 6.0); struts-6.5.dtd is bundled in the 7.4.0 jar and apache/struts-examples uses it

Configuration: struts.xml

Maven archetypes

struts2-archetype-blank and -convention stop at 6.0.0; maven-archetype-webapp 1.5 still generates a Servlet 4.0 xmlns.jcp.org web.xml with Java 8

Getting Started, Getting Started (Struts)

struts2-bom 7.4.0

Lists struts2-embeddedjsp-plugin, -gxp-, -osgi- and -plexus-plugin, none of which exists at 7.4.0 on Maven Central (last 6.12.0); the plugins page lists DWR, OVal, OSGi, Plexus, Portlet, Embedded JSP and SiteMesh as bundled

Other Plugins

Struts 7.4.0 build baseline

Core is built against Jakarta EE 10 APIs (Servlet 6.0, Pages 3.1) and runs unchanged on Tomcat 11 (Servlet 6.1); the Spring plugin is built against Spring 6.2.19 and runs with Spring 7.0.9

Spring Integration

apache/struts-examples

Still on struts2.version 7.3.0 although 7.4.0 was released on 2026-10-02, builds for Java 17 and configures Jetty 11, which is end of life

Deployment and Production

Pages 4.0 landing and specification

The landing page says "Jakarta Pages", the specification document and the Maven artifact still say "Jakarta Server Pages"; the document is dated 2024-04-01 although Pages 4.0 ships in Jakarta EE 11; the page lists jakarta.pages:jakarta.pages-api, which does not exist on Maven Central

Introduction and History

GlassFish

The download page still advertises 8.0.4 while Maven Central has 8.0.5; GlassFish 8.0.5 answers X-Powered-By: JSP/3.0 while its error page says JSP/4.0; a record in EL (${b.title}) failed there with PropertyNotFoundException although it works on Tomcat 11 and Jetty 12.1

Getting Started, Deployment and Production

Tomcat 9

The issue wrote the end of support as a date; the Tomcat page says "no earlier than 31 March 2027"; the 9.1.x branch has no end date

The Version Map

EL 6.0 and 6.1

The EL 6.1 page gives Java 17 as the minimum while the platform for EE 12 is stated as Java 21; the array length property is listed both under "6.0 and 5.0" and "5.0 and 4.0" of the change log

Configuration and ELProcessor, Static Access, Records and Optional

Test tooling

The issue says JUnit 5; the verified baseline is junit-jupiter 6.1.3 (same coordinates); with the vintage engine absent Surefire ran only the Jupiter tests and reported success

Testing, Testing and Debugging

CVE-2024-52318 (Tomcat)

The CVE record says "upgrade to 11.0.1, 10.1.32 or 9.0.97" and lists 10.1.31 as affected; the Tomcat 10 page says 10.1.32 was not released and 10.1.33 is the first fixed release

Custom Tags: Classic, Security

Specifications Against Their Implementations

Specification text Observed on Tomcat 11.0.26 Page

Pages 4.0 5.5: jsp:forward after a flushed buffer results in IllegalStateException

A 200 with the already committed text; the server log has IOException: Error: Attempt to clear a buffer that's already been flushed

Includes, Forwards and Page Composition, Standard Actions

Pages 4.0 1.10.5: the content of jsp:include "is not parsed"

The target is a dynamic resource whose output is included; a JSP target is executed (the wording is about the including page’s translation unit)

Includes, Forwards and Page Composition

Pages 4.0 3.3: "the properties in that group apply" (the most specific group)

Each property is taken from the most specific matching group that declares it; include-prelude and include-coda of all matching groups apply in descriptor order

Configuration and Property Groups

Pages 4.0 and Servlet 6.1: the default character encoding is ISO-8859-1

text/html;charset=UTF-8 and UTF-8 form decoding, because the stock conf/web.xml sets request-character-encoding and response-character-encoding; with those lines removed the specification default is observed

Character Encoding

Pages 4.0 1.4.3: a self-referencing error page is a translation error

It compiled and ended in a StackOverflowError at run time

Error Handling

Pages 4.0 Table JSP.1-7: exception is an implicit variable

${exception} is empty even with isErrorPage="true"; ${pageContext.exception} works

Error Handling

Pages 4.0 RTAttributeScriptingEnabled: a request-time attribute value is a translation error when scripting is disabled

Accepted on Tomcat 11 with scripting-invalid; only declarations, scriptlets and top-level expressions are rejected; jakarta.tags.scriptfree does reject it

Scripting Elements

Pages 4.0 6.2.2 and 1.6: quoting of \$, # and %=

\\${expr} in template text becomes \${expr} unevaluated; href="%= 1 %" is rewritten to href=" 1 " in a JSP document

Syntax, Comments and Escaping

Pages 4.0 JspPage Javadoc: a page "should redefine the init() method"

init, destroy and service of HttpJspBase are final; only jspInit and jspDestroy can be redefined

Lifecycle and Translation

Pages 4.0 8.4.3 and 8.5.2: a .tag and .tagx pair, a primitive type and a non-fragment jsp:invoke are translation errors

Accepted silently, or a javac error in the generated class; Jasper uses the .tag file

Tag Files and Layouts

Pages 4.0 7.2.3 and 2.11.2: duplicate TLD URIs are fatal and a non-static function method is a translation error

The later TLD wins silently; a non-static function fails at request time with NullPointerException

TLDs and EL Functions

Pages 4.0 appendix A and 7.3: web-app_2_3.dtd, <webapp> roots, web-jsptaglibrary_2_1.xsd

Stale examples; the current schemas are web-app_6_1.xsd and web-jsptaglibrary_4_0.xsd; Tomcat does not validate TLDs against the schema

Getting Started, TLDs and EL Functions

Servlet 6.1 Table 10-1: seven error request attributes

The eighth, jakarta.servlet.error.method, is defined by section 10.9.2, issue 544 and RequestDispatcher.ERROR_METHOD

Servlet Fundamentals

Servlet 6.1 5.5: sendRedirect throws IllegalArgumentException for an invalid location

Location: http://[bad host was sent with 302 (relative redirects are on by default)

Servlet Fundamentals

Servlet 6.1: HttpServletMapping.getMatchValue()

Returned null, not an empty string, for /map against /map/*; a jsp-property-group or servlet-mapping pattern that mixes a path and an extension fails deployment with Invalid <url-pattern>

Servlet Fundamentals, Directives

EL 6.0 spec: the typos in the lambda and stream examples (.List(), sorted(p->p.unitPrice), an unbalanced reduce), "null results in a NullPointerException`", the resolver name `StaticFieldResolver, section references 1.23 and 1.19

The API class is StaticFieldELResolver; type conversion is section 1.25 and the identifier rule is in 1.26; sorted with a one-parameter lambda silently returns an arbitrary order; limit(null) raises an ELException wrapping the NullPointerException

Lambdas, Collections and Streams, Expression Language Basics

EL 6.0 spec 1.25.5 and 1.9.2: boolean coercion and equality

'yes' and true is silently false because Boolean.valueOf never throws; price == 12.50 is false for a BigDecimal 12.50 (scale); letter == 'a' throws ELException

Operators and Type Coercion

Pages 4.0 1.24 and 2.10: imports and the resolver chain

An explicit import of an interface such as HttpServletResponse raises ELException, and on the implicit import list it is skipped silently, so ${HttpServletResponse.SC_NOT_FOUND} prints nothing; OptionalELResolver is opt-in and ${empty opt} stays false for an empty Optional

Static Access, Records and Optional

JspApplicationContext.addELResolver Javadoc

A shorter resolver order than the specification chapter; registration was possible in a first-request servlet and failed only after a JSP had been evaluated

Configuration and ELProcessor

Pages 4.0 JSP documents 5.12 and 6.4

> in a text node is accepted and < and && are rejected; the examples still use the dead java.sun.com JSTL namespace; the XML view always shows jsp:root version="2.0"

JSP Documents and XML Syntax

JSTL Documentation Against the Implementation

Documentation Observed with JSTL 3.0.1 Page

c:url value and c:redirect url documented as required=false

Omitting them fails with NullPointerException in UrlSupport.resolveUrl; c:import url="" raises NullAttributeException

JSTL URLs, Imports and Redirects

c:import "works seamlessly with the XML tags"

var stays empty for a static resource or a file under WEB-INF and the file is written into the page; x:parse with the import in its body works; a non-2xx status of an imported JSP throws JspTagException

JSTL SQL and XML, JSTL URLs, Imports and Redirects

c:out escapeXml: only false disables escaping

escapeXml="flase", "FALSE", "" and an EL null all print raw markup (Boolean.valueOf semantics)

JSTL Core

c:choose processes the first true c:when

Every c:when test is evaluated, so a failing second test aborts the request even after the first matched

JSTL Core

c:forEach over a String "deprecated"

Still works on 3.0.1; an empty begin or end coerces to 0

JSTL Iteration

fmt locale selection: the preferred locales come from ServletRequest.getLocales()

With no Accept-Language header the implementation sees none and prints the bare value; the JVM default locale is consulted by ResourceBundle as the last step although the specification says it is not

JSTL Formatting, JSTL I18n and Messages

Tags 3.0 9.11.1: Config.FMT_TIMEZONE

The constant is Config.FMT_TIME_ZONE

JSTL Formatting

Tags 3.0 chapter 14 and 15 examples

Not well formed (<jsp-version>2.0</jsp.version>), fn:substring with two arguments, param:userName with a colon, •, $customer/[location='UK'] is not XPath 1.0

JSTL Functions, JSTL SQL and XML, TLDs and EL Functions

Tags 3.0: "requires a Jakarta Server Pages 3.0 container" and EL 4.0

Stale; the library runs on Pages 4.0 and EL 6.0, and the cross references (EL section 1.23, Pages 1.8.2) are garbled

JSTL Core

Tags 3.0: only jakarta.tags.* documented

The 3.0.1 implementation jar still ships the JSTL 1.0 and 1.2 descriptors, so the java.sun.com URIs resolve; http://xmlns.jcp.org/jsp/jstl/* does not

JSTL 1.0 to 3.0: the Library URIs, JSTL I18n and Messages

The Jakarta EE Tutorial pages for Pages and Tags

The Pages page is a stub that says Pages "has been archived in favor of Facelets"; the Tags page says "a subset of Jakarta Tags has been archived" without naming it, while SQL, XML and formatting are still in the Tags 3.0 specification; both link to the Java EE 5 tutorial (javax.*, JSP 2.1, JSTL 1.2), still the last complete official tutorial

JSTL SQL and XML, Introduction and History

Tomcat Documentation Against the Container

Documentation Observed on 11.0.26 Page

Jasper How-To: fork is true by default; keepgenerated and the "keep" in its name

The stock conf/web.xml sets fork to false on the jsp servlet; keepgenerated defaults to true, so generated source stays in work/; compilerSourceVM and compilerTargetVM default to 17, a value of 8 is raised to 17, and JspC -help still prints 1.8

Jasper Configuration and Precompilation, Testing and Debugging

Jasper How-To: addWebXmlMappings

The Ant task needs webXmlInclude as well, otherwise NullPointerException in JspC.mergeIntoWebXml; recompileOnFail retried after about 4 seconds, not at the next access; enablePooling took effect as a servlet init-param; the tag handler pool classes read three more parameters than documented

Jasper Configuration and Precompilation, Custom Tags: Classic

Error Report Valve documentation

Declared in a META-INF/context.xml pipeline it writes the report before the host dispatches <error-page> entries, so every <error-page> stopped working

Error Handling

Manager documentation and tutorials

Session persistence across restarts is off unless configured (pathname default null); older tutorials assume SESSIONS.ser

Servlet Fundamentals

Messages of the JSP translator

Several errors show a raw resource key or an unsubstituted {0} (jsp.error.page.badCombo, jsp.error.setProperty.invalid, Duplicate bean name: [{0}]); a second request after a translation failure can report ClassNotFoundException: org.apache.jsp.<page>

Directives, Standard Actions, Syntax, Comments and Escaping

Jetty operations guide, JSP page

Names org.eclipse.jetty.jsp.JettyJspServlet where 12.1.14 has org.eclipse.jetty.ee11.jsp.JettyJspServlet; its parameter table uses the GlassFish spellings (genStrAsCharArray, scratchDir, mappedFile)

Jasper Configuration and Precompilation

Spring reference, JSP chapter

The sample output of the form tags is out of date for 7.0.9 (<form id="book" ...>, the marker field value, <span id="title.errors">) and it still documents the deprecated mvc: XML namespace; Boot 4.1.1 manages Tomcat 11.0.24 while 11.0.26 is current

Spring MVC with JSP

Struts Documentation Against 7.4.0

The documentation of the Core Developer Guide, the Tag Developer Guide and the plugin pages was largely written for the 2.x line, and the table keeps to what was verified by running 7.4.0.

Documentation Verified on 7.4.0 Page

Many pages (action configuration, localization, interceptors, results, exception handling, file upload, validation, JSON, CDI, plugins architecture, Security log example)

Still name com.opensymphony.xwork2.* (ActionSupport, Action, Validateable, ObjectFactory, Result, ValidatorSupport, the logger category) or javax.validation and javax.servlet.context.tempdir; the 7.4.0 jar has only org.apache.struts2.* and jakarta.*

Packages, Aware Interfaces and Actions, Actions

Architecture "big picture" and nutshell

Describe FilterDispatcher and ActionContextCleanUp (neither class exists), Servlet API 2.4, JSP API 2.0 and Java 7, and JavaServer Faces components; ActionProxyFactory.setFactory does not exist

Architecture and Request Lifecycle

web-xml page, the 2.0 comparison wiki page, birdseye

Still shows the Java EE 3.1 xmlns.jcp.org descriptor; the comparison table was last updated in 2007; birdseye lists AngularJS and does not mention 6.x and 7.x

Introduction and History, Architecture and Request Lifecycle

Interceptors table

Lists timer, fileUpload, parameterFilter and annotationParameterFilter, none of which is registered in 7.4.0 (<interceptor-ref name="timer"/> makes the application fail at startup), and omits httpMethod; the parameters page spells excludedValuePatterns and excludeValuePatterns and overriding params.excludeParams replaces the built-in exclusions

Interceptors, Parameter Injection

Constants

struts.xwork.chaining.* is struts.chaining.*; the LRU-mode OGNL cache constants are struts.ognl.expressionCacheType and beanInfoCacheType; struts.configuration.files does not exist (the config filter init-param does the job); the Spring class-reloading constants carry the struts.objectFactory.spring. prefix; struts.convention.classLoader.excludeParent is struts.convention.exclude.parentClassLoader; struts.conversion.properties.processor is struts.converter.properties.processor; struts.ognl.allowStaticMethodAccess does not exist

Configuration, Action Chaining, Spring Integration

Configuration load order and static content

default.properties is registered first, not fourth; an application default.properties is never read (struts.properties is); static content is served under /static, not /struts/; with configuration reload on, the first reload leaves /static/* answering 500

Configuration, Deployment and Production

Wildcard mappings

The guide says the last matching pattern wins; the source and a run show the first declared match wins; a regex with braces breaks the matcher; an action class chosen only through {1} is not allowlisted; strict method invocation is "2.3" in one page and "2.5" in another

Wildcard Mappings and Method Invocation

Packages and namespaces

An action not found in a namespace does not fall back to the default namespace in 7.4.0 (only / falls back); a parent package declared later in the file works; abstract-package actions are inherited

Packages and Namespaces

Exception handling

Mappings are searched "first local and then global"; the closest class wins across both lists; an unmapped result is HTTP 404, not 500; with struts.devMode=true an unmapped exception answers 200 with the Problem Report

Exception Handling and Global Results

Value stack, OGNL and tags

OGNL method calls on JDK collections fail silently under the allowlist while the property form works; ${x} in a Struts tag attribute fails at translation; the disallowed-property-names list (parameters, session) does not stop an action getter of that name; the tag reference lists 26 generic tags and the TLD has 27 (number); text('key') in s:i18n prints nothing outside s:text

Value Stack, ActionContext and OGNL, Generic Tags

Java records in OGNL (corrected)

Records are supported, with one limit: qualified paths (isbn.code, top.title, #b.title) and accessor calls (title()) read record components, subject to the OGNL allowlist, while a bare name such as title inside s:iterator stays empty because the stack search looks only for JavaBean getters. The earlier note that records are unsupported is withdrawn

Java Records in OGNL

Themes and UI tags

An ajax theme is still listed and fails with TemplateNotFoundException (HTTP 500); the html5 theme’s "validation and error reporting" renders neither labels nor field errors; the checkbox hidden field is written only with submitUnchecked; the tabIndex attribute is tabindex; lower-case labelposition is written onto the element as an unknown attribute; escapeHtmlTag is escapeHtmlBody

Themes and Templates, UI and Form Tags

FreeMarker and Velocity

attributes.myParameter for a request parameter is Parameters.myParameter; req, res and ognl do not resolve; struts.freemarker.templatesCache.updateDelay defaults are FreeMarker’s own 5 seconds; JspTaglibs fails with NoClassDefFoundError: jakarta/servlet/jsp/el/VariableResolver; a Velocity toolbox location drops $stack and $action

FreeMarker and Velocity Views

Tiles plugin

The listener is StrutsTilesListener (there is no org.apache.tiles.listener.TilesListener); wildcard definition names need the WILDCARD: prefix; ${version.tiles} is not defined; commons-beanutils 1.11.0 must be added or startup fails with NoClassDefFoundError

Tiles and Page Layouts

Validation and Bean Validation

The validation page omits the long and creditcard validators and four annotations; creditcard accepts a wrong checksum; regexp is regex; @Validation does not exist; the Bean Validation example imports javax.validation and the old @Email; a provider is mandatory (without one every URL answers 404); the second sample stack validates before params

Validation, Bean Validation Plugin

Localization and conversion

The i18n interceptor reacts to request_locale only for session storage; the key-hierarchy search peels the first segment only; a {0} in invalid.fieldvalue.* prints literally; a date pattern in a bundle goes through MessageFormat; blank BigDecimal becomes null and not an error; en-US 1,5 becomes 15.0

Localization and I18n, Type Conversion

Execute and Wait, tokens, chaining

executeAndWaitStack does not work for an action that needs parameters or a session; the background thread is <action>_background-process; the token is stored per session and a second s:token of the same name replaces the first; chain.excludes replaces the built-in exclusions; the skipActions setter splits on commas

Double-Submit Tokens and Wait Pages, Action Chaining

File upload and results

@StrutsParameter examples for File setters do nothing without the removed interceptor; the "mx" typo in a message; Retry-After on a 200 static response; the postback result URL-encodes hidden values; headers.X-Book-Count sets no header while headers['X-Book-Count'] does

File Upload and Download, Results and Result Types

REST plugin

The documented setup misses jackson-dataformat-xml; automatic error handling is not JSON (an HTML 500, or the serialised Throwable with status 200); the Accept header ignores quality values; If-Modified-Since is inverted in DefaultHttpHeaders; _method=DELETE on a GET deletes

REST Plugin

JSON, Convention, CDI, Spring and other plugins

The CDI jndiKey constant has no effect; only thread of the documented bean scopes works; fetchMetadata.exemptedPaths compares only the context path; the Async plugin does not complete a request on Tomcat 11; the matchingPattern parameter of the XSLT result is ignored; JasperReports 7 needs jackson-bom 2.22.2

CDI, Other Plugins, JSON Plugin

Security headers, debugging and testing

Fetch Metadata allows a cross-site request when the mode is navigate or the method is GET, and a rejection is the result 403 that gives HTTP 404 unless mapped; ?debug=xml is broken on 7.4.0; devMode -Dstruts.devMode=true is ignored; StrutsTestCase is JUnit 3, the JUnit 4 base is StrutsJUnit4TestCase; StrutsTestCaseHelper disables the allowlist

Security Headers, Debugging, Testing

Security guide

The OGNL sandbox is said not to work with JDK 21 and later (a security manager still installs on 21.0.6 with warnings and JDK 25 refuses the flag); the ambiguous-action-methods section (WW-4405) is historical, the guide’s example runs on 7.4.0; struts.disallowProxyMemberAccess and struts.ognl.enableEvalExpression are undocumented there

Security Hardening

Struts 1 documentation

The Struts 1 documentation is no longer online (/1.x/ and the FAQ answer 404); the press release page is struts1eol-press; the "four non-exclusive paths" of Migration Strategies list three; the "Migration Tools" page describes tools that were never built

Struts 1 (Classic) and Migration

Two of these rows describe behaviour of released code and not of documentation, and an upgrade should look at them before the plugin is trusted: the REST plugin’s inverted conditional GET (DefaultHttpHeaders.compareIfModifiedSince answered 304 for a client copy older than the resource) and the Async plugin that does not complete a request (see Other Plugins). Both are reported on their plugin pages with the request that shows them.

Pitfalls

Pitfall Symptom Remedy

A javax WAR dropped into Tomcat 10.1 or 11

A filter or listener in web.xml stops the context (NoClassDefFoundError: javax/servlet/Filter, every URL 404); a servlet registered only by annotation deploys silently and answers 404

Convert at the source, with the migration tool ahead of time, or through webapps-javaee; read the log, not the HTTP status (What Happens Without Migration)

Trusting the converted WAR to be modern

The descriptors still name Java EE 8 (xmlns.jcp.org, version="4.0", the 2.1 TLD schema) and the JSTL URIs are unchanged

Plan the source-level migration anyway; the next javax import mixes two namespaces in one application (Conversion Ahead of Time: the Migration Tool)

Converting only the application’s own jars

The tool also converts the jars bundled in WEB-INF/lib, but not libraries installed in Tomcat’s lib directory or other shared locations

Keep libraries in the WAR, or convert the shared jars with the same tool before installing them

JasperException for an old page on the first request

isThreadSafe or jsp:plugin fails at translation, not at startup

Precompile with JspC in the build to find every old page at once (Jasper Configuration and Precompilation)

${bean.class.name} or ${exception.class.name}

Failed to parse the expression

${bean.getClass().getName()}

Expecting EL to work under an old descriptor

${...} printed literally in every page of a WAR whose web.xml is a Servlet 2.3 DTD

Move the descriptor to the schema of the version in use, or set el-ignored to false in a property group

Old JSTL URIs left in place

Nothing fails today (the 3.0.1 jar still resolves them), so they survive into the next library change; xmlns.jcp.org URIs fail immediately

Replace with jakarta.tags.* while the diff is small

fileUpload or timer interceptor references

The whole application fails to start; every URL of the context is 404 and the only trace is Unable to find interceptor class referenced by ref-name in the log

Replace fileUpload with actionFileUpload and drop timer; test the startup, not only a request

Unannotated setters after the move to 7

Parameters silently not bound, no error on the page

Add @StrutsParameter after reviewing each setter; use the transition mode of 7.4.0 while you do it

Plain POJOs on a page after the move to 7

<s:property> prints nothing and the log has a warning about a class that is "not allowlisted"

Add the model package to struts.allowlist.packageNames; do not turn the allowlist off

Old constant names

Silently ignored (struts.xwork.chaining.copyMessages)

Use the 6.0 names from the constants table

action!method links and buttons

404 There is no Action mapped for namespace [/] and action name [books!lend]

Map the method as its own action, or use wildcard method selection with <allowed-methods> placed after the results

A new servlet next to Struts at an extensionless URL

Struts answers 404 for the URL before the servlet is reached

struts.action.extension set to action, or struts.action.excludePattern; update the legacy links

Copying a class from the official Struts documentation

package com.opensymphony.xwork2 does not exist, or a javax.validation import that does not resolve

Take the org.apache.struts2 and jakarta names of Packages, Aware Interfaces and Actions; the documentation page is stale, not the framework

Upgrade Checklist

The checklist is the order that the sections above follow, and each step names what proves it. It assumes the target of this section, Tomcat 11 with Java 21 and Struts 7.4, and takes a legacy javax JSP application with Struts 2.3 as the longest case; a JSP-only application skips the Struts steps.

Before touching the code
  1. Inventory the application: container and Java versions, web.xml version, JSP and JSTL libraries and their URIs, Struts version and plugins, and every third-party jar that still imports javax.*.

  2. Run the tests, and record the answer of ten real URLs with curl; they are the regression suite for everything below.

  3. If the aim is only to run on Tomcat 11 first, convert with the migration tool (webapps-javaee or the command line), run the ten URLs, and stop here for the first release.

Platform and JSP
  1. Change javax.servlet, javax.servlet.jsp and javax.el imports to jakarta.*, the dependencies to jakarta.servlet-api 6.1.0, jakarta.servlet.jsp-api 4.0.0 and jakarta.el-api 6.0.1 (provided), and JSTL to the 3.0 API and the 3.0.1 implementation (Conversion at the Source).

  2. Move web.xml to the 6.1 schema and every TLD to the 4.0 schema; keep version and namespace in step.

  3. Replace the removed Servlet and Pages API (SingleThreadModel, HttpUtils, getValue and friends, EVAL_BODY_TAG, getRootCause, the jakarta.servlet.jsp.el classes) with the replacements of Jakarta Servlet: What Changed and What Was Removed.

  4. Delete isThreadSafe and jsp:plugin; replace .class.name in EL; replace the JSTL URIs with jakarta.tags.*.

  5. Precompile all JSPs with JspC in the build, so translation errors surface at build time.

Struts, in the order of the guides
  1. Move to 2.5.33, then 6.12.0, then 7.4.0, running the ten URLs after each step: filter class and DTD (2.5), tag attributes and the removed s:div (2.5), constants, the Aware interfaces and static method access (6), jakarta, org.apache.struts2, @StrutsParameter, the allowlist, actionFileUpload and the removed plugins (7).

  2. Start the application and read the log from the top: a Struts filter that fails to initialise leaves a running context and a 404 for every URL.

  3. Review every @StrutsParameter you added, every struts.allowlist.* entry, and every place where a DMI link was replaced.

  4. Update the version of every plugin to the one of the core, and set the struts2-bom so that no plugin version is hard-coded.

After
  1. Replace the old descriptor namespaces and URIs that the tool or Tomcat tolerated.

  2. Run the security checks of the section (Security Hardening, Security) and read the security bulletins of the line you are on (Security Bulletins).

  3. Decide what the application is: maintained on JSP and Struts, or on its way to Spring MVC and Thymeleaf (Modernisation Paths), and write the decision next to the version numbers.

The Bookshelf, Migrated

Everything above was done with one scenario, so that the proof sits in one place. The table lists each experiment, the application that carried it, and its outcome; the applications were built with Maven, deployed on Tomcat 11.0.26 with Java 21 and requested with curl, and removed afterwards.

Experiment Application Outcome

javax WAR in webapps

Bookshelf, Servlet 4.0.1, JSTL 1.2

Context failed (NoClassDefFoundError: javax/servlet/Filter), 404

javax WAR in webapps-javaee, default legacyAppBase

the same WAR

Converted and deployed, 200, jakarta.servlet.http.HttpServletRequest printed by the scriptlet

Migration tool 1.0.13 (EE profile) and the bundled migrate.sh (TOMCAT profile)

the same WAR

Equivalent results: classes and the JSTL jar converted, descriptors and URIs unchanged

Source-level migration

Bookshelf, Servlet 6.1, Pages 4.0, JSTL 3.0

200, output as the converted WAR with jakarta in the filter header and the badge

Removed Servlet and Pages API

javac against jakarta.servlet-api 6.1.0 and jakarta.servlet.jsp-api 4.0.0

7 errors, replacements compile

isThreadSafe, jsp:plugin, ${x.class.name}

Bookshelf pages

500 each, with the messages quoted above; getClass().getName() works

JSTL URIs of five generations

Bookshelf pages

The 1.0 EL URI fails for ${...}, the 1.0 RT URI, the 1.2 URI and jakarta.tags.core work, the xmlns.jcp.org URI does not resolve

EL features of five generations

Bookshelf page

Method call, lambdas, literals, +=, assignment, static access, record and array length all printed as expected

web.xml versions 2.3, 2.4, 2.5 and 6.1

four WARs

EL printed literally only under 2.3; the annotated servlet answered 200 under all

Struts filter classes of 2.0 and 2.3

Bookshelf on Struts 7.4.0

ClassNotFoundException, 404

struts-2.3.dtd in struts.xml

Bookshelf on Struts 7.4.0

Starts and serves

Old com.opensymphony.xwork2 and interceptor.SessionAware

javac against 7.4.0

4 errors; the withSession form runs and counts to 3

s:url id, s:set name, s:property escape, s:div

Bookshelf pages

Translation errors as listed; var and escapeHtml work

Unannotated setter, allowlist, static OGNL

Bookshelf action

As in the tables: empty output until annotated or allowlisted; static fields need the constant and the class allowlisted

DMI off and on, wildcard methods

Bookshelf action

As in the table of Dynamic Method Invocation and the Method Prefixes

fileUpload and actionFileUpload

Bookshelf upload action

The old reference stops the application; the new one received dune.txt and rejected the PDF with a 404 for the input result

Extension narrowing for a new servlet

Bookshelf WAR

/api/books 200 and /books 404 with action; the reverse with the default

Not run for this page: the Struts 6 to 7 steps that only change a constant name (the guide’s table was transcribed), the Struts 2.3 era "before" code (no Struts 2.3 or 6 runtime was installed), anything on Jetty or GlassFish, and the javax libraries other than JSTL 1.2.

Where to Go Next

The pages of this section that hold the evidence behind the tables are named in them. For the other direction, an application that is already current and needs hardening, the next pages are Security Hardening and Security; for a first read of the section, Introduction and History and Getting Started.

References