Getting Started
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Jakarta Pages 4.0, Jakarta Expression Language 6.0, Jakarta Standard Tag Library 3.0 (API 3.0.2,
implementation 3.0.1) and Jakarta Servlet 6.1, running on Java 21 and Apache Tomcat 11.0.26 (the latest 11.0.x
release on 2026-10-08, when every fact and example below was checked). Eclipse Jetty 12.1.14 and Eclipse GlassFish 8.0.5
are covered as alternatives. All examples were built with Maven and exercised with curl against a scratch
Tomcat 11.0.26 on Java 21; the Jetty, Cargo, GlassFish and Docker variants were run too, and each snippet is followed by
the official page it derives from. The page takes the reader from an empty folder to the first working page of the
Bookshelf — a catalogue of books, authors, reviews and loans that this section builds twice, as bookshelf-jsp in
Part I and as bookshelf-struts in Part II — and explains each moving part on the way: the JDK, the container, the
WAR, the first servlet, the first JSP with JSTL, the ways to deploy it, and a Docker image.
What You Need
A JSP application is a web application archive (WAR) that a servlet container unpacks and runs. The container supplies the Servlet, Pages and Expression Language APIs and the JSP engine; the application brings its own classes, its views and, with Tomcat, the JSTL jars. Four pieces are therefore needed.
| Piece | Version used here | Role |
|---|---|---|
JDK |
Java 21 (an LTS release; Tomcat 11 and Pages 4.0 need 17 or later) |
Compiles the servlets, runs the container, and lets Jasper compile each JSP into a class (Tomcat bundles the Eclipse JDT compiler for that). |
Servlet container |
Apache Tomcat 11.0.26 (Servlet 6.1, Pages 4.0, EL 6.0, WebSocket 2.2) |
Receives HTTP requests, maps them to servlets and JSPs, manages sessions and translates JSPs into servlets. |
Build tool |
Apache Maven 3.9 or later, with |
Compiles the classes, assembles |
JSTL implementation |
|
The tag library ( |
Tomcat’s own documentation states the support matrix: Tomcat 11.0.x implements Servlet 6.1, JSP 4.0, EL 6.0, WebSocket 2.2
and Authentication 3.1 (the versions required by the Jakarta EE 11 platform) and runs on Java 17 and later, while
Tomcat 10.1.x implements Servlet 6.0 and JSP 3.1 on Java 11 and later, and Tomcat 9.0.x still implements the
javax.* Servlet 4.0 and JSP 2.3 on Java 8 and later.
Choosing a Container
Any Jakarta EE 11 servlet container runs the examples of this section. The three that matter in practice differ in what they are and how a JSP is supported.
| Apache Tomcat 11.0.26 | Eclipse Jetty 12.1.14 | Eclipse GlassFish 8.0.5 | |
|---|---|---|---|
What it is |
A servlet container: Servlet, Pages, EL, WebSocket and Authentication, nothing else |
A servlet container and HTTP server, embeddable, with one "environment" per Jakarta EE release ( |
A full application server; the Platform and Web Profile distributions are Jakarta EE 11 products |
JSP engine |
Jasper (Apache), built in |
Apache Jasper, packaged with the |
Eclipse WaSP for Pages, Expressly for EL |
JSTL |
Not bundled: add the API and the implementation to |
The WAR’s own JSTL jars were used (verified with the Maven plugin; the Jetty documentation also lists a JSTL module) |
Provided by the server; a WAR that carries its own copy also ran (verified) |
Deploy by |
Copy to |
Maven plugin |
|
Use it for |
The reference for this section; the most common production container for JSP |
Fast edit-run loops from Maven and tests that start a server in the JVM |
Applications that also need the wider Jakarta EE platform (CDI, Persistence, Messaging) |
Tomcat is the one the examples assume. Jetty and GlassFish are shown further down with the same WAR; a portability difference between them (records in EL) is listed in the pitfalls table. Issue #116 (IDE links) is referenced here in prose only: every IDE with Jakarta EE tooling can start Tomcat 11, Jetty 12 or GlassFish 8 from a run configuration and deploy the exploded project into it, so nothing in this section depends on a particular IDE; use one only if it can run a server whose Servlet level is 6.1.
Installing the JDK
Install a JDK (not only a JRE) of version 21: the servlet compiler, Maven and Tomcat’s startup scripts all use it.
Distributions such as Eclipse Temurin (https://adoptium.net/) and the OpenJDK builds listed on
https://openjdk.org/projects/jdk/21/ are interchangeable for this purpose. Tomcat’s RUNNING.txt states that Tomcat 11.0
requires a Java SE Runtime Environment version 17 or later and that a full JDK may be used instead of a JRE. Point
JAVA_HOME at the JDK and check both the compiler and the runtime.
export JAVA_HOME=/path/to/jdk-21
export PATH="$JAVA_HOME/bin:$PATH"
java -version # openjdk version "21.x.y"
javac -version # javac 21.x.y
mvn -version # Apache Maven 3.9.x ... Java version: 21.x.y
Maven prints the Java version it runs with, which is the quickest way to find a JAVA_HOME that points at the wrong
JDK. Installing Maven itself is covered by the Maven project (https://maven.apache.org/install.html); the Java
language and its tools are covered elsewhere on this site (Java Getting Started
and Build & Tooling).
Source: Apache Maven — Installing Maven
Installing Tomcat 11
Tomcat is a directory, not an installer: download the "Core" archive from the Tomcat 11 download page, verify it, unpack
it and run its scripts. The download page publishes OpenPGP signatures and SHA-512 checksums for every file and asks users
to check the integrity of what they download. The checksum file has the format hash *filename, which shasum reads
directly.
curl -O https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.26/bin/apache-tomcat-11.0.26.tar.gz
curl -O https://downloads.apache.org/tomcat/tomcat-11/v11.0.26/bin/apache-tomcat-11.0.26.tar.gz.sha512
shasum -a 512 -c apache-tomcat-11.0.26.tar.gz.sha512 # apache-tomcat-11.0.26.tar.gz: OK
tar xzf apache-tomcat-11.0.26.tar.gz
export CATALINA_HOME="$PWD/apache-tomcat-11.0.26"
"$CATALINA_HOME/bin/version.sh"
Server version: Apache Tomcat/11.0.26
Server built: Sep 9 2026 21:36:42 UTC
Server number: 11.0.26.0
...
JVM Version: 21.0.x+y-LTS
(downloads.apache.org serves the checksum files of current releases and archive.apache.org keeps every older
release; the download page links both. On Windows use the "Windows zip" distribution.)
What Is in the Directory
The unpacked archive is the Tomcat home. The documentation distinguishes CATALINA_HOME, the installation with the
jars and scripts, from CATALINA_BASE, the configuration, logs and applications of one running instance. By default both
point to the same directory, which is the right choice until a second instance is needed.
| Directory | Content (Tomcat 11.0.26, as unpacked) |
|---|---|
|
|
|
|
|
The container’s classes, shared by every web application: |
|
|
|
The default |
|
Generated JSP sources and classes ( |
The fact that lib/ has the Servlet, Pages and EL APIs and no JSTL jar explains the Maven scopes used later: the three
APIs are provided (the container already has them) and the JSTL API and implementation are packaged into the WAR.
The default applications are a security consideration, not only a convenience. Tomcat’s security guide says that the
applications that ship enabled have had vulnerabilities in the past, that unnecessary ones should be removed, that the
examples application should always be removed from a security-sensitive installation, and that the Manager, which
allows remote deployment, is frequently targeted. A development machine can keep them; a production image should not
(the Docker section below starts from an empty webapps).
Starting and Stopping
startup.sh runs catalina.sh start in the background, shutdown.sh stops the instance through the shutdown port
declared on the Server element of server.xml, and catalina.sh run keeps the server in the foreground with the log on
the console, which is the mode to use in a terminal and in a container. The HTTP connector listens on port 8080 and the
shutdown port is 8005 by default; the verification runs for this page used other ports because several servers ran side
by side, which changes nothing but the port number in the URLs.
"$CATALINA_HOME/bin/startup.sh"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/ # 200: the ROOT application
tail -f "$CATALINA_HOME/logs/catalina.$(date +%Y-%m-%d).log"
"$CATALINA_HOME/bin/shutdown.sh"
If the page of the ROOT application opens, the container, the JDK and the port are fine. The examples application
that ships in webapps/ has runnable JSP and servlet samples (its JSP index answered 200 at /examples/jsp/ in the
check); it is useful for a first look and, as said above, the first thing to remove from anything that faces a
network. The remaining problems come from
the application: its WAR, its descriptor or its classes.
Jetty 12.1 and GlassFish 8 as Alternatives
Both alternatives run the same WAR that the rest of this page builds, so the instructions are short. They are written after the WAR exists; the full runs are in the Maven plugins and GlassFish sections below. The differences to know in advance:
-
Jetty 12 is organised in environments, one per Jakarta EE release, and the Maven plugin is repackaged for each one with an
eeXin the groupId and the artifactId. For Jakarta EE 11 the plugin isorg.eclipse.jetty.ee11:jetty-ee11-maven-plugin(12.1.14 on 2026-10-08), and the namespace of the application must bejakarta.*. -
GlassFish 8 is the Jakarta EE 11 line. Its download page offers the Platform and the Web Profile distributions (Maven Central has
org.glassfish.main.distributions:glassfishandorg.glassfish.main.distributions:web8.0.5; the download page itself still advertised 8.0.4 on 2026-10-08). The Web Profile distribution is enough for JSP and servlets. -
Neither product removes the need for
jakarta.*imports: ajavax.servletapplication does not run unchanged on any of the three (see the pitfalls table).
A Maven WAR From Scratch
The smallest useful project has a pom.xml, a folder of Java classes, and a folder of web content. Everything under
src/main/webapp becomes the root of the WAR, and WEB-INF inside it is the private part: nothing in it is served
directly to a browser. Create the layout first, then fill the files in.
bookshelf-jsp/
├── pom.xml
└── src/main
├── java/org/example/bookshelf
│ ├── book/ Book.java, BookService.java
│ └── web/ HelloServlet.java, CatalogueServlet.java, VisitServlet.java,
│ Greeting.java, StartupListener.java, PoweredByFilter.java
└── webapp
├── index.html welcome page
├── first-steps.jsp a page that may be requested directly
├── scriptlet.jsp a legacy-style page, for reading old code
├── catalogue.bks static file with a custom MIME type
└── WEB-INF
├── web.xml deployment descriptor (Servlet 6.1)
├── jsp/ catalogue.jsp, visits.jsp, about.jsp, not-found.jsp, banner.jspf
└── tags/ rating.tag
The folder names follow the usual Maven and Servlet conventions: src/main/java holds the classes, src/main/webapp the
web content, WEB-INF/jsp the views that only a servlet may forward to, and WEB-INF/tags the tag files that
Jasper discovers automatically when a page declares tagdir="/WEB-INF/tags". The later pages of this section add
author, review and loan packages beside book, a tlds folder and a messages resource bundle.
The pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>bookshelf-jsp</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Supplied by the container: compile against them, never package them -->
<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.el</groupId>
<artifactId>jakarta.el-api</artifactId>
<version>6.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Not supplied by Tomcat: the JSTL API and its implementation travel inside the WAR -->
<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>
</dependencies>
<build>
<finalName>bookshelf</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
</plugins>
</build>
</project>
Each line of the file earns its place.
| Element | Why it is there |
|---|---|
|
Selects the WAR lifecycle: |
|
Compiles for Java 21 and checks against the Java 21 API. Class files built for a newer release than the JDK that runs Tomcat fail at load time (see the pitfalls table). |
|
The Servlet API to compile against. |
|
The Pages API, needed to compile tag handlers and anything that uses |
|
The EL API, needed to compile EL functions, resolvers and custom tags. Tomcat has it in |
|
The JSTL API and the implementation that contains the TLDs and tag handlers. They must be in |
|
The WAR is |
|
Pinned so that builds are reproducible. Since 3.1.0 the plugin no longer requires a |
The jars with provided scope are visible to the compiler and to the tests but are left out of the WAR, so the only
jars in
WEB-INF/lib are the two JSTL ones.
Source: Apache Maven — Dependency Scope
About the Maven Archetype
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp produces a WAR project in one command, and it is
tempting. In the check made for this page (archetype version 1.5, the latest) it produced a pom.xml with
maven.compiler.source and target set to 8, and a web.xml for Servlet 4.0 in the old xmlns.jcp.org/xml/ns/javaee
namespace, plus an index.jsp file. Generate it if you like, then replace the descriptor with the one below and the
compiler setting with release 21; or start from the files on this page. There is no archetype for Jakarta EE 11 web
applications to rely on, so this page builds the project by hand.
Source: Maven Archetype Webapp
The Deployment Descriptor
A Servlet 6.1 container can run a WAR with no web.xml at all, because servlets, filters and listeners can be declared
with annotations. A descriptor is still the place for the settings that belong to the deployment rather than to the code,
and Tomcat reads it for session, error-page and MIME settings. This is the one the Bookshelf uses; it exercises each
element that a getting-started application typically needs.
<?xml version="1.0" encoding="UTF-8"?>
<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">
<display-name>Bookshelf (JSP)</display-name>
<context-param>
<param-name>bookshelf.pageSize</param-name>
<param-value>20</param-value>
</context-param>
<filter>
<filter-name>poweredBy</filter-name>
<filter-class>org.example.bookshelf.web.PoweredByFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>poweredBy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.example.bookshelf.web.StartupListener</listener-class>
</listener>
<!-- A JSP registered as a servlet, so it gets a clean URL without ".jsp" -->
<servlet>
<servlet-name>about</servlet-name>
<jsp-file>/WEB-INF/jsp/about.jsp</jsp-file>
</servlet>
<servlet-mapping>
<servlet-name>about</servlet-name>
<url-pattern>/about</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
</cookie-config>
</session-config>
<mime-mapping>
<extension>bks</extension>
<mime-type>application/vnd.bookshelf+json</mime-type>
</mime-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/jsp/not-found.jsp</location>
</error-page>
</web-app>
| Element | What it does | Verified behaviour |
|---|---|---|
|
Declares the descriptor schema. The namespace changed from |
The WAR deployed and started on Tomcat 11.0.26. |
|
An application-wide initialisation parameter, read with |
|
|
Declares a filter class and the URL patterns it applies to; the filter sees every request before the servlet. Here |
|
|
Registers a |
|
|
Registers a JSP as a named servlet and maps it to a URL pattern, so the page can be reached at |
|
|
The session timeout in minutes and the session cookie attributes ( |
The |
|
Maps a file extension to a content type for static files. |
|
|
The files tried, in order, for a request that ends in a directory name. |
|
|
Maps an HTTP status code or an exception type to a page inside the WAR. |
|
The PoweredByFilter class is a dozen lines: it implements jakarta.servlet.Filter, calls
((HttpServletResponse) response).setHeader("X-Bookshelf", "getting-started") and then chain.doFilter(request,
response).
Filter ordering, wrappers and the other listener types, <jsp-config> property groups, <taglib> mappings and the
rest of
the descriptor belong to the Servlet Fundamentals page and to Part I; the point here is that an application with one
web.xml and a handful of
classes is already a complete deployable unit.
The First Servlet
A servlet is a Java class that extends HttpServlet and turns a request into a response. With the @WebServlet
annotation
no registration is needed; the container finds the class while scanning the WAR. The first servlet answers with plain text
and shows what the container reports about itself, which makes it a good smoke test for any new environment.
package org.example.bookshelf.web;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
try (PrintWriter out = resp.getWriter()) {
out.println("Hello from Bookshelf on " + getServletContext().getServerInfo());
out.println("Servlet API " + getServletContext().getMajorVersion() + "."
+ getServletContext().getMinorVersion());
out.println("Context path " + req.getContextPath());
}
}
}
Build it, copy the WAR to webapps/ and request the URL. The three parts of the path are the container’s context path
(/bookshelf, from the WAR’s file name), the servlet’s URL pattern (/hello) and nothing else, because the servlet has
no extra path.
mvn -q package
cp target/bookshelf.war "$CATALINA_HOME/webapps/"
curl http://localhost:8080/bookshelf/hello
Hello from Bookshelf on Apache Tomcat/11.0.26
Servlet API 6.1
Context path /bookshelf
getServerInfo() names the container, getMajorVersion() and getMinorVersion() report the Servlet API level the
container
implements, and getContextPath() returns the path under which the application is mounted. The same WAR printed
jetty/12.1.14 and Eclipse GlassFish 8.0.5 for the first line on those servers, with the same two other lines.
The First JSP and JSTL Page
A servlet is good at deciding and bad at producing markup. The Model 2 arrangement used throughout Part I therefore
keeps
both jobs apart: a servlet handles the request, puts the model in a request attribute and forwards to a JSP that
renders it.
The view lives under WEB-INF/jsp, so a browser cannot request it directly.
package org.example.bookshelf.book;
public record Book(long id, String title, String author, int year) {
}
package org.example.bookshelf.book;
import java.util.List;
import java.util.Locale;
/** In-memory catalogue used by the first Bookshelf pages (a real application would call a repository). */
public class BookService {
private static final List<Book> BOOKS = List.of(
new Book(1, "Dune", "Frank Herbert", 1965),
new Book(2, "Neuromancer", "William Gibson", 1984),
new Book(3, "The Left Hand of Darkness", "Ursula K. Le Guin", 1969),
new Book(4, "R&D <Notes>", "A. Tester", 2020));
public List<Book> findAll(String filter) {
if (filter == null || filter.isBlank()) {
return BOOKS;
}
String needle = filter.toLowerCase(Locale.ROOT);
return BOOKS.stream()
.filter(b -> b.title().toLowerCase(Locale.ROOT).contains(needle)
|| b.author().toLowerCase(Locale.ROOT).contains(needle))
.toList();
}
}
The fourth title contains markup characters on purpose: it proves, in the output below, that the view escapes what it prints.
package org.example.bookshelf.web;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.example.bookshelf.book.BookService;
@WebServlet(urlPatterns = {"/books"})
public class CatalogueServlet extends HttpServlet {
private final BookService books = new BookService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String filter = req.getParameter("filter");
req.setAttribute("books", books.findAll(filter));
req.setAttribute("filter", filter == null ? "" : filter);
req.getRequestDispatcher("/WEB-INF/jsp/catalogue.jsp").forward(req, resp);
}
}
<%@ page session="false" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="bs" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bookshelf catalogue</title>
</head>
<body>
<jsp:include page="/WEB-INF/jsp/banner.jspf"/>
<form action="${pageContext.request.contextPath}/books" method="get">
<input type="search" name="filter" value="<c:out value='${filter}'/>">
<button type="submit">Search</button>
</form>
<c:choose>
<c:when test="${empty books}">
<p>No books match "<c:out value="${filter}"/>".</p>
</c:when>
<c:otherwise>
<ul>
<c:forEach var="b" items="${books}" varStatus="s">
<li class="${s.index % 2 == 0 ? 'even' : 'odd'}"><c:out value="${b.title}"/> by <c:out value="${b.author}"/> (${b.year}) <bs:rating value="${b.id}"/></li>
</c:forEach>
</ul>
<p>${books.size()} book(s)</p>
</c:otherwise>
</c:choose>
</body>
</html>
<h1>Bookshelf</h1>
<%@ tag body-content="empty" pageEncoding="UTF-8" %>
<%@ attribute name="value" required="true" type="java.lang.Integer" %>
<span class="rating" title="${value} out of 5">${value} / 5</span>
Build and deploy, then ask for the catalogue and for a filtered one.
curl http://localhost:8080/bookshelf/books
curl "http://localhost:8080/bookshelf/books?filter=gib"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bookshelf catalogue</title>
</head>
<body>
<h1>Bookshelf</h1>
<form action="/bookshelf/books" method="get">
<input type="search" name="filter" value="">
<button type="submit">Search</button>
</form>
<ul>
<li class="even">Dune by Frank Herbert (1965)
<span class="rating" title="1 out of 5">1 / 5</span>
</li>
<li class="odd">Neuromancer by William Gibson (1984)
<span class="rating" title="2 out of 5">2 / 5</span>
</li>
<li class="even">The Left Hand of Darkness by Ursula K. Le Guin (1969)
<span class="rating" title="3 out of 5">3 / 5</span>
</li>
<li class="odd">R&D <Notes> by A. Tester (2020)
<span class="rating" title="4 out of 5">4 / 5</span>
</li>
</ul>
<p>4 book(s)</p>
</body>
</html>
(The blank lines that the directives and tags leave in the real output were removed above.) The second request returns
only
the Neuromancer item and <p>1 book(s)</p>; a filter with no match prints No books match "zzz"., and a filter such as
"><script> is echoed back as "><script>, because c:out escapes <, >, &, ' and
".
Read the page from top to bottom.
| Construct | What happens |
|---|---|
|
A directive, processed at translation time. |
|
Makes the JSTL core library available under the prefix |
|
An EL expression in template text. |
|
Prints a value with XML escaping on. The model values that come from users or databases are printed with |
|
Conditional rendering; |
|
Loops over the list; |
|
A standard action that includes another resource at request time, here a fragment with the page header. |
|
A tag file call: the file |
|
EL calls a method on the list. Method calls in EL have been possible since EL 2.2. |
The model in the request travels from the servlet to the JSP through request.setAttribute("books", ...). The EL
expression
${books} searches the page, request, session and application scopes in that order and finds it in the request scope.
First Steps With JSP Features
The catalogue page used a handful of features without introducing them. Before moving on, the next page shows the smaller
steps that an introduction to JSP normally takes: template text, EL, request parameters, a JavaBean, jsp:include,
a session, and what a scriptlet looks like (for reading old code only).
Template Text, Parameters, Beans and Includes
Everything in a JSP that is not a directive, an action or a scripting element is template text and is copied to the
response unchanged. EL expressions in the template text are evaluated and printed. A JavaBean is a class with a public
no-argument constructor and getters and setters; the standard actions jsp:useBean, jsp:setProperty and
jsp:getProperty create one in a scope and read and write its properties.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<jsp:useBean id="hello" class="org.example.bookshelf.web.Greeting" scope="request"/>
<jsp:setProperty name="hello" property="name" param="name"/>
<!DOCTYPE html>
<html lang="en">
<body>
<jsp:include page="/WEB-INF/jsp/banner.jspf"/>
<p><jsp:getProperty name="hello" property="message"/></p>
<p>Hello, <c:out value="${empty param.name ? 'reader' : param.name}"/></p>
<p>Request method: ${pageContext.request.method}, context path: ${pageContext.request.contextPath}</p>
<p>2 + 3 = ${2 + 3}</p>
<ul>
<c:forEach var="i" begin="1" end="3">
<li>Row ${i}<c:if test="${i % 2 == 0}"> (even)</c:if></li>
</c:forEach>
</ul>
</body>
</html>
package org.example.bookshelf.web;
/** A JavaBean: public no-argument constructor plus getters and setters. */
public class Greeting {
private String name = "reader";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMessage() {
return "Welcome to the Bookshelf, " + name + "!";
}
}
curl "http://localhost:8080/bookshelf/first-steps.jsp?name=Ada"
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Bookshelf</h1>
<p>Welcome to the Bookshelf, Ada!</p>
<p>Hello, Ada</p>
<p>Request method: GET, context path: /bookshelf</p>
<p>2 + 3 = 5</p>
<ul>
<li>Row 1</li>
<li>Row 2 (even)</li>
<li>Row 3</li>
</ul>
</body>
</html>
Without ?name=Ada the bean keeps its default and the page says "Welcome to the Bookshelf, reader!" and "Hello, reader".
jsp:setProperty with param="name" copies the request parameter name into the bean property of the same name; if the
parameter is absent, the property is not touched. The EL implicit object param reads the same parameter directly,
and the
?: operator picks a default. Unlike jsp:getProperty, the c:out call escapes the value, which is why it is the safer
choice for anything that came from a user.
Sessions
HTTP is stateless: the server does not remember the previous request. A session gives each browser a server-side map
identified by a cookie, JSESSIONID. The first servlet that needs it calls request.getSession(), which creates the
session and makes the container send the cookie; JSPs read what the servlet stored through the sessionScope EL object.
package org.example.bookshelf.web;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/visits")
public class VisitServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
int visits = session.getAttribute("visits") instanceof Integer n ? n + 1 : 1;
session.setAttribute("visits", visits);
req.getRequestDispatcher("/WEB-INF/jsp/visits.jsp").forward(req, resp);
}
}
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html lang="en">
<body>
<p>You have visited this page ${sessionScope.visits} time(s) in this session.</p>
</body>
</html>
curl keeps the cookie between calls when it is given a cookie jar, which shows the session at work.
curl -s -c jar.txt http://localhost:8080/bookshelf/visits | grep visited
curl -s -b jar.txt -c jar.txt http://localhost:8080/bookshelf/visits | grep visited
curl -s -b jar.txt -c jar.txt http://localhost:8080/bookshelf/visits | grep visited
<p>You have visited this page 1 time(s) in this session.</p>
<p>You have visited this page 2 time(s) in this session.</p>
<p>You have visited this page 3 time(s) in this session.</p>
The cookie jar shows the cookie scoped to the context path with the HttpOnly flag that the descriptor requested:
#HttpOnly_localhost FALSE /bookshelf FALSE 0 JSESSIONID <id>. Sessions in depth (timeouts, URL rewriting, the
SameSite attribute) are the subject of Servlet Fundamentals and of Forms, Validation & Sessions in Part I.
Source: Jakarta Servlet 6.1 — Sessions
A Scriptlet, for Reading Old Code
Older JSP code puts Java inside the page with scriptlets (<% ... %>), expressions (<%= ... %>) and
declarations. The
syntax still works on Tomcat 11, which is why it is shown, but only as a "before" snippet: it is hard to test, mixes logic
into markup, and is exactly what EL, JSTL and tag files replace.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.Date" %>
<html>
<body>
<%-- legacy style, shown only as a "before" snippet --%>
<% String name = request.getParameter("name"); if (name == null) { name = "reader"; } %>
<p>Hello, <%= name %> at <%= new Date().getClass().getSimpleName() %></p>
<ul>
<% for (int i = 1; i <= 3; i++) { %>
<li>Row <%= i %><% if (i % 2 == 0) { %> (even)<% } %></li>
<% } %>
</ul>
</body>
</html>
The same loop and condition written with EL and JSTL are the three lines in first-steps.jsp above
(<c:forEach var="i" begin="1" end="3"> with <c:if test="${i % 2 == 0}">). Both pages printed the same list:
Row 1, Row 2 (even), Row 3. Requesting the scriptlet page with ?name=Bob printed Hello, Bob at Date. Notice
what the scriptlet page does not do: it prints name
without escaping, so ?name=<script>... would be executed by the browser, while the EL and c:out version above
escapes
it. The import attribute of the page directive is the only place where a Java import belongs in a JSP; the session
attribute, the buffer attribute and the other directives are the subject of Directives in Part I.
What Happens to a Request
Putting the pieces together shows what the container does for the request GET /bookshelf/books?filter=dune.
| Step | What the container does |
|---|---|
1-2 |
The connector parses the HTTP request. The mapper picks the web application whose context path is the longest prefix of the URL ( |
3 |
The container calls |
4-5 |
The servlet builds the model and forwards. The forward does not send a response to the browser: it hands the same request and response objects to the target. |
6 |
The target is a JSP. The first time it is requested, Jasper translates it into a Java source file, compiles it with the Eclipse JDT compiler and loads the class; the file |
7 |
|
That is why the first request to a JSP is slower than the following ones, and why a syntax error in a JSP appears as
an error
page at request time and not at deployment time (Jasper can also precompile all JSPs at build time; Jasper Configuration & Precompilation explains
how). By default, Tomcat’s JSP servlet runs in development mode: it checks the modification time of the JSP file at most
every few seconds (the modificationTestInterval parameter, default 4) and recompiles a changed page on the next
request. In the
verification for this page, editing first-steps.jsp in the deployed directory changed the response within five seconds
without a restart.
Building the WAR and Looking Inside
mvn clean package
unzip -l target/bookshelf.war
META-INF/MANIFEST.MF
first-steps.jsp
index.html
catalogue.bks
WEB-INF/web.xml
WEB-INF/classes/org/example/bookshelf/book/Book.class
WEB-INF/classes/org/example/bookshelf/book/BookService.class
WEB-INF/classes/org/example/bookshelf/web/CatalogueServlet.class
WEB-INF/classes/org/example/bookshelf/web/Greeting.class
WEB-INF/classes/org/example/bookshelf/web/HelloServlet.class
WEB-INF/classes/org/example/bookshelf/web/StartupListener.class
WEB-INF/classes/org/example/bookshelf/web/VisitServlet.class
WEB-INF/jsp/about.jsp
WEB-INF/jsp/banner.jspf
WEB-INF/jsp/catalogue.jsp
WEB-INF/jsp/not-found.jsp
WEB-INF/jsp/visits.jsp
WEB-INF/tags/rating.tag
WEB-INF/lib/jakarta.servlet.jsp.jstl-api-3.0.2.jar
WEB-INF/lib/jakarta.servlet.jsp.jstl-3.0.1.jar
META-INF/maven/org.example/bookshelf-jsp/pom.xml
META-INF/maven/org.example/bookshelf-jsp/pom.properties
(The META-INF/maven entries and the directory entries are omitted from some listings; the order depends on the plugin.)
This is the contents a servlet container expects: the public files at the root, and under WEB-INF the descriptor, the
classes in classes/, the jars in lib/, the private views and the tag files. The three provided APIs are absent from
WEB-INF/lib, as intended. A mvn clean matters when files are renamed or moved: without it the old target/classes and
target/<finalName> directories can leak a stale file into the WAR (a WEB-INF/views/catalogue.jsp left over from an
earlier
layout appeared in a build made for this page until clean was used).
Deploying to Tomcat
Copying the WAR to webapps/
The simplest deployment is a file copy. The Host element of server.xml has an appBase (default webapps), and with
the default deployOnStartup="true" and autoDeploy="true" Tomcat deploys what it finds there at startup and keeps
scanning
while running: a new WAR, an exploded directory or a context descriptor is deployed, and a replaced WAR is redeployed
(with the old session state discarded). By default the WAR is unpacked into a directory of the same name (unpackWARs
defaults to true).
cp target/bookshelf.war "$CATALINA_HOME/webapps/"
grep 'bookshelf.war' "$CATALINA_HOME/logs/catalina.$(date +%Y-%m-%d).log"
curl -s http://localhost:8080/bookshelf/hello
rm "$CATALINA_HOME/webapps/bookshelf.war" # autoDeploy undeploys it and deletes the unpacked directory
INFO [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [.../webapps/bookshelf.war]
INFO [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [.../webapps/bookshelf.war] has finished in [141] ms
The scan runs on a timer, so the application may need up to about ten seconds before it answers: the 404 right after the
copy and the 200 a few seconds later is normal (in the verification, the new /about page answered 404 immediately
after a
redeploy and 200 twelve seconds later). Wait for the "has finished" line in catalina.<date>.log rather than guessing.
The file name is the context path. Tomcat derives the name and context path of an application deployed by the host from its
file name; the context path may therefore not be defined inside the application’s own META-INF/context.xml.
File name in webapps/ |
Context path | Notes |
|---|---|---|
|
|
The usual case. |
|
|
The base name is |
|
|
A |
|
|
Parallel deployment: several versions of one context path run side by side. New sessions go to the latest version, and existing sessions stay on theirs. |
Versions are compared as strings, so bookshelf##11.war is considered earlier than bookshelf##2.war; Tomcat’s
documentation recommends zero padding (bookshelf##002.war) when versions are numeric.
Source: Tomcat 11 Context — Naming
The Manager Application
The Manager is a web application that ships in webapps/manager and deploys, reloads and removes other applications
over
HTTP. Its access is disabled until a user with a manager-* role exists: the default tomcat-users.xml has no such
user.
The roles are manager-gui (HTML interface), manager-script (the plain-text interface used by tools),
manager-jmx and manager-status. The documentation warns that the HTML interface is protected against CSRF but the
text and
JMX interfaces are not, and recommends that a user holding manager-gui should not also hold manager-script or
manager-jmx. The Manager’s META-INF/context.xml also restricts it to local addresses with a RemoteCIDRValve
(127.0.0.0/8 and ::1/128 in the shipped file).
<tomcat-users>
<role rolename="manager-script"/>
<user username="deployer" password="CHANGE_ME" roles="manager-script"/>
</tomcat-users>
M=http://localhost:8080/manager/text
curl -u deployer:"$MANAGER_PASSWORD" -T target/bookshelf.war "$M/deploy?path=/bookshelf&update=true"
curl -u deployer:"$MANAGER_PASSWORD" "$M/list"
curl -u deployer:"$MANAGER_PASSWORD" "$M/reload?path=/bookshelf"
curl -u deployer:"$MANAGER_PASSWORD" "$M/undeploy?path=/bookshelf"
OK - Deployed application at context path [/bookshelf]
OK - Listed applications for virtual host [localhost]
/bookshelf:running:0:bookshelf
/manager:running:0:manager
OK - Reloaded application at context path [/bookshelf]
OK - Undeployed application at context path [/bookshelf]
(MANAGER_PASSWORD is an environment variable you set yourself; never put the real value in a script that is committed.)
The path parameter decides the context path here, regardless of the WAR file’s name, and version=1.0 added to the
deploy request produced /bookshelf##1.0 in the listing. A request without valid credentials gets the 401 page that
explains which role is missing. In production the Manager is normally not exposed at all (remove it, or keep it
behind the RemoteCIDRValve and TLS); this section uses it only to show the command-line deployment.
Context Descriptors
Settings that belong to the server and not to the application, such as a JNDI data source, a session manager or a
different
docBase, live in a context descriptor: a context.xml file either in
$CATALINA_BASE/conf/<engine>/<host>/<name>.xml
(outside the WAR) or in the WAR’s own META-INF/context.xml. The deployer guide says that putting Context elements in
server.xml still works but is discouraged. Everything about the Context element is in the Tomcat configuration
reference;
this section’s examples need no descriptor.
Running Without Installing a Server: Maven Plugins
For an edit-run loop, a Maven plugin that starts a container from the build is quicker than copying WARs. Two are in common use: the Jetty plugin, which embeds Jetty, and Cargo, which downloads and runs an installed container.
The Jetty Maven Plugin
Since Jetty 12 the Maven plugin carries the Jakarta EE environment in its coordinates. For EE 11 it is
org.eclipse.jetty.ee11:jetty-ee11-maven-plugin, 12.1.14 on 2026-10-08. Declaring it with a context path is enough; the
project’s provided APIs are not packaged, and the JSTL jars of WEB-INF/lib are used.
<plugin>
<groupId>org.eclipse.jetty.ee11</groupId>
<artifactId>jetty-ee11-maven-plugin</artifactId>
<version>12.1.14</version>
<configuration>
<webApp>
<contextPath>/bookshelf</contextPath>
</webApp>
</configuration>
</plugin>
mvn jetty:run
curl http://localhost:8080/bookshelf/hello # in another terminal
Hello from Bookshelf on jetty/12.1.14
Servlet API 6.1
Context path /bookshelf
jetty:run runs the Maven build up to compilation, starts Jetty on port 8080 by default (-Djetty.http.port=9090
changed it
in the verification) and serves src/main/webapp directly, so a JSP edit is visible without a build. It runs until
Ctrl+C. By default
it does not restart the web application when classes change; the scan setting turns on redeployment on change. The
Jetty documentation does not list extra dependencies to enable JSP: in the verification, /books and /first-steps.jsp
rendered with only the plugin above declared.
Cargo
The Codehaus Cargo Maven plugin controls containers from the build: it installs one from a URL, a local directory or Maven
Central, deploys the project’s WAR, and starts or stops it. Its tomcat11x container id covers Tomcat 11. With a
zipUrlInstaller the first run downloads Tomcat into target/cargo/installs.
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven3-plugin</artifactId>
<version>1.10.29</version>
<configuration>
<container>
<containerId>tomcat11x</containerId>
<zipUrlInstaller>
<url>https://archive.apache.org/dist/tomcat/tomcat-11/v11.0.26/bin/apache-tomcat-11.0.26.zip</url>
</zipUrlInstaller>
</container>
<configuration>
<properties>
<cargo.servlet.port>8080</cargo.servlet.port>
</properties>
</configuration>
<deployables>
<deployable>
<type>war</type>
<properties>
<context>/bookshelf</context>
</properties>
</deployable>
</deployables>
</configuration>
</plugin>
mvn package cargo:run
curl http://localhost:8080/bookshelf/hello # in another terminal
Hello from Bookshelf on Apache Tomcat/11.0.26
Servlet API 6.1
Context path /bookshelf
The WAR is built first because cargo:run deploys the project’s artifact. The download took a few minutes on the
first run
and the container stayed in the foreground until it was stopped. Cargo runs a real Tomcat (the same code as the
distribution), which is its advantage over an embedded server when a test needs the exact container.
Source: Codehaus Cargo — Tomcat 11.x
The Same WAR on GlassFish 8
GlassFish needs no descriptor and no extra libraries for this WAR. The commands come from its Quick Start Guide: start the
default domain, deploy with asadmin deploy, list, and stop.
glassfish8/bin/asadmin start-domain
glassfish8/bin/asadmin deploy --contextroot bookshelf target/bookshelf.war
glassfish8/bin/asadmin list-applications
curl http://localhost:8080/bookshelf/hello
glassfish8/bin/asadmin stop-domain
Application deployed with name bookshelf.
Command deploy executed successfully.
bookshelf <web>
Hello from Bookshelf on Eclipse GlassFish 8.0.5
Servlet API 6.1
Context path /bookshelf
(The check was made with the Web Profile distribution 8.0.5, in a domain created with asadmin create-domain
--portbase 18500,
so its ports differed from the defaults 8080 and 4848.) One thing differs from Tomcat and Jetty. On GlassFish 8.0.5 the
catalogue page failed with jakarta.el.PropertyNotFoundException: The class 'org.example.bookshelf.book.Book' does
not have the property 'title':
the Book is a Java record, and EL there did not resolve record components, although the Pages 4.0 specification lists
jakarta.el.RecordELResolver in the chain of resolvers and EL 6.0 says record support is enabled by default. The same
WAR rendered on Tomcat 11.0.26 and on Jetty 12.1.14. Adding JavaBean-style getters to the record (getTitle(),
getAuthor(),
getYear(), getId()) made the page render on GlassFish too. If an application must run on several servers, give
model types
getters, or use ordinary classes, until the GlassFish behaviour is fixed.
A Docker Image for the WAR
The official tomcat image on Docker Hub is a Tomcat install in /usr/local/tomcat; tags such as
11.0-jdk21-temurin pin the
Tomcat line and the JDK. Two details matter for a WAR. First, the image’s webapps directory is empty: the default
applications are kept in webapps.dist (verified: ls /usr/local/tomcat/webapps lists nothing, webapps.dist has
ROOT,
docs, examples, host-manager and manager), so a copied WAR is the only application. Second, a WAR named
ROOT.war is deployed at
the root context path, so the container answers at / and not at /bookshelf/.
A multi-stage build keeps Maven and the sources out of the final image: the first stage builds the WAR, the second copies it into a Tomcat image. Only these lines are specific to a WAR; the general mechanics (layers, caches, non-root users, health checks, scanning) are covered by the Docker pages of this site, for example Dockerfile Fundamentals, Build Cache and Multi-Stage Builds, Java and Spring Boot Images and Image Best Practices and Security.
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn -q -B dependency:go-offline
COPY src ./src
RUN mvn -q -B package
FROM tomcat:11.0-jdk21-temurin
COPY --from=build /build/target/bookshelf.war /usr/local/tomcat/webapps/ROOT.war
EXPOSE 8080
printf 'target\n.git\n' > .dockerignore
docker build -t bookshelf-jsp:dev .
docker run -d --rm --name bookshelf -p 8080:8080 bookshelf-jsp:dev
curl http://localhost:8080/hello
curl "http://localhost:8080/books?filter=gib"
docker stop bookshelf
Hello from Bookshelf on Apache Tomcat/11.0.26
Servlet API 6.1
Context path
<li class="even">Neuromancer by William Gibson (1984)
The context path is empty because the WAR is ROOT.war, which is why every URL lost its /bookshelf prefix. The
image built for
this check was 533 MB; the official image runs Tomcat as root by default (verified with id -un), so a production image
adds a non-root user as the Docker security page describes. If the application needs a database or other services, the
Compose and Testcontainers pages of this site show how to start them beside the container.
Source: Docker Official Image — tomcat
Source: Docker — Multi-stage builds
Worked Example: From an Empty Folder to a Catalogue
The sections above are the explanation; this is the sequence a reader performs, with the check after each step. It uses only files shown on this page.
| Step | Action | Check |
|---|---|---|
1 |
Install JDK 21 and Maven; set |
|
2 |
Download, verify and unpack Tomcat 11.0.26; run |
|
3 |
Create |
|
4 |
Add |
|
5 |
Copy the WAR to |
|
6 |
Add |
|
7 |
Open |
The response contains |
8 |
Request |
|
9 |
Edit |
The new text appears without restarting Tomcat (development mode of the JSP servlet). |
From here Part I takes the same project further: servlets as controllers with a service layer (MVC with Servlets & JSP), EL and the JSTL libraries
in depth (Expression Language Basics, JSTL Core), tag files and tag libraries, forms with validation and sticky fields, internationalisation (JSTL i18n & Messages), security, error
pages
and a Spring MVC variant of the catalogue page. Part II then starts bookshelf-struts, a second WAR on the same
Tomcat built on
Apache Struts 7.4; its own Struts Getting Started page builds it by hand with the struts2-bom, because the Struts
Maven archetypes stop at 6.0.0, and the two applications can run on one Tomcat side by side because each has its own
context
path.
Source: Apache Struts — Getting Started
Pitfalls
Every row below was reproduced on Tomcat 11.0.26, Jetty 12.1.14 or GlassFish 8.0.5 unless the right-hand column cites the documentation instead.
| Symptom | Cause | Fix |
|---|---|---|
|
|
Wait for the "Deployment of web application archive … has finished" line in |
|
The context path is the WAR’s file name, not the Maven artifactId or the |
Set |
A WAR deploys without errors but its servlet answers |
The classes import |
Recompile against |
|
The classes were compiled for Java 25 (69) and Tomcat runs on Java 21 (65). |
Compile with |
JSP fails with |
|
Add |
The WAR is large and holds |
The Servlet, Pages or EL API was added without |
Use scope |
A JSP under |
Anything outside |
Keep views under |
|
A JSP participates in a session by default ( |
Add |
|
EL on GlassFish 8.0.5 did not resolve record components in JSP, unlike Tomcat 11.0.26 and Jetty 12.1.14. |
Add JavaBean getters to the record, or use a class, if the WAR must run on GlassFish. |
|
The archetype (1.5) still targets the |
Replace the descriptor with the Servlet 6.1 one and set |
A JSP edit does not show up, or a Java edit does not show up |
A JSP is recompiled when its file changes, but a class change needs the context to be reloaded or redeployed; and JSP checking is a development-mode feature. |
Redeploy the WAR (copy it again, or use the Manager’s |
|
No user with the |
Add a user as shown above, restart, and use the placeholder-free password from your own environment variable. |
Parallel versions |
Versions are compared as strings, so |
Zero-pad numeric versions ( |
|
The WAR was copied to |
Name the file |
JSP pages in an executable Spring Boot JAR do not work |
The Spring Boot reference lists JSP limitations: JSPs are not supported in an executable jar, and it advises avoiding JSPs where possible. |
Package a WAR (see the Spring Boot pages of this site) or choose Thymeleaf. |
What Changed Recently
-
Tomcat 11.0 is the Jakarta EE 11 line. It implements Servlet 6.1, Pages 4.0 and EL 6.0 on Java 17 or later; 11.0.26 is the latest release on 2026-10-08. Tomcat 10.1.60 implements Servlet 6.0 and Pages 3.1 (Jakarta EE 10) and Tomcat 9.0.x keeps the
javax.*namespace; its end of support is "no earlier than 31 March 2027". Tomcat 10.0.x reached end of life on 2022-10-31. -
Servlet 6.1. New methods on
HttpServletResponsechoose the redirect status code and body; the error dispatch gains thejakarta.servlet.error.query_stringattribute; containers must support HTTPS;jakarta.servlet.request.secure_protocolis a required attribute for secure requests. Servlet 6.0 had already removedSingleThreadModel(Servlet Fundamentals covers this). -
Pages 4.0. The minimum Java version is 17; the
isThreadSafepage attribute and thejsp:pluginaction, both deprecated in 3.1, were removed; the deprecatedjakarta.servlet.jsp.elclasses were removed;ErrorDatagained the query string and the HTTP method of the failed request. Pages 3.1 had added an option to raisePropertyNotFoundExceptionfor unknown identifiers (errorOnELNotFound). -
Expression Language 6.0 made Java 17 the minimum and added
RecordELResolver(on by default), an Optional resolver and thelengthproperty for arrays. -
Jakarta Tags 3.0. The URIs are
jakarta.tags.core,jakarta.tags.fmt,jakarta.tags.functions,jakarta.tags.sqlandjakarta.tags.xml. The implementation jarorg.glassfish.web:jakarta.servlet.jsp.jstlis still 3.0.1 (2023-01-19) on Maven Central; a 3.1 milestone exists but no final release. The old URIshttp://java.sun.com/jsp/jstl/coreandhttp://xmlns.jcp.org/jsp/jstl/coreare still accepted by this implementation (verified on Tomcat 11.0.26). -
Jetty 12. One module set per Jakarta EE environment, hence the
ee11in the plugin’s coordinates; Jetty 12.1.14 is current and Jetty 11 is end of life. -
GlassFish 8.0 is the Jakarta EE 11 release, with virtual thread support among the items its release announcement highlights; 8.0.5 is on Maven Central.
-
Jakarta EE Tutorial. The tutorial lists "Jakarta Pages" under Archived in the Web Profile, with the note that it "has been archived in favor of Facelets". Pages is still part of the Jakarta EE 11 Web Profile and of Tomcat 11, and it is what the many existing applications run on.
-
Tomcat security fixes touch JSP directly. Tomcat’s security pages list, for the 11.0 line, CVE-2024-52318 ("Incorrect JSP tag recycling leads to XSS", fixed in 11.0.1), CVE-2024-50379 and CVE-2024-56337 (a time-of-check time-of-use race in JSP compilation, fixed in 11.0.2) and CVE-2025-24813 (path equivalence in partial PUT, fixed in 11.0.3); run the latest 11.0.x, which is 11.0.26 today.
-
Tomcat ships the migration tool.
lib/jakartaee-migration-1.0.12-shaded.jarandbin/migrate.shconvertjavaxbinaries; the Host’slegacyAppBaseattribute names the directory whose applications are converted on deployment.
Source: Apache Tomcat Versions
Source: Apache Tomcat 11 vulnerabilities
Where the Books Are Outdated
Two of the section’s reference books cover this ground: Web Development with JavaServer Pages (2nd edition, JSP 1.2) for the first steps, how JSP works and deploying, and JSTL in Action (JSTL 1.0) for the tag libraries; the section bibliography lists them. Their opening chapters introduce what a servlet container, a servlet and a JSP are, and those concepts are unchanged, but the platform they were written for has changed beyond recognition:
| Area | The books (as consulted) | Current at 2026-10-08 |
|---|---|---|
Platform |
J2EE 1.3 or 1.4, Servlet 2.3 or 2.4, JSP 1.2 or 2.0, JDK 1.2 to 5, Tomcat 4 or 6, the |
Jakarta EE 11 with Servlet 6.1, Pages 4.0, EL 6.0, Java 21 (17 is the minimum), Tomcat 11 and the |
|
The Servlet 2.3 DTD and a |
The Servlet 6.1 schema ( |
JSTL URIs |
|
|
Scripting |
Scriptlet-centric pages as the norm, with |
EL and JSTL; scriptlets still compile (verified) but are the exception, and a deployment descriptor can forbid them |
Client formats |
WML and WAP for phones, |
HTML5, JSON and CSV or XLSX downloads |
Removed JSP features |
|
Both removed in Pages 4.0 |
Setup advice of the Tomcat 4 and 5 era has also changed, as Tomcat’s current guides show. Tomcat’s deployer guide says
that
Context elements used to be stored in server.xml and that this is now discouraged, in favour of a WAR in webapps/
or a context descriptor. The single manager role of early Tomcat versions was replaced from Tomcat 7 by four roles
(manager-gui, manager-status, manager-script, manager-jmx), and the Manager is unusable until a user holds one
of them. The <jsp-config>
and <taglib> descriptor elements and the other web.xml details of the books' deployment chapters are covered by
Servlet Fundamentals and Part I.
The code samples of chapters 2 to 4 of the JSP book contain typographic (curly) quotation marks that make them invalid if copied; every example on this page was typed from the official documentation and compiled. Later pages note the outdated details next to the concepts they touch, and What’s Changed & Migration collects them.
Source: Manning — JSTL in Action