PREREQUISITE TUTORIAL · MANUAL 00

Servlets and JSPs Foundation

From a browser request to a working Java todo application, one step at a time.

Beginner prerequisiteCore Java + HTMLServlet 6.0 / JSP 3.1Tomcat 10.1Port 8083
Start here if Servlets and JSP are new. This lesson assumes basic Java classes, collections, exceptions, and methods. It introduces enough HTML and HTTP to build a working Java web app, then connects each concept to Spring. Students already comfortable with these topics can go straight to Spring foundations.

1. What you will learn and build

You will build a small web application in two milestones. First, a static welcome page submits a name to a Servlet that returns a greeting. Then you will add a todo list with create, read, edit, completed status, delete, validation, and JSP views. Tasks are held in the current browser session, so this lesson needs no database credentials.

This is plain Java web development: there is no Spring Boot, no Spring container, and no dependency injection framework. You will explicitly configure a Servlet container, package a WAR, and deploy it. That makes the infrastructure supplied later by Spring visible.

  1. Understand HTML, HTTP requests, and browser/server roles.
  2. Install the tools and build a Maven WAR project.
  3. Run your first Servlet, then examine its request and response.
  4. Use JSP, Expression Language, and JSTL to render a todo list.
  5. Understand sessions, request attributes, filters, validation, and redirect behavior.
  6. Verify CRUD and explain how the implementation changes in Spring MVC.

2. What is a web application?

A console program receives input from a terminal and prints output there. A web application receives an HTTP request from a client and sends an HTTP response. The browser and the Java server are different processes, often on different machines. The browser displays HTML and runs browser JavaScript; your Servlet’s Java code runs on the server.

Browser → HTTP request → Tomcat → Java Servlet → HTTP response → Browser

A static page is a saved file that the server sends as-is. A dynamic page is generated from the current request or application data. Tomcat can serve index.html as a static file, execute HelloServlet for a greeting, and execute a JSP to generate HTML from task data.

The browser does not execute Servlet Java classes and does not receive your JSP source. “View page source” shows the rendered HTML, not the Java statements or JSTL tags that produced it. This explains why you must use a server URL instead of opening a JSP file from disk.

3. The HTML you need before coding Java

HTML describes the page structure. A heading uses h1 or h2, a paragraph uses p, a hyperlink uses a, and a form uses form with input and button elements. CSS controls presentation. This application does not need JavaScript to submit forms: the browser can submit them directly.

Small HTML example
<form action="hello" method="get">
  <label>Name <input name="name" maxlength="40"></label>
  <button type="submit">Send</button>
</form>

action chooses the destination URL; method chooses the HTTP method; name chooses the submitted parameter key. An input with id="title" but no name is not submitted as a normal named form field. The label improves usability and accessibility.

GET form submissions append URL-encoded parameters to the URL, such as hello?name=Priya. POST form submissions normally put form-encoded fields in the request body. A checkbox submits its name/value only when checked; without an explicit value its checked value is “on”. The TodoServlet uses that behavior to read completed.

required and maxlength guide the user, but a client can bypass them. The Servlet must validate the submitted values again. A button inside a form submits by default; use type="button" for a button that should not submit.

Before running Java, find these elements in src/main/webapp/index.html. That file is included in full below.

4. Understand HTTP, URLs, methods, and status codes

Consider http://localhost:8083/todo-servlet-jsp/hello?name=Priya. http is the scheme, localhost is the host, 8083 is the port, /todo-servlet-jsp is the application context path, /hello is a route inside that application, and name=Priya is a query parameter. The context path normally comes from the deployed WAR name.

PartMeaning in this lab
GETRead a page or greeting; do not delete or modify a task with GET.
POSTSubmit a task change or reset the session.
Request headersMetadata such as Cookie and Content-Type.
Request bodyPOST form fields; here they are application/x-www-form-urlencoded.
Response status200 success, 302 redirect, 400 invalid input, 403 failed form-token check, 404 unavailable route/task, 500 unexpected server failure.
Response headersContent-Type selects how the body is interpreted; Location identifies a redirect target; Set-Cookie creates/updates a cookie.

The response body is the actual content, such as HTML or plain text. Sending text does not automatically mean an operation succeeded; inspect the status. An HTTP request ends when its response completes. To remember a visitor between requests, you need an explicit mechanism such as a session.

In browser developer tools, open Network before submitting a form. Inspect method, URL, status, request fields, response headers, and response body. This habit remains useful for the Spring and React labs.

5. Java, Jakarta, Tomcat, and compatibility

The Servlet API defines Java types such as HttpServletRequest and HttpServletResponse. A Servlet container implements that API and invokes your classes. Apache Tomcat is the container used here; it also includes Jasper, which compiles and runs JSP pages.

This lab pins Java 21, Tomcat 10.1.60, Servlet 6.0, JSP 3.1, and Jakarta JSTL 3.x. Tomcat 10.1 implements Servlet 6.0 and Pages 3.1. The source uses jakarta.servlet.*, not the older javax.servlet.* namespace. Tomcat 9 and earlier examples often use javax; do not combine those imports with this Tomcat 10.1 project.

The Servlet API dependency is marked provided because Tomcat supplies it at runtime. JSTL has separate API and implementation JARs that this app packages itself. The project target is Java 21, so start Tomcat with a JDK capable of executing Java 21 class files.

References: Tomcat 10.1 compatibility, Servlet 6.0, Jakarta Pages 3.1, and Jakarta Tags 3.0.

6. Install and prepare your tools

Using a MacBook? Open the complete macOS setup and troubleshooting guide in the third column. It includes Apple Silicon/Intel downloads, Zsh settings, Homebrew or archive installation, and deployment.

Open a guide below to read the full installation steps in the third column. Each guide includes setup and verification commands.

  1. JDK 21 installation — prepare Java and its compiler.
  2. Maven installation — download Maven, configure Path, and verify your first build.
  3. Tomcat installation — set up the Servlet container on port 8083.

Once the tools are ready, open the project in your IDE as a Maven project and continue to Hello World starter downloadProject structure. A special enterprise IDE edition is not required.

7. Understand the project and WAR structure

Start smaller: Hello World starter

New to Java web development? Download the Hello World starter ZIP (click here) before the full todo application. It contains just one Servlet, one HTML form, Maven configuration, and an offline step-by-step starter guide.

  1. Extract it and open the hello-servlet folder containing pom.xml.
  2. Run mvn clean package. Copy target/hello-servlet.war into your Tomcat webapps folder.
  3. Use the already running Tomcat, or start it if stopped. Open localhost:8080/hello-servlet/ (use 8083 if configured).
  4. Click the Hello World link: expect Hello, World!. Submit your name: expect a personalized greeting.

This starter has no JSP or database setup. Continue with the full todo project below when it works. The starter uses hello-servlet.war and /hello-servlet/; the rest of this manual uses todo-servlet-jsp.war and /todo-servlet-jsp/.

Download projects/todo-servlet-jsp (click here) to get just this project. Extract the ZIP, then open the todo-servlet-jsp folder containing pom.xml in your IDE. To build manually instead, create the directory and each file shown below at the indicated path.

Complete project paths
pom.xml
src/main/java/com/example/web/HelloServlet.java
src/main/java/com/example/web/Todo.java
src/main/java/com/example/web/SessionTodoStore.java
src/main/java/com/example/web/Csrf.java
src/main/java/com/example/web/RequestFilter.java
src/main/java/com/example/web/TodoServlet.java
src/main/webapp/index.html
src/main/webapp/WEB-INF/web.xml
src/main/webapp/WEB-INF/views/list.jsp
src/main/webapp/WEB-INF/views/form.jsp
src/main/webapp/assets/style.css
src/test/java/com/example/web/SessionTodoStoreTest.java
src/test/java/com/example/web/CsrfTest.java
.gitignore

src/main/java contains Java source. src/main/webapp is the web application’s source document root. Public index.html and CSS live there. WEB-INF contains configuration and JSP views that should be reached through a controller rather than directly by the browser.

Maven packages compiled classes under WEB-INF/classes and dependency JARs under WEB-INF/lib. Source folders and deployed folders are related but are not identical: Maven transforms source into the runtime WAR layout. target/todo-servlet-jsp.war is the deployable artifact.

A WAR is a Web Application Archive, essentially a ZIP with a standardized web application layout. Tomcat can deploy it and expand it into a directory. The usual context path for todo-servlet-jsp.war is /todo-servlet-jsp. There is no application main() method here: Tomcat is the running Java process and creates the Servlet instances.

Layout reference: Tomcat WAR deployment layout.

8. Write the complete runnable project

Each block below is a complete file, not a fragment. Save them at the exact paths. If you downloaded the bundle, these files already exist. Read the file explanation before moving on. The first milestone uses pom.xml, HelloServlet, index.html, and web.xml; the remaining classes and views add the todo workflow.

pom.xml Download file

This is an ordinary Maven WAR project, with no Spring dependencies and no Java main method. Tomcat supplies the Servlet API, so provided keeps that API out of WEB-INF/lib. The JSTL API and implementation are both packaged because Tomcat does not supply JSTL. The compiler targets Java 21; finalName determines both the WAR filename and its usual deployment context path.

Complete file · pom.xml
<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>com.example</groupId><artifactId>todo-servlet-jsp</artifactId><version>1.0.0</version>
  <packaging>war</packaging>
  <properties><maven.compiler.release>21</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
  <dependencies>
    <dependency><groupId>jakarta.servlet</groupId><artifactId>jakarta.servlet-api</artifactId><version>6.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>
    <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.11.4</version><scope>test</scope></dependency>
  </dependencies>
  <build>
    <finalName>todo-servlet-jsp</finalName>
    <plugins>
      <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.0</version></plugin>
      <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-war-plugin</artifactId><version>3.4.0</version></plugin>
      <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.4</version></plugin>
    </plugins>
  </build>
</project>

src/main/java/com/example/web/HelloServlet.java Download file

Tomcat discovers @WebServlet and routes /hello to this class within the application context. HttpServlet dispatches GET to doGet. getParameter reads the name query parameter. The response is explicitly plain text, so user text is not interpreted as HTML. This tiny endpoint is the first executable milestone before the todo application.

Complete file · src/main/java/com/example/web/HelloServlet.java
package com.example.web;

import java.io.IOException;
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 IOException {
        String name=request.getParameter("name");
        if(name==null || name.isBlank()) name="Student";
        if(name.length()>40) { response.sendError(400,"Name must be at most 40 characters"); return; }
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().println("Hello, "+name.strip()+"! This response came from a Java Servlet.");
    }
}

src/main/java/com/example/web/Todo.java Download file

A Todo is an immutable Java object. JSP EL accesses title through getTitle(), id through getId(), and completed through isCompleted(). Public getters are needed for property resolution. Serializable allows a container to serialize session-held data if configured; it does not give this application a durable database.

Complete file · src/main/java/com/example/web/Todo.java
package com.example.web;

import java.io.Serializable;

public class Todo implements Serializable {
    private static final long serialVersionUID=1L;
    private final String id;
    private final String title;
    private final boolean completed;
    public Todo(String id,String title,boolean completed) {
        this.id=id; this.title=title; this.completed=completed;
    }
    public String getId() { return id; }
    public String getTitle() { return title; }
    public boolean isCompleted() { return completed; }
}

src/main/java/com/example/web/SessionTodoStore.java Download file

A separate store handles data operations. Each browser session receives its own store, so there is no static application-wide list. Synchronization protects compound operations when requests for the same session overlap. list returns an immutable snapshot and Todo has immutable fields, keeping iteration safe. update of a missing id never inserts a new task.

Complete file · src/main/java/com/example/web/SessionTodoStore.java
package com.example.web;

import java.io.Serializable;
import java.util.*;

public class SessionTodoStore implements Serializable {
    private static final long serialVersionUID=1L;
    private final Map<String,Todo> todos=new LinkedHashMap<>();
    public synchronized List<Todo> list() { return List.copyOf(todos.values()); }
    public synchronized Optional<Todo> find(String id) { return Optional.ofNullable(todos.get(id)); }
    public synchronized Todo create(String title,boolean completed) {
        Todo todo=new Todo(UUID.randomUUID().toString(),title,completed);
        todos.put(todo.getId(),todo); return todo;
    }
    public synchronized Optional<Todo> update(String id,String title,boolean completed) {
        if(!todos.containsKey(id)) return Optional.empty();
        Todo todo=new Todo(id,title,completed); todos.put(id,todo); return Optional.of(todo);
    }
    public synchronized boolean delete(String id) { return todos.remove(id)!=null; }
}

src/main/java/com/example/web/Csrf.java Download file

This helper creates a random anti-forgery token stored in the session. Forms include it as a hidden field. The filter compares the submitted value with the session value before any state-changing route runs. It is not a login token, and it is not JWT. A CSRF token distinguishes the session’s intended form submissions from forged cross-site submissions.

Complete file · src/main/java/com/example/web/Csrf.java
package com.example.web;

import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import jakarta.servlet.http.HttpSession;

public final class Csrf {
    private static final SecureRandom RANDOM=new SecureRandom();
    private Csrf() {}
    public static String token(HttpSession session) {
        synchronized(session) {
            String token=(String)session.getAttribute("csrfToken");
            if(token==null) {
                byte[] bytes=new byte[32]; RANDOM.nextBytes(bytes);
                token=Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
                session.setAttribute("csrfToken",token);
            }
            return token;
        }
    }
    public static boolean valid(String expected,String submitted) {
        return expected!=null && submitted!=null && MessageDigest.isEqual(
            expected.getBytes(StandardCharsets.UTF_8),submitted.getBytes(StandardCharsets.UTF_8));
    }
}

src/main/java/com/example/web/RequestFilter.java Download file

A Filter runs around the target servlet. UTF-8 is set before any form parameters are read. For /todos requests, it supplies a request-scoped csrf value; for POST it rejects missing or incorrect tokens with 403. chain.doFilter continues to the target resource. A return before that call prevents the servlet from running. Static assets and /hello are not made to create todo sessions.

Complete file · src/main/java/com/example/web/RequestFilter.java
package com.example.web;

import java.io.IOException;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.*;

@WebFilter("/*")
public class RequestFilter implements Filter {
    @Override public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
        throws IOException,ServletException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        HttpServletRequest req=(HttpServletRequest)request;
        HttpServletResponse res=(HttpServletResponse)response;
        if(req.getServletPath().equals("/todos")) {
            String expected=Csrf.token(req.getSession());
            req.setAttribute("csrf",expected);
            if(req.getMethod().equals("POST") && !Csrf.valid(expected,req.getParameter("csrf"))) {
                res.sendError(403,"Invalid form token. Reload the form and try again."); return;
            }
        }
        chain.doFilter(request,response);
    }
}

src/main/java/com/example/web/TodoServlet.java Download file

This is the controller in plain Servlet MVC. /todos/* lets getPathInfo identify /new, /edit, /create, and other subroutes. doGet forwards request attributes to a protected JSP; doPost validates, changes the session store, then redirects. The form helper preserves fields and sends status 400 for invalid input. getContextPath builds a redirect valid under the deployed WAR name. Visitor data stays in local variables and the session rather than servlet instance fields. Reset invalidates the current session and its tasks.

Complete file · src/main/java/com/example/web/TodoServlet.java
package com.example.web;

import java.io.IOException;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;

@WebServlet("/todos/*")
public class TodoServlet extends HttpServlet {
    private SessionTodoStore store(HttpServletRequest request) {
        HttpSession session=request.getSession();
        synchronized(session) {
            SessionTodoStore store=(SessionTodoStore)session.getAttribute("todoStore");
            if(store==null) { store=new SessionTodoStore(); session.setAttribute("todoStore",store); }
            return store;
        }
    }
    private String path(HttpServletRequest request) {
        return request.getPathInfo()==null ? "/" : request.getPathInfo();
    }
    private void form(HttpServletRequest req,HttpServletResponse res,String id,String title,
        boolean completed,String error) throws ServletException,IOException {
        req.setAttribute("id",id); req.setAttribute("title",title); req.setAttribute("completed",completed);
        req.setAttribute("error",error); req.setAttribute("formAction",id==null ? "/todos/create" : "/todos/update");
        req.getRequestDispatcher("/WEB-INF/views/form.jsp").forward(req,res);
    }
    private void redirect(HttpServletRequest req,HttpServletResponse res,String message) throws IOException {
        req.getSession().setAttribute("flash",message);
        res.sendRedirect(req.getContextPath()+"/todos");
    }
    @Override protected void doGet(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException {
        SessionTodoStore store=store(req);
        switch(path(req)) {
            case "/" -> {
                req.setAttribute("todos",store.list());
                HttpSession session=req.getSession();
                synchronized(session) {
                    Integer count=(Integer)session.getAttribute("listVisits");
                    session.setAttribute("listVisits",count==null ? 1 : count+1);
                    req.setAttribute("message",session.getAttribute("flash")); session.removeAttribute("flash");
                }
                req.getRequestDispatcher("/WEB-INF/views/list.jsp").forward(req,res);
            }
            case "/new" -> form(req,res,null,"",false,null);
            case "/edit" -> {
                Todo todo=store.find(req.getParameter("id")).orElse(null);
                if(todo==null) { res.sendError(404,"Todo not found"); return; }
                form(req,res,todo.getId(),todo.getTitle(),todo.isCompleted(),null);
            }
            default -> res.sendError(404,"Route not found");
        }
    }
    @Override protected void doPost(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException {
        String action=path(req);
        if(action.equals("/reset")) {
            req.getSession().invalidate(); res.sendRedirect(req.getContextPath()+"/todos"); return;
        }
        SessionTodoStore store=store(req);
        if(action.equals("/delete")) {
            if(!store.delete(req.getParameter("id"))) { res.sendError(404,"Todo not found"); return; }
            redirect(req,res,"Todo deleted"); return;
        }
        if(!action.equals("/create") && !action.equals("/update")) { res.sendError(404,"Route not found"); return; }
        String id=action.equals("/update") ? req.getParameter("id") : null;
        if(action.equals("/update") && store.find(id).isEmpty()) { res.sendError(404,"Todo not found"); return; }
        String title=req.getParameter("title"); title=title==null ? "" : title.strip();
        boolean completed="on".equals(req.getParameter("completed"));
        if(title.isBlank() || title.length()>120) {
            res.setStatus(400);
            form(req,res,id,title,completed,"Enter a title containing 1 to 120 characters."); return;
        }
        if(action.equals("/create")) store.create(title,completed);
        else if(store.update(id,title,completed).isEmpty()) { res.sendError(404,"Todo not found"); return; }
        redirect(req,res,action.equals("/create") ? "Todo created" : "Todo updated");
    }
}

src/main/webapp/index.html Download file

index.html is a static welcome page. The GET form sends name in the query string to HelloServlet. The name attribute determines the parameter key; id would only identify the DOM element. Relative URLs work because this page is served at the application context root.

Complete file · src/main/webapp/index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Java web basics</title><link rel="stylesheet" href="assets/style.css"></head>
<body><main><h1>Java web basics</h1><p>This page is static HTML served by Tomcat.</p>
<h2>Milestone 1: your first Servlet</h2><form action="hello" method="get"><label>Name<input name="name" maxlength="40" placeholder="Student"></label><button>Send GET request</button></form>
<h2>Milestone 2: Servlet + JSP todo list</h2><p><a href="todos">Open your session’s todos</a></p>
<p>This beginner lab keeps tasks in the current browser session. Later manuals introduce database persistence and authenticated users.</p></main></body></html>

src/main/webapp/WEB-INF/web.xml Download file

web.xml supplies deployment settings while annotations supply servlet/filter mappings. The welcome file serves index.html at the context root. Sessions have a 20-minute inactivity timeout, use cookie tracking, and mark the session cookie HttpOnly. Local development uses HTTP; deployed applications should use HTTPS and an appropriate Secure cookie configuration. Do not set metadata-complete=true here because that would disable annotation discovery.

Complete file · src/main/webapp/WEB-INF/web.xml
<?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_0.xsd" version="6.0">
  <display-name>Servlet JSP training</display-name>
  <welcome-file-list><welcome-file>index.html</welcome-file></welcome-file-list>
  <session-config><session-timeout>20</session-timeout><cookie-config><http-only>true</http-only></cookie-config><tracking-mode>COOKIE</tracking-mode></session-config>
</web-app>

src/main/webapp/WEB-INF/views/list.jsp Download file

JSP is the view. Directives select UTF-8 and the Jakarta JSTL core library. c:forEach loops over the request’s todos snapshot; c:out escapes user text; c:choose renders status. c:url adds the context path and c:param encodes a query value. Delete and Reset are POST forms with hidden anti-forgery fields. sessionScope makes the visit-count scope explicit.

Complete file · src/main/webapp/WEB-INF/views/list.jsp
<%@ 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>Session todos</title>
<c:url var="cssUrl" value="/assets/style.css"/><link rel="stylesheet" href="${cssUrl}"></head>
<body><main><h1>Session todos</h1>
<p>List visits in this session: <c:out value="${sessionScope.listVisits}"/></p>
<c:if test="${not empty message}"><p role="status"><c:out value="${message}"/></p></c:if>
<c:url var="newUrl" value="/todos/new"/><a href="${newUrl}">Add a todo</a>
<c:if test="${empty todos}"><p>No todos yet.</p></c:if>
<c:if test="${not empty todos}"><table><thead><tr><th>Title</th><th>Status</th><th>Actions</th></tr></thead><tbody>
<c:forEach items="${todos}" var="todo"><tr>
<td><c:out value="${todo.title}"/></td><td><c:choose><c:when test="${todo.completed}">Done</c:when><c:otherwise>Pending</c:otherwise></c:choose></td>
<td><c:url var="editUrl" value="/todos/edit"><c:param name="id" value="${todo.id}"/></c:url><a href="${editUrl}">Edit</a>
<c:url var="deleteUrl" value="/todos/delete"/><form action="${deleteUrl}" method="post" class="inline">
<input type="hidden" name="csrf" value="<c:out value='${csrf}'/>"><input type="hidden" name="id" value="<c:out value='${todo.id}'/>"><button>Delete</button></form></td>
</tr></c:forEach></tbody></table></c:if>
<c:url var="resetUrl" value="/todos/reset"/><form action="${resetUrl}" method="post"><input type="hidden" name="csrf" value="<c:out value='${csrf}'/>"><button>Reset this session</button></form>
<p>Tasks belong to this browser session, with no login or database in this introductory lab.</p>
</main></body></html>

src/main/webapp/WEB-INF/views/form.jsp Download file

EL expressions read values prepared by the servlet; c:out safely places the title and token into quoted HTML attributes. c:if includes id only when editing and checked only for a completed task. An unchecked checkbox sends no completed parameter, which the servlet converts to false. The same JSP supports create and edit, including re-rendering an invalid submission with the entered values retained.

Complete file · src/main/webapp/WEB-INF/views/form.jsp
<%@ 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>Todo form</title>
<c:url var="cssUrl" value="/assets/style.css"/><link rel="stylesheet" href="${cssUrl}"></head>
<body><main><h1><c:choose><c:when test="${empty id}">Add a todo</c:when><c:otherwise>Edit a todo</c:otherwise></c:choose></h1>
<c:if test="${not empty error}"><p class="error" role="alert"><c:out value="${error}"/></p></c:if>
<c:url var="saveUrl" value="${formAction}"/><form action="${saveUrl}" method="post">
<input type="hidden" name="csrf" value="<c:out value='${csrf}'/>">
<c:if test="${not empty id}"><input type="hidden" name="id" value="<c:out value='${id}'/>"></c:if>
<label for="title">Title</label><input id="title" name="title" value="<c:out value='${title}'/>" maxlength="120" required>
<label><input type="checkbox" name="completed" <c:if test="${completed}">checked</c:if>> Completed</label>
<button>Save todo</button></form><c:url var="backUrl" value="/todos"/><a href="${backUrl}">Back to list</a>
</main></body></html>

src/main/webapp/assets/style.css Download file

Static CSS belongs in the public webapp directory and is served by Tomcat. WEB-INF is for protected configuration and views, not assets the browser must fetch directly.

Complete file · src/main/webapp/assets/style.css
body{font-family:system-ui,sans-serif;background:#f2f6f3;color:#17352a;margin:0}main{max-width:850px;margin:40px auto;padding:32px;background:white;border-radius:12px}input,button{font:inherit;padding:9px;margin:6px}input:not([type=checkbox]):not([type=hidden]){display:block;width:min(90%,450px)}button{cursor:pointer;background:#176746;color:white;border:0;border-radius:5px}a{color:#176746}table{width:100%;border-collapse:collapse;margin-top:24px}td,th{text-align:left;padding:12px;border-bottom:1px solid #ddd}.inline{display:inline}.error{color:#ae2525}

src/test/java/com/example/web/SessionTodoStoreTest.java Download file

These tests verify stable ids, safe list snapshots, missing-update behavior, and independent stores. They do not require a running server. The deployment checklist separately tests Servlet routes and actual JSP rendering.

Complete file · src/test/java/com/example/web/SessionTodoStoreTest.java
package com.example.web;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SessionTodoStoreTest {
    @Test void crudKeepsIdAndSnapshotsStable() {
        SessionTodoStore store=new SessionTodoStore();
        Todo original=store.create("Learn Servlets",false);
        var snapshot=store.list();
        Todo updated=store.update(original.getId(),"Learn JSP",true).orElseThrow();
        assertEquals(original.getId(),updated.getId()); assertTrue(updated.isCompleted());
        assertEquals("Learn Servlets",snapshot.get(0).getTitle());
        assertTrue(store.delete(original.getId())); assertTrue(store.find(original.getId()).isEmpty());
    }
    @Test void missingUpdateCannotCreateAndStoresAreIndependent() {
        SessionTodoStore alice=new SessionTodoStore(),bob=new SessionTodoStore();
        Todo task=alice.create("Alice task",false);
        assertTrue(bob.find(task.getId()).isEmpty());
        assertTrue(bob.update(task.getId(),"Changed",true).isEmpty());
        assertTrue(bob.list().isEmpty()); assertEquals("Alice task",alice.find(task.getId()).orElseThrow().getTitle());
    }
}

src/test/java/com/example/web/CsrfTest.java Download file

The token comparison test checks accepted, mismatched, and missing values. The live HTTP test verifies that the filter actually rejects a forged request before CRUD runs.

Complete file · src/test/java/com/example/web/CsrfTest.java
package com.example.web;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CsrfTest {
    @Test void comparisonRejectsMissingAndDifferentTokens() {
        assertTrue(Csrf.valid("secret","secret"));
        assertFalse(Csrf.valid("secret","other"));
        assertFalse(Csrf.valid("secret",null));
        assertFalse(Csrf.valid(null,"secret"));
    }
}

.gitignore Download file

Generated artifacts and local secret files stay out of source control. The project contains no database or JWT credentials.

Complete file · .gitignore
target/
*.log
.env

9. Build, deploy, and run — from download to a working page

Download & extract → Open project → Maven build → Copy WAR to Tomcat → Open browser

Installing Java, Maven, and Tomcat prepares your computer. Your application becomes available only after you build it and deploy its WAR to the running Tomcat. Follow the checkpoints below in order.

9.1. Download and open the correct project folder

  1. Download the Servlet/JSP project ZIP (click here). Your browser normally saves it in Downloads; a blank browser tab at the ZIP address is not the project editor. Check your browser's Downloads list.
  2. In File Explorer, right-click the downloaded ZIP → Extract All. Open the extracted folders until you see pom.xml and src together. Work in this folder, not inside the ZIP and not in src or target.
  3. In VS Code, choose File → Open Folder and select that folder. Choose Terminal → New Terminal. Check the shell name in its terminal dropdown before copying commands.

9.2. Choose commands for your terminal

TerminalTypical promptVariable / copy / server script
Windows Command Prompt (CMD)C:\Users\Student\todo-servlet-jsp>%CATALINA_HOME% · copy · catalina.bat
Windows PowerShellPS C:\Users\Student\todo-servlet-jsp>$env:CATALINA_HOME · Copy-Item · & 'path\catalina.bat'
macOS/Linux Bash or ZshUsually ends with $ or %$CATALINA_HOME · cp · catalina.sh
Copy one platform's commands only. cp, export, and bash ...catalina.sh are not Windows CMD commands. Do not type the prompt (such as C:\...>) itself. In VS Code, choose Command Prompt from the terminal dropdown if you want to follow the CMD examples.

9.3. Build and find the WAR

From the project folder, run these commands in any of the three terminals:

Project terminal · all platforms
java -version
javac -version
mvn -version
mvn clean package

The project targets Java 21. The tutorial uses JDK 21 for consistent results; if you have another JDK, inspect Maven's Java version before proceeding. If a command is missing or JAVA_HOME is incorrect, use the JDK guide or Maven guide in the third column.

Expect BUILD SUCCESS, passing tests, and a file named target/todo-servlet-jsp.war. A WAR is the packaged web application. Maven does not copy it to Tomcat or start a server just because the build succeeds.

Windows CMD · check the project and built file
dir pom.xml
dir target\todo-servlet-jsp.war

If there is no pom.xml, change to the correct extracted folder. In CMD, cd /d "C:\your actual folder\todo-servlet-jsp" changes drive and directory; replace the example path. If target contains no WAR, read the first build error and fix it before deploying. Do not copy the whole project, the ZIP, or an empty target folder into Tomcat.

9.4. Simplest Windows method: use File Explorer

  1. Find your actual Tomcat installation folder. A Windows installer often uses C:\Program Files\Apache Software Foundation\Tomcat 10.1; a ZIP installation uses the folder where you extracted it. The correct folder contains bin, conf, logs, and webapps. Open the Tomcat guide in the third column for installation and port setup.
  2. Copy target/todo-servlet-jsp.war from your project. Paste that file directly into this Tomcat's webapps folder. Approve Windows' administrator prompt if required for Program Files.
  3. Confirm that webapps/todo-servlet-jsp.war exists. With default unpacking enabled, Tomcat also creates a webapps/todo-servlet-jsp directory during deployment.
  4. If Tomcat is already running, do not start another instance. With default auto-deployment enabled it picks up a new WAR; wait for the deployment log and then refresh the application. If automatic deployment is disabled, restart that Tomcat after copying.
  5. If Tomcat is stopped and installed as a Windows service, press Win + R, enter services.msc, find your Apache Tomcat service and choose Start. If you use an extracted ZIP instead, open its bin folder and run startup.bat, or use the foreground command below to see startup errors. Choose one startup method.

Choose the right port: Tomcat normally uses 8080. This manual's lab URLs use 8083 after changing the HTTP Connector in conf/server.xml. If you kept the default, use 8080 in the browser instead. Copying a WAR or setting CATALINA_HOME does not change the port. Restart the same Tomcat after a port change.

9.5. Windows CMD alternative

These are examples: replace both installation paths with directories that exist on your computer. JAVA_HOME is the JDK root; CATALINA_HOME is the Tomcat root. Neither value ends in bin. Start in the project folder containing pom.xml.

Windows Command Prompt · verify paths and build
set "JAVA_HOME=C:\Program Files\Java\jdk-21"
set "CATALINA_HOME=C:\Program Files\Apache Software Foundation\Tomcat 10.1"
echo %JAVA_HOME%
echo %CATALINA_HOME%
dir "%JAVA_HOME%\bin\javac.exe"
dir "%CATALINA_HOME%\webapps"
mvn -version
mvn clean package
dir "target\todo-servlet-jsp.war"

Checkpoint: Both directories must exist and Maven must finish successfully. Then copy the built WAR:

Windows Command Prompt · deploy the WAR
copy /Y "target\todo-servlet-jsp.war" "%CATALINA_HOME%\webapps\todo-servlet-jsp.war"
dir "%CATALINA_HOME%\webapps\todo-servlet-jsp.war"

If Tomcat is stopped and the installation includes catalina.bat, start it in the foreground:

Windows Command Prompt · start only if Tomcat is stopped
"%CATALINA_HOME%\bin\catalina.bat" run

Keep this terminal open; use Ctrl+C to stop the foreground instance. A Windows service runs independently of this terminal. If the copy reports Access is denied, use File Explorer's administrator prompt, or an administrator CMD for the deployment operation. If catalina.bat is absent, use your installed Tomcat service rather than a Linux shell script.

9.6. Windows PowerShell alternative

Use this block only in PowerShell, with your actual installation paths and the project as the current directory:

Windows PowerShell · verify, build, and copy
$env:JAVA_HOME = 'C:\Program Files\Java\jdk-21'
$env:CATALINA_HOME = 'C:\Program Files\Apache Software Foundation\Tomcat 10.1'
Test-Path "$env:JAVA_HOME\bin\javac.exe"
Test-Path "$env:CATALINA_HOME\webapps"
mvn -version
mvn clean package
Get-Item '.\target\todo-servlet-jsp.war'
Copy-Item -LiteralPath '.\target\todo-servlet-jsp.war' -Destination "$env:CATALINA_HOME\webapps\todo-servlet-jsp.war" -Force

The two Test-Path checks must return True. After a successful copy, start Tomcat only if it is stopped:

Windows PowerShell · foreground startup
& "$env:CATALINA_HOME\bin\catalina.bat" run

9.7. macOS / Linux alternative

MacBook users: first follow the complete macOS setup guide in the third column for actual path discovery, persistent variables, and Mac-specific errors.

Use your actual JDK, extracted Tomcat, and project paths. These commands belong in Bash/Zsh, not Windows CMD. Stop a previous foreground Tomcat before replacing an existing deployed application.

macOS / Linux · Bash or Zsh
export JAVA_HOME='/path/to/your/jdk-21'
export CATALINA_HOME="$HOME/tools/apache-tomcat-10.1.60"
cd /path/to/your/todo-servlet-jsp
mvn -version
mvn clean package
cp target/todo-servlet-jsp.war "$CATALINA_HOME/webapps/todo-servlet-jsp.war"

If Tomcat is stopped, start it with:

macOS / Linux · foreground startup
bash "$CATALINA_HOME/bin/catalina.sh" run

9.8. Check each page and its expected output

CheckDefault port 8080Lab port 8083Expected output
ServerTomcat rootTomcat rootTomcat responds. This alone does not prove your application deployed.
WelcomeOpen welcomeOpen welcomeJava web basics; a name field; Send GET request; link to session todos.
ServletOpen greetingOpen greetingHello, Student! This response came from a Java Servlet.
JSP todo listOpen todosOpen todosThe Session todos page, initially empty for a new session; controls to add and manage tasks.

Try adding “Learn Servlets”, editing it, marking it completed, and deleting it. The URL must include /todo-servlet-jsp: /hello at the server root is a different URL. Do not open index.html or JSP files directly from disk; use the HTTP URLs above.

9.9. After changing application code

Save your source, run mvn clean package, and copy the new WAR to the same Tomcat. For a predictable replacement, stop the instance that owns the application, replace the WAR, then restart that same instance. Do not edit Tomcat's generated application directory as your source. Refresh the browser and repeat the checks. Re-deployment can reset this introductory app's session data. A Java build alone does not validate JSP rendering, so always open the todo page too.

If you see an error, follow the symptom-specific checklist in Troubleshooting. Reference: Tomcat deployment documentation.

10. Milestone 1: understand your first Servlet

Visit /todo-servlet-jsp/hello?name=Priya. Tomcat identifies the deployed app, matches @WebServlet("/hello"), and invokes HttpServlet.service(), which dispatches a GET request to doGet. Your code reads name from the request and writes a plain-text response.

Try omitting name: the default is Student. Try a name with spaces: the browser encodes it in the URL and Tomcat decodes the parameter. Try more than 40 characters using a manually edited URL: the server returns 400 even if the HTML form normally stops that input.

Tomcat controls the Servlet lifecycle: it constructs an instance, calls init once for initialization, invokes service for requests, and calls destroy when removing it. Overriding doGet/doPost is enough here because HttpServlet supplies the dispatching logic. Do not call doGet manually to “start” a web app.

A Servlet instance is commonly reused for concurrent requests. Local variables such as name belong to one method call. Mutable instance fields such as currentUser or currentTitle would be shared and could leak data between visitors. Store appropriate visitor state in the session or storage layer instead.

Servlet itemRole
@WebServletRegisters a URL mapping.
HttpServletRequestIncoming parameters, headers, path information, attributes, and session access.
HttpServletResponseOutgoing status, headers, content type, body, or redirect.
doGet / doPostHandlers for the HTTP method.
getWriterWrites character data into the response body.
sendErrorSends an error status; return afterwards so normal processing does not continue.

11. Parameters are different from attributes

A parameter is client input: a query value or submitted form field. getParameter("title") returns a String or null. The client chooses it, so validate it. Repeated names can be read with getParameterValues. This form app does not parse a JSON body; that is a later REST lesson.

An attribute is a server-side object placed in a request, session, or application context. request.setAttribute("todos", store.list()) prepares a Java list for the JSP. The browser does not submit that list, and the list does not become a URL query string.

Parameters and attributes · explanatory excerpt
String title = request.getParameter("title");
request.setAttribute("todos", store.list());
request.getRequestDispatcher("/WEB-INF/views/list.jsp")
       .forward(request, response);

HTML input name="title" corresponds to the request parameter title. The JSP expression ${todos} reads the attribute prepared by the Servlet. Keeping those two directions separate helps explain later Spring form binding and Model attributes.

12. Forward versus redirect

How request data reaches the JSP (click here) — open the third column for a step-by-step explanation of setAttribute(), getRequestDispatcher(), forward(), and JSP output.

A forward is a server-side handoff within the current request. Request attributes remain available to the JSP, and the browser’s URL stays on the controller route. The browser did not make another request to the protected JSP.

A redirect sends a response with a new destination. The browser then makes a separate request. Request attributes from the original POST are gone. If you need a one-time message after redirect, put it in the session and remove it after reading, as this app does with flash.

ForwardRedirect
Same request and request attributes.New browser request; old request attributes are unavailable.
Used for rendering a view or validation failure.Used after a successful data-changing submission.
URL remains the controller route.Browser changes to the redirect target.
getRequestDispatcher(...).forward(req,res)sendRedirect(contextPath + "/todos")

The successful form flow is POST → change task → 302 → GET list. This is Post/Redirect/Get. Refreshing the list repeats GET rather than the successful task creation. An invalid submission instead sends 400 and forwards to the form so its fields and error message remain available.

Do not write a response body and then try to forward or redirect after the response has already been committed. Choose one response strategy for each path.

13. JSP, Expression Language, and JSTL

JSP is a server-side view technology. Tomcat’s Jasper translates a JSP into a Servlet, compiles it, and uses it to generate the response. A JSP contains mostly HTML plus view expressions and tag actions. It is useful for understanding server rendering and maintaining existing Servlet/JSP applications. It is optional in the later Spring labs, which use Thymeleaf or React.

The page directive controls content type and source encoding. The taglib directive imports JSTL core tags. Expression Language, abbreviated EL, reads model properties; ${todo.title} resolves the current todo’s getTitle method. Use getters with JavaBean-style names.

JSP view concepts
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>

<c:forEach items="${todos}" var="todo">
  <p><c:out value="${todo.title}"/></p>
</c:forEach>

JSTL means Jakarta Standard Tag Library. c:forEach repeats markup, c:if selects optional content, c:choose/c:when/c:otherwise provide branches, c:url constructs application-aware links, c:param adds encoded query values, and c:out outputs escaped text.

EL output such as ${todo.title} is not automatically HTML-escaped merely because it is EL. Use c:out for user-provided values, including values inside quoted attributes. This app uses c:out for titles, messages, tokens, and ids. The browser should show literal angle brackets rather than executing an entered HTML tag.

Older tutorials sometimes embed Java code in JSP scriptlets, written <% ... %>. Know that syntax for reading old code, but keep application logic in Servlets and Java classes here. Do not place store mutation, passwords, database calls, or business rules inside a JSP.

WEB-INF/views protects the JSPs from direct client requests. The controller can forward there, but a direct URL to /WEB-INF/views/list.jsp should return 404. Open the application through /todos so the controller supplies the required data.

14. Scopes, cookies, and sessions

HTTP itself does not remember that two requests belong to the same visitor. A cookie is a value stored by the browser and sent back on matching requests. A server-side HttpSession holds Java attributes; Tomcat uses a JSESSIONID cookie to find the right session. The cookie contains an identifier, not the todo list itself.

ScopeLifetime and sharingAppropriate example
PageOne JSP rendering context.A local value used during a JSP view.
RequestOne request, including its forwards.todos, error, title, and the prepared csrf field.
SessionRequests associated with one session until timeout/invalidation.todoStore, listVisits, flash, and csrfToken.
Application / ServletContextShared by the web application.Shared configuration; not a visitor’s private task list.

getSession() returns the existing session or creates one. getSession(false) only looks for an existing session. session.setAttribute stores an object; getAttribute retrieves it; invalidate ends it. This app shows a list-visit counter and a Reset button that invalidates the session.

Open another browser profile or an incognito window to test independent sessions. Two ordinary tabs in the same profile generally share cookies and therefore share the same session todo list. Closing a browser is not a reliable programmatic logout demonstration because browsers may restore sessions.

Session separation here is not user-account authentication. There is no password, login, or guaranteed long-term ownership. Data is not stored in a database. A container may serialize sessions across a restart, so do not promise that restarting always clears them or reliably preserves them. Reset explicitly clears this lab’s session.

HttpOnly restricts script access to the session cookie; HTTPS and Secure cookie settings are separate deployment concerns. Session timeout is 20 minutes of inactivity, not a fixed lifetime from creation.

15. Filters, encoding, validation, and form safety

A Filter can inspect or modify a request/response and then call chain.doFilter to continue to the Servlet. This app’s RequestFilter sets UTF-8 before reading form parameters. That matters because calling getParameter first may decode the body using the previous character-encoding setting.

The filter also manages a session-scoped CSRF token for todo forms. GET renders the token in a hidden field. POST must include the same token. Missing or invalid tokens produce 403 before any state change. This is a small example implementation; Spring Security later supplies established form protection.

Server validation strips surrounding whitespace and requires 1–120 title characters. The completed checkbox becomes true only when its submitted value is on. Unknown task ids return 404. The view escapes text so a title cannot become executable markup.

These rules have distinct purposes: validation checks acceptable input; output escaping controls how text is interpreted in HTML; CSRF checks intended session form submission. None of them is account authentication. Make sure you can explain these differences before starting the JWT tutorial.

16. Follow every todo route

Method / route inside contextOperationResponse
GET /todosList current session’s tasks.Forward to list.jsp.
GET /todos/newPrepare empty input values.Forward to form.jsp.
GET /todos/edit?id=...Load current session’s task.Forward to form.jsp; 404 if unavailable.
POST /todos/createValidate and add.302 then GET list; 400 form on invalid input.
POST /todos/updateValidate and replace task values.302 then GET list; 404 if missing.
POST /todos/deleteRemove task by submitted id.302 then GET list; 404 if missing.
POST /todos/resetInvalidate current session.302 then fresh GET list.

Trace create step by step: browser submits named form fields → Tomcat identifies the app and Servlet → RequestFilter checks encoding/token → TodoServlet.doPost reads and validates values → SessionTodoStore creates a UUID task → response redirects to /todos → the browser sends a new GET → controller supplies a snapshot → JSP renders escaped HTML.

To explain routing precisely, distinguish getContextPath (/todo-servlet-jsp), getServletPath (/todos), and getPathInfo (/create). A Servlet mapping ending in /* makes subroute information available through getPathInfo. @WebServlet is Servlet registration, not Spring’s @Controller.

17. Browser and unit-test acceptance checklist

  1. Run mvn clean package. Confirm the three unit tests pass and the WAR is generated.
  2. Deploy on Tomcat, visit the welcome page, submit the name form, and observe the plain-text greeting.
  3. Open /todos. Expect an empty list and a visit counter.
  4. Add “Learn Servlets”; expect Pending and a success message.
  5. Edit it to “Learn JSP” and check Completed; expect the same id with Done.
  6. Submit a whitespace-only title. Expect 400 and the form error; the original task remains unchanged.
  7. Create a title containing <script>alert(1)</script> or <b>test</b>. Expect literal text with no script execution or bold formatting from the title.
  8. Inspect the form request in Network. Verify title, optional completed, id when editing, and csrf parameters.
  9. Refresh after a successful create. Expect no duplicate insertion.
  10. Open the list in a separate incognito/profile session. Expect an independent empty list. A second tab in the same profile should show the original session’s tasks.
  11. Try /todos/edit?id=missing. Expect 404. Request /WEB-INF/views/list.jsp directly. Expect 404.
  12. Submit a POST without the csrf field using a terminal request. Expect 403 and no change.
  13. Delete a task, then use its previous edit URL. Expect 404.
  14. Use Reset this session. Expect an empty list and a fresh counter.
Windows terminal · expect 403 because csrf is absent
curl.exe -i -X POST http://localhost:8083/todo-servlet-jsp/todos/create -d "title=Forged&completed=on"

On Linux/macOS, use curl without .exe. The supplied JUnit tests cover the store and token comparison; the browser checklist exercises actual routing, JSP compilation, sessions, and filtering.

18. Troubleshooting

First-run errors: identify the failed step

SymptomMeaning and next action
CMD: cp or bash is not recognizedYou copied macOS/Linux commands into Windows CMD. Use the CMD block: copy, %CATALINA_HOME%, and catalina.bat. Installing Bash is not necessary for this Windows workflow.
mvn is not recognized after setting MAVEN_HOMEAdd Maven's actual bin directory to your user Path and restart the terminal/IDE. MAVEN_HOME alone does not add a command to Path.
Java works but JAVA_HOME is blankThe java executable is being found through Path. Set JAVA_HOME to the JDK root. Verify bin/javac.exe and Maven's reported runtime; a bin directory is not the JDK root.
setx reports success but echo shows an old valuesetx changes future terminals. Open a new CMD or use set for the current CMD. Restart VS Code when necessary.
The system cannot find the path specifiedEcho CATALINA_HOME in the correct shell and use dir on its bin folder. Replace example paths with real folders. A quoted path with spaces needs the correct CMD/PowerShell invocation syntax.
Access is denied under Program FilesUse File Explorer's administrator prompt or an administrator terminal for the copy/configuration operation. For a new ZIP installation, a writable user folder avoids Program Files permissions; do not change broad filesystem permissions just to copy a WAR.
BindException: Address already in use / failed Connector http-nio-8080The HTTP port is occupied. Another Tomcat service may already own it. Stop the extra foreground instance with Ctrl+C, then use the running instance. Do not terminate an unidentified process. If another application needs the port, choose an available Connector port and restart Tomcat.
Native-access warning or optional Tomcat Native library messageThese lines alone do not prove startup failed. Find the first SEVERE/Exception and inspect its cause. A later “Server startup” line does not cancel an earlier HTTP Connector failure; verify the actual HTTP endpoint.
Browser cannot connectCheck that Tomcat is running and the URL uses its configured HTTP port. This differs from an HTTP 404 page, which proves an HTTP server answered.
Tomcat-branded 404 at /todo-servlet-jsp/Check for webapps/todo-servlet-jsp.war in the running instance's CATALINA_BASE. A WAR still in the project's target folder has not been deployed. If present, wait for deployment, then inspect logs for deployment errors. Confirm the WAR name and URL match; do not assume every 404 has the same cause.
Welcome works, but /hello or /todos failsCheck the full context path, deployment logs, route mappings, and JSP errors. Copy the complete new WAR, not selected source files.
Port 8080 works but the tutorial's 8083 link failsYou retained the default port. Use 8080 consistently, or configure and restart this instance on 8083. Environment variables do not select the HTTP port.
WAR was copied, but another Tomcat is serving requestsRead CATALINA_BASE from that server's startup log. Deploy to its webapps directory (with the default appBase), not another Tomcat installation.

Inspect the port and logs without stopping unknown processes

In Windows CMD, use these read-only commands for the port you selected. The last column of netstat output is the process ID; identify it before deciding what to stop.

Windows CMD · identify who is listening
netstat -ano | findstr LISTENING | findstr :8080
tasklist /FI "IMAGENAME eq Tomcat10.exe"
tasklist /FI "IMAGENAME eq java.exe"

For a specific PID from netstat, run tasklist /FI "PID eq 12345", replacing 12345 with that PID. Check the Tomcat logs folder: catalina and localhost logs can explain deployment/Servlet failures; an access log records requests and response codes. In a foreground launch, also read the terminal. Record the failing URL, selected port, terminal type, and first relevant exception when asking for help.

SymptomLikely cause and correction
java / mvn not foundInstall the tools, add their bin directories to PATH, and open a new terminal. Verify JAVA_HOME and mvn -version.
release version 21 not supportedMaven is running with a JDK older than the project target. Correct its launch environment.
Tomcat cannot start / port in useStop the other process or change the HTTP Connector port. Use the new port in the browser.
404 at /helloInclude /todo-servlet-jsp before /hello and verify the WAR deployed.
Servlet routes all 404Check annotation imports, mappings, and server logs. Do not disable annotation scanning with metadata-complete=true.
javax / jakarta class errorUse the complete Jakarta project with Tomcat 10.1; do not mix Tomcat 9 Servlet imports.
JSTL URI cannot be resolvedInclude both Jakarta JSTL dependencies and use jakarta.tags.core. Rebuild and redeploy the whole WAR.
JSP compilation / property errorInspect the Tomcat error and line number. Check tag syntax, public getters, and the exact file path under WEB-INF/views.
403 on form submitUse the form generated for this session. Reset/expired sessions invalidate an old token; reload the form.
Accented text is wrongKeep UTF-8 source files, page directives, and the filter’s encoding before parameters are read.
Old behavior after Java editRebuild the WAR, copy it to the intended Tomcat instance, restart, and inspect the deployment log.
Cannot find saved tasks laterThis lab is session storage without database persistence. The Spring/Atlas manuals introduce durable storage.
No content / wrong attributesUse the controller URL so it supplies request attributes, rather than trying to visit a JSP directly.

19. Practice and check understanding

  1. Change the greeting to include the request method. Explain why sending POST to a GET-only Servlet does not call doGet.
  2. Add a due-date form field. Trace it from input name to request parameter, validated Java value, object getter, and escaped JSP output.
  3. Add a completed-task count using a request attribute. Explain why the count belongs in the rendered request rather than a shared Servlet field.
  4. Show the difference between two tabs and an incognito window for session storage.
  5. Explain the difference between a missing parameter, a request attribute, a session attribute, and a cookie.
  6. Draw the successful POST/Redirect/GET flow and the invalid-form forward flow.
  7. Explain why JSP lives under WEB-INF and why c:out is used for a title.

Answer notes: HttpServlet dispatches by HTTP method; form names become parameter keys; request data survives a forward but not a redirect; session identity comes from its cookie; a reused Servlet instance must not hold per-user state; JSP is compiled on the server; c:out escapes user text.

20. Make the transition to Spring foundations

Plain Servlet/JSP labLater Spring approach
Tomcat started separately; deploy a WAR.Boot starts an embedded server and can run an executable JAR.
@WebServlet + doGet/doPost + manual route checks.@Controller / @RestController + mapping annotations.
getParameter and manual input conversion.@ModelAttribute, @RequestParam, @RequestBody, and form/DTO binding.
Manual validation and error attributes.Bean Validation with @Valid and BindingResult or API advice.
request.setAttribute + forward to JSP.Model attributes + a view name resolved to Thymeleaf.
SessionTodoStore directly constructed.Constructor-injected services and repositories.
Session-only task storage.MongoDB Atlas document persistence.
Small custom form-token filter.Spring Security’s form and bearer-token security support.

Spring does not remove HTTP or the Servlet request/response model. It supplies abstractions for routing, binding, validation, view resolution, object wiring, and configuration. Now you have seen the work those abstractions replace.

You are ready to continue when you can deploy the WAR, complete CRUD, explain a request’s lifecycle, and distinguish parameters, attributes, sessions, forwards, and redirects.

Next: Spring foundations →
Added 11 October 2026 · Java 21 target · Tomcat 10.1.60 · Jakarta Servlet 6.0 / JSP 3.1
Complete files are supplied. Session storage is an introductory step before the database-backed labs.