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.
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 |
|
Current focus of development; the runtime of this section |
10.1.x (10.1.60) |
6.0 |
3.1 |
5.0 |
|
Supported |
10.0.x |
5.0 |
3.0 |
4.0 |
|
Superseded, end of life 2022-10-31 |
9.0.x (9.0.122) |
4.0 |
2.3 |
3.0 |
|
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 |
|
End of life |
7.x |
3.0 |
2.2 |
2.2 |
|
End of life |
6.x |
2.5 |
2.1 |
(within JSP 2.1) |
|
End of life |
4.x |
2.3 |
1.2 |
none |
|
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 |
JSTL 1.0 to 1.1 |
2003-11-24 |
The tag library URIs lose the twin |
JSP 2.0 to 2.1, JSTL 1.1 to 1.2 |
2006-05-11 |
|
Servlet 2.5 to 3.0, JSP 2.1 to 2.2 |
2009-12-10 |
Annotations ( |
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: |
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; |
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, |
Source: JSR 53 (Servlet 2.3 and JSP 1.2), JSR 152 (JSP 2.0), JSR 245 (JSP 2.1), JSR 315 (Servlet 3.0), JSR 341 (EL 3.0), Jakarta Servlet 6.1 — Changes Since Jakarta Servlet 4.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.
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.
Source: Jakarta Servlet 6.1 — Compatibility with Jakarta Servlet Specification Version 4.0, Apache Tomcat — Migration Guide 10.0.x, Specification APIs
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.
// 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);
}
}
<!-- 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
Source: Apache Tomcat — Migration Tool for Jakarta EE Software Downloads, Maven Central — org.apache.tomcat:jakartaee-migration 1.0.13
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) |
|---|---|
|
The default: convert the Java EE APIs that Tomcat provides (Servlet, Pages, EL, WebSocket, Authentication) |
|
Convert all Java EE APIs, for a WAR that also uses, say, Persistence or Validation |
|
Convert the Jakarta Servlet API only |
|
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 |
|
Skip files that match; repeatable; matched against the file name unless |
|
Process archives in memory when the default streaming fails on an unusual zip structure |
|
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 |
|
String constants in the JSP |
Yes |
The scriptlet’s |
|
Yes |
29 |
|
No |
Still |
TLD namespace and version |
No |
Still the |
JSTL library URIs in the JSP |
No |
|
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):
<!-- 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.
<!-- 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 ( |
printed literally, |
200 |
Servlet 2.4 schema, |
|
200 |
Servlet 2.5 schema, |
|
200 |
Servlet 6.1 schema, |
|
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, |
Servlet 5.0 to 6.0 |
Java 11 minimum; |
Servlet 6.0 to 6.1 |
|
Source: Jakarta Servlet 6.1 — Changes Since Jakarta Servlet 6.0, Jakarta Servlet 6.1 — Changes Since Jakarta Servlet 5.0
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 |
|---|---|---|
|
Thread-safe servlets; keep state in locals, request attributes or synchronised services |
The |
|
|
|
|
none |
Session enumeration was never portable |
|
|
|
|
|
The spelling changed, not just the case: |
|
|
|
|
Hold your own references; |
|
|
|
|
|
|
Verified by compiling the replacements in the same file |
|
|
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 2.0 (2003, Servlet 2.4) |
The expression language becomes part of JSP; tag files and |
|
JSP 2.1 (2006) |
Deferred |
|
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) |
|
JSP 2.3 (2017) |
The Java EE 8 version, implemented by Tomcat 9 together with Servlet 4.0 and EL 3.0 |
|
Pages 3.0 (2020) |
API moved from |
|
Pages 3.1 (2022) |
|
|
Pages 4.0 (2024, Jakarta EE 11) |
Java 17 minimum; removed: the deprecated |
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 |
|---|---|---|
|
JSTL 1.0, EL library |
Translation error: |
|
JSTL 1.0, RT library |
Works: |
|
JSTL 1.1 and 1.2 |
Works like |
|
Never defined (the Java EE 7 namespace style) |
|
|
Tags 3.0 |
Works: |
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 1.2 |
|
Tags 3.0 |
|
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).
Source: Jakarta Tags 3.1 — release page
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 ( |
EL 3.0 to 4.0 |
The API moved from |
EL 4.0 to 5.0 (Java 11) |
|
EL 5.0 to 6.0 (Java 17) |
Deprecated code removed; the |
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
Source: Jakarta Expression Language 6.0 — Lambda Expressions, Static Field and Method Reference, Construction of Collection Objects
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. -
Optionalis not resolved unless the application registersOptionalELResolver; 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 |
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 |
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 |
|
End of life since 2019-09-12 |
Struts 2.5.x (GA 2016-05-09) |
The 2.3 to 2.5 guide: |
|
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 |
|
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 |
|
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.
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.
Source: Struts — web.xml, Simple Example
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(none; the file upload interceptor bound |
|
|
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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
plain |
|
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|---|---|
|
|
not tested |
the default method |
|
404, |
|
|
|
|
|
the |
|
|
not tested |
wildcard method selection with |
|
404, |
not tested |
the method is not in |
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 |
|---|---|
|
`Filter: ` (empty, status 200, no error on the page) |
|
|
|
|
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.
Source: Struts — StrutsParameter Annotation, Struts Wiki — 6.x.x to 7.x.x migration, Action parameter injection
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 |
|
|
|
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 |
|
|
|
|
|
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, |
Plain |
Codebehind plugin (2.5) |
|
Struts 1 plugin (2.5) |
|
Tiles 2 ( |
|
Velocity in core (until 5.x) |
|
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 |
|
|
Spring 2 XML with |
Spring Framework 7 with |
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 |
7.4.0 |
With |
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.*tojakarta.*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 |
|---|---|---|
|
200 |
404 (links must say |
|
200 |
200 |
|
404 from the Struts filter |
200, |
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) |
|
Tomcat 11.0.x |
Java 17 minimum, no |
Struts 7.0.0 (2024-12-19) |
|
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 |
Struts 7.4.0 and 6.12.0 (2026-10-02) |
Fixes for S2-075 to S2-078; |
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 |
Jakarta EE 11 with Servlet 6.1, Pages 4.0 and EL 6.0 on Java 21 (17 minimum), Tomcat 11, |
|
Scripting (JSP book) |
Scriptlet-centric pages as the norm |
EL, JSTL and tag files; scriptlets switched off with |
|
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, |
Expression Language Basics, Lambdas, Collections and Streams, Static Access, Records and Optional |
JSTL URIs (JSTL book) |
|
One library per area with |
|
Custom tags (JSP and JSTL books) |
Classic |
|
Custom Tags: SimpleTag, Tag Files and Layouts, TLDs and EL Functions |
Removed JSP features |
|
Both deprecated in Pages 3.1 and removed in 4.0 (verified: |
|
Removed Servlet API |
|
Removed in Servlet 6.0; use |
|
|
The Servlet 2.3 DTD, |
The Servlet 6.1 schema ( |
|
Data access in views (JSP and JSTL books) |
JDBC in JSPs, |
Data access in services and repositories (see the Hibernate pages of the repository); |
|
Client formats (JSP book) |
WML and WAP, VoiceXML, |
HTML5, JSON, CSV or XLSX with |
|
Authentication (JSP and JSTL books) |
A hand-made login with passwords in beans, session or tables |
Container security ( |
|
Struts 1 (Struts 1 book) |
Struts 1.0 and 1.1 beta |
End of life since 2013 with unfixed vulnerabilities (CVE-2014-0114); |
|
Struts 2 filter (Struts 2 book) |
|
|
|
Struts 2 packages (Struts 2 book) |
|
|
|
Annotations (Struts 2 book) |
Zero-configuration |
The Convention plugin with |
|
Ajax (Struts 2 book) |
The Dojo plugin, the |
Dojo removed in 2.5; the JSON plugin, the REST plugin, plain |
|
DMI (Struts 2 book) |
|
|
|
OGNL (Struts 2 book) |
Free |
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) |
|
Parameter binding (Struts 2 book) |
Any public setter bound from request parameters |
|
|
File upload (Struts 2 book) |
The |
|
|
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) |
|
Tiles (Struts 1 and Struts 2 books) |
Struts Tiles 1, Apache Tiles 2 |
Apache Tiles is retired (Attic, 2019); its code is bundled in |
|
Velocity (Struts 1 book) |
VelocityStruts and VelocityTools |
|
|
|
Constants in |
|
|
Spring (Struts 2 book) |
Spring 2 XML and |
Spring Framework 7 with |
Source: Struts Spring Plugin — Migration Note: autowire alwaysRespect default change (7.2.0), Struts — Plugins
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 ( |
The samples do not compile or translate if copied; the actions are spelled |
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 |
|
The constant’s value is lower-case |
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 |
|---|---|---|
|
256; the 6 to 7 migration guide says "reduced from 200 to 150" and shows |
|
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 DTDs |
|
|
Maven archetypes |
|
|
|
Lists |
|
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 |
|
|
Still on |
|
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 |
|
GlassFish |
The download page still advertises 8.0.4 while Maven Central has 8.0.5; GlassFish 8.0.5 answers |
|
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 |
|
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 |
Configuration and ELProcessor, Static Access, Records and Optional |
Test tooling |
The issue says JUnit 5; the verified baseline is |
|
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 |
Specifications Against Their Implementations
| Specification text | Observed on Tomcat 11.0.26 | Page |
|---|---|---|
Pages 4.0 5.5: |
A 200 with the already committed text; the server log has |
|
Pages 4.0 1.10.5: the content of |
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) |
|
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; |
|
Pages 4.0 and Servlet 6.1: the default character encoding is ISO-8859-1 |
|
|
Pages 4.0 1.4.3: a self-referencing error page is a translation error |
It compiled and ended in a |
|
Pages 4.0 Table JSP.1-7: |
|
|
Pages 4.0 RTAttributeScriptingEnabled: a request-time attribute value is a translation error when scripting is disabled |
Accepted on Tomcat 11 with |
|
Pages 4.0 6.2.2 and 1.6: quoting of |
|
|
Pages 4.0 JspPage Javadoc: a page "should redefine the |
|
|
Pages 4.0 8.4.3 and 8.5.2: a |
Accepted silently, or a |
|
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 |
|
Pages 4.0 appendix A and 7.3: |
Stale examples; the current schemas are |
|
Servlet 6.1 Table 10-1: seven error request attributes |
The eighth, |
|
Servlet 6.1 5.5: |
|
|
Servlet 6.1: |
Returned |
|
EL 6.0 spec: the typos in the lambda and stream examples ( |
The API class is |
Lambdas, Collections and Streams, Expression Language Basics |
EL 6.0 spec 1.25.5 and 1.9.2: boolean coercion and equality |
|
|
Pages 4.0 1.24 and 2.10: imports and the resolver chain |
An explicit import of an interface such as |
|
|
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 |
|
Pages 4.0 JSP documents 5.12 and 6.4 |
|
JSTL Documentation Against the Implementation
| Documentation | Observed with JSTL 3.0.1 | Page |
|---|---|---|
|
Omitting them fails with |
|
|
|
|
|
|
|
|
Every |
|
|
Still works on 3.0.1; an empty |
|
|
With no |
|
Tags 3.0 9.11.1: |
The constant is |
|
Tags 3.0 chapter 14 and 15 examples |
Not well formed ( |
|
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 |
|
Tags 3.0: only |
The 3.0.1 implementation jar still ships the JSTL 1.0 and 1.2 descriptors, so the |
|
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 ( |
Tomcat Documentation Against the Container
| Documentation | Observed on 11.0.26 | Page |
|---|---|---|
Jasper How-To: |
The stock |
Jasper Configuration and Precompilation, Testing and Debugging |
Jasper How-To: |
The Ant task needs |
Jasper Configuration and Precompilation, Custom Tags: Classic |
Error Report Valve documentation |
Declared in a |
|
Manager documentation and tutorials |
Session persistence across restarts is off unless configured ( |
|
Messages of the JSP translator |
Several errors show a raw resource key or an unsubstituted |
|
Jetty operations guide, JSP page |
Names |
|
Spring reference, JSP chapter |
The sample output of the form tags is out of date for 7.0.9 ( |
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 |
|
Architecture "big picture" and |
Describe |
|
|
Still shows the Java EE 3.1 |
Introduction and History, Architecture and Request Lifecycle |
Interceptors table |
Lists |
|
Constants |
|
|
Configuration load order and static content |
|
|
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 |
|
Packages and namespaces |
An action not found in a namespace does not fall back to the default namespace in 7.4.0 (only |
|
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 |
|
Value stack, OGNL and tags |
OGNL method calls on JDK collections fail silently under the allowlist while the property form works; |
|
Java records in OGNL (corrected) |
Records are supported, with one limit: qualified paths ( |
|
Themes and UI tags |
An |
|
FreeMarker and Velocity |
|
|
Tiles plugin |
The listener is |
|
Validation and Bean Validation |
The validation page omits the |
|
Localization and conversion |
The i18n interceptor reacts to |
|
Execute and Wait, tokens, chaining |
|
|
File upload and results |
|
|
REST plugin |
The documented setup misses |
|
JSON, Convention, CDI, Spring and other plugins |
The CDI |
|
Security headers, debugging and testing |
Fetch Metadata allows a cross-site request when the mode is |
|
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 1 documentation |
The Struts 1 documentation is no longer online ( |
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 |
A filter or listener in |
Convert at the source, with the migration tool ahead of time, or through |
Trusting the converted WAR to be modern |
The descriptors still name Java EE 8 ( |
Plan the source-level migration anyway; the next |
Converting only the application’s own jars |
The tool also converts the jars bundled in |
Keep libraries in the WAR, or convert the shared jars with the same tool before installing them |
|
|
Precompile with JspC in the build to find every old page at once (Jasper Configuration and Precompilation) |
|
|
|
Expecting EL to work under an old descriptor |
|
Move the descriptor to the schema of the version in use, or set |
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; |
Replace with |
|
The whole application fails to start; every URL of the context is 404 and the only trace is |
Replace |
Unannotated setters after the move to 7 |
Parameters silently not bound, no error on the page |
Add |
Plain POJOs on a page after the move to 7 |
|
Add the model package to |
Old constant names |
Silently ignored ( |
Use the 6.0 names from the constants table |
|
404 |
Map the method as its own action, or use wildcard method selection with |
A new servlet next to Struts at an extensionless URL |
Struts answers 404 for the URL before the servlet is reached |
|
Copying a class from the official Struts documentation |
|
Take the |
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.
-
Inventory the application: container and Java versions,
web.xmlversion, JSP and JSTL libraries and their URIs, Struts version and plugins, and every third-party jar that still importsjavax.*. -
Run the tests, and record the answer of ten real URLs with
curl; they are the regression suite for everything below. -
If the aim is only to run on Tomcat 11 first, convert with the migration tool (
webapps-javaeeor the command line), run the ten URLs, and stop here for the first release.
-
Change
javax.servlet,javax.servlet.jspandjavax.elimports tojakarta.*, the dependencies tojakarta.servlet-api6.1.0,jakarta.servlet.jsp-api4.0.0 andjakarta.el-api6.0.1 (provided), and JSTL to the 3.0 API and the 3.0.1 implementation (Conversion at the Source). -
Move
web.xmlto the 6.1 schema and every TLD to the 4.0 schema; keepversionand namespace in step. -
Replace the removed Servlet and Pages API (
SingleThreadModel,HttpUtils,getValueand friends,EVAL_BODY_TAG,getRootCause, thejakarta.servlet.jsp.elclasses) with the replacements of Jakarta Servlet: What Changed and What Was Removed. -
Delete
isThreadSafeandjsp:plugin; replace.class.namein EL; replace the JSTL URIs withjakarta.tags.*. -
Precompile all JSPs with JspC in the build, so translation errors surface at build time.
-
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,actionFileUploadand the removed plugins (7). -
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.
-
Review every
@StrutsParameteryou added, everystruts.allowlist.*entry, and every place where a DMI link was replaced. -
Update the version of every plugin to the one of the core, and set the
struts2-bomso that no plugin version is hard-coded.
-
Replace the old descriptor namespaces and URIs that the tool or Tomcat tolerated.
-
Run the security checks of the section (Security Hardening, Security) and read the security bulletins of the line you are on (Security Bulletins).
-
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 |
|---|---|---|
|
Bookshelf, Servlet 4.0.1, JSTL 1.2 |
Context failed ( |
|
the same WAR |
Converted and deployed, 200, |
Migration tool 1.0.13 ( |
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 |
Removed Servlet and Pages API |
|
7 errors, replacements compile |
|
Bookshelf pages |
500 each, with the messages quoted above; |
JSTL URIs of five generations |
Bookshelf pages |
The 1.0 EL URI fails for |
EL features of five generations |
Bookshelf page |
Method call, lambdas, literals, |
|
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 |
|
|
Bookshelf on Struts 7.4.0 |
Starts and serves |
Old |
|
4 errors; the |
|
Bookshelf pages |
Translation errors as listed; |
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 |
|
Bookshelf upload action |
The old reference stops the application; the new one received |
Extension narrowing for a new servlet |
Bookshelf WAR |
|
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.