
A maintainable JSP application gives each layer a clear job: a servlet handles the request, application code makes decisions and a JSP renders prepared data. This guide builds that flow with a small downloadable WAR project, then uses it to explain output handling and the compatibility checks needed when moving older Java EE applications to Jakarta.
Choose a compatible application and container
This lab targets JDK 25, Maven 3.9.x and Tomcat 10.1. It uses Servlet 6.0, Pages 3.1 and Jakarta Tags 3.0. The exact test container was Tomcat 10.1.59. The download pins the Servlet API, Tags API and implementation, and Maven plugins. Review current patches before deployment. The Tomcat version matrix maps container families to their specification versions and minimum Java versions.
Tomcat 9 applications use the Java EE servlet namespace. Tomcat 10 introduced the Jakarta namespace. Plan a migration against the complete dependency and container combination, because changing imports in one file leaves libraries and deployment descriptors to check. Java SE packages such as javax.sql are separate and should not be indiscriminately renamed.
Trace one request through the application
The browser requests /jsp-demo/hello?name=Ada. Tomcat routes /hello to the servlet. The servlet supplies a default for missing input, rejects names over 80 characters and sets a request attribute. It then forwards the same request to the JSP under WEB-INF/views.
package com.yenra.demo;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String name = request.getParameter("name");
if (name == null || name.isBlank()) name = "reader";
if (name.length() > 80) {
response.sendError(400, "Name must be at most 80 characters");
return;
}
response.setContentType("text/html;charset=UTF-8");
request.setAttribute("displayName", name);
request.getRequestDispatcher("/WEB-INF/views/hello.jsp").forward(request, response);
}
}The servlet keeps request-specific values in local variables. A servlet instance can serve concurrent requests, so storing the current user's name in an instance field would create unwanted shared state. A larger application would call a service from this controller, then pass only the data needed by the view.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>JSP greeting</title></head>
<body><h1>Hello, <c:out value="${displayName}" />!</h1>
<p>The servlet prepares data; this template renders HTML text.</p></body></html>c:out escapes characters for the HTML-text position used here, with escaping enabled by default. The Jakarta Tags specification defines that behavior and the jakarta.tags.core library URI. Plain expression-language output is not a general context-aware encoder. Use an appropriate mechanism for URLs, attributes or JavaScript rather than transferring this text-node example into another context.
Build and run a local test deployment
Extract the ZIP, open its project folder in a terminal and build:
mvn -q packageThe result is target/jsp-demo.war. Use a fresh local Tomcat 10.1 installation from the official downloads. In conf/server.xml, configure the HTTP Connector on port 8080 with address="127.0.0.1" for this local lab. Use another available port if 8080 is occupied, and adjust the browser URL accordingly. Keep the Servlet API dependency scoped as provided because the container supplies it; the Tags implementation is included in the WAR.
In PowerShell, with JAVA_HOME already pointing to JDK 25, replace the Tomcat path:
$tomcatRoot = "C:\Tools\apache-tomcat-10.1.59"
Copy-Item -LiteralPath "target/jsp-demo.war" -Destination "$tomcatRoot/webapps/jsp-demo.war"
& "$tomcatRoot/bin/catalina.bat" runIn a Unix shell:
TOMCAT_ROOT="/path/to/apache-tomcat-10.1.59"
cp target/jsp-demo.war "$TOMCAT_ROOT/webapps/jsp-demo.war"
sh "$TOMCAT_ROOT/bin/catalina.sh" runKeep the terminal open, then visit http://127.0.0.1:8080/jsp-demo/hello?name=Ada. Expect “Hello, Ada!” Stop the local foreground server with Ctrl+C when finished. If deployment fails, inspect its terminal output and logs. The Tomcat deployment guide explains WAR layout and deployment behavior.
Verify output, boundaries and view access
On a narrow screen, scroll the table horizontally. Keyboard users can focus the table and use the arrow keys.
| Request | Expected result | What it checks |
|---|---|---|
/jsp-demo/hello | Hello, reader! | The missing-input default. |
/jsp-demo/hello?name=Ada | Hello, Ada! | Parameter-to-attribute-to-view flow. |
Name contains <script>alert(1)</script> | Visible literal text, no script execution | Escaping in the HTML text node. |
| Name contains 81 characters | HTTP 400 | The controller enforces its explicit size rule. |
/jsp-demo/WEB-INF/views/hello.jsp | HTTP 404 | The browser cannot request the private view directly. |
For the script-shaped input, URL-encode the value when constructing a request and inspect the rendered text or response source. The WEB-INF location protects direct access to that resource under the servlet specification; it does not authenticate a user or authorize access to application data. The Servlet 6.0 specification describes application structure and request handling.
This example only displays a GET response. Forms that change state also need the application's authentication, authorization, CSRF protection and validation policies. Keep SQL and business logic out of the JSP so those policies can be tested before rendering.
Plan a migration in observable steps
- Inventory the current JDK, container, Servlet/Pages/Tags versions, framework dependencies and deployment descriptors.
- Capture current behavior with tests for routing, sessions, encoding, authentication and error pages.
- Select a compatible target container and update the relevant Jakarta EE dependencies together.
- Convert affected package names and descriptors, rebuild and deploy to a separate test environment.
- Repeat behavioral and concurrency checks, inspect logs, then plan the production rollout and recovery path.
The Tomcat 9-to-10 migration guide documents the namespace boundary, and the Apache migration tool can assist conversion. Conversion is one step; it does not replace testing of framework compatibility or application behavior. Use the migration guide for each additional container-version jump.
When a tag URI cannot be resolved, inspect the Tags API and implementation packaged in the WAR. When a servlet class is missing, check dependency scope and whether the application and container use the same namespace. For unexpected encoding, trace request decoding and response content type explicitly. These focused checks are more useful than changing unrelated libraries until deployment happens to succeed.