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.
Understand HTML, HTTP requests, and browser/server roles.
Install the tools and build a Maven WAR project.
Run your first Servlet, then examine its request and response.
Use JSP, Expression Language, and JSTL to render a todo list.
Understand sessions, request attributes, filters, validation, and redirect behavior.
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.
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.
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.
Part
Meaning in this lab
GET
Read a page or greeting; do not delete or modify a task with GET.
POST
Submit a task change or reset the session.
Request headers
Metadata such as Cookie and Content-Type.
Request body
POST form fields; here they are application/x-www-form-urlencoded.
Content-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.
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.
Extract it and open the hello-servlet folder containing pom.xml.
Run mvn clean package. Copy target/hello-servlet.war into your Tomcat webapps folder.
Use the already running Tomcat, or start it if stopped. Open localhost:8080/hello-servlet/ (use 8083 if configured).
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.
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.
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.
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.
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.
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.
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; }
}
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.
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.
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.
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.
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>
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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:
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
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.
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.
Confirm that webapps/todo-servlet-jsp.war exists. With default unpacking enabled, Tomcat also creates a webapps/todo-servlet-jsp directory during deployment.
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.
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:
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.
The 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.
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 item
Role
@WebServlet
Registers a URL mapping.
HttpServletRequest
Incoming parameters, headers, path information, attributes, and session access.
HttpServletResponse
Outgoing status, headers, content type, body, or redirect.
doGet / doPost
Handlers for the HTTP method.
getWriter
Writes character data into the response body.
sendError
Sends 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.
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.
Forward
Redirect
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.
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.
Scope
Lifetime and sharing
Appropriate example
Page
One JSP rendering context.
A local value used during a JSP view.
Request
One request, including its forwards.
todos, error, title, and the prepared csrf field.
Session
Requests associated with one session until timeout/invalidation.
todoStore, listVisits, flash, and csrfToken.
Application / ServletContext
Shared 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 context
Operation
Response
GET /todos
List current session’s tasks.
Forward to list.jsp.
GET /todos/new
Prepare 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/create
Validate and add.
302 then GET list; 400 form on invalid input.
POST /todos/update
Validate and replace task values.
302 then GET list; 404 if missing.
POST /todos/delete
Remove task by submitted id.
302 then GET list; 404 if missing.
POST /todos/reset
Invalidate 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
Run mvn clean package. Confirm the three unit tests pass and the WAR is generated.
Deploy on Tomcat, visit the welcome page, submit the name form, and observe the plain-text greeting.
Open /todos. Expect an empty list and a visit counter.
Add “Learn Servlets”; expect Pending and a success message.
Edit it to “Learn JSP” and check Completed; expect the same id with Done.
Submit a whitespace-only title. Expect 400 and the form error; the original task remains unchanged.
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.
Inspect the form request in Network. Verify title, optional completed, id when editing, and csrf parameters.
Refresh after a successful create. Expect no duplicate insertion.
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.
Submit a POST without the csrf field using a terminal request. Expect 403 and no change.
Delete a task, then use its previous edit URL. Expect 404.
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
Symptom
Meaning and next action
CMD: cp or bash is not recognized
You 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_HOME
Add 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 blank
The 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 value
setx 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 specified
Echo 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 Files
Use 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-8080
The 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 message
These 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 connect
Check 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 fails
Check 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 fails
You 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 requests
Read 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.
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.
Symptom
Likely cause and correction
java / mvn not found
Install the tools, add their bin directories to PATH, and open a new terminal. Verify JAVA_HOME and mvn -version.
release version 21 not supported
Maven is running with a JDK older than the project target. Correct its launch environment.
Tomcat cannot start / port in use
Stop the other process or change the HTTP Connector port. Use the new port in the browser.
404 at /hello
Include /todo-servlet-jsp before /hello and verify the WAR deployed.
Servlet routes all 404
Check annotation imports, mappings, and server logs. Do not disable annotation scanning with metadata-complete=true.
javax / jakarta class error
Use the complete Jakarta project with Tomcat 10.1; do not mix Tomcat 9 Servlet imports.
JSTL URI cannot be resolved
Include both Jakarta JSTL dependencies and use jakarta.tags.core. Rebuild and redeploy the whole WAR.
JSP compilation / property error
Inspect the Tomcat error and line number. Check tag syntax, public getters, and the exact file path under WEB-INF/views.
403 on form submit
Use the form generated for this session. Reset/expired sessions invalidate an old token; reload the form.
Accented text is wrong
Keep UTF-8 source files, page directives, and the filter’s encoding before parameters are read.
Old behavior after Java edit
Rebuild the WAR, copy it to the intended Tomcat instance, restart, and inspect the deployment log.
Cannot find saved tasks later
This lab is session storage without database persistence. The Spring/Atlas manuals introduce durable storage.
No content / wrong attributes
Use the controller URL so it supplies request attributes, rather than trying to visit a JSP directly.
19. Practice and check understanding
Change the greeting to include the request method. Explain why sending POST to a GET-only Servlet does not call doGet.
Add a due-date form field. Trace it from input name to request parameter, validated Java value, object getter, and escaped JSP output.
Add a completed-task count using a request attribute. Explain why the count belongs in the rendered request rather than a shared Servlet field.
Show the difference between two tabs and an incognito window for session storage.
Explain the difference between a missing parameter, a request attribute, a session attribute, and a cookie.
Draw the successful POST/Redirect/GET flow and the invalid-form forward flow.
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 lab
Later Spring approach
Tomcat started separately; deploy a WAR.
Boot starts an embedded server and can run an executable JAR.
@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.
This project targets Java 21. Install a JDK 21 distribution, then locate its installation directory. A JDK includes both the Java runtime and the javac compiler.
Download JDK 21 for your operating system from Eclipse Temurin and follow its installer instructions.
Set JAVA_HOME to your actual JDK root, for example C:\Program Files\Java\jdk-21. Do not include \bin in JAVA_HOME.
Add %JAVA_HOME%\bin to your Windows user Path, keeping its existing entries. On macOS/Linux, set JAVA_HOME to your installed JDK and add $JAVA_HOME/bin to PATH in your shell configuration.
Open a new terminal and verify both commands below. They should report version 21. Check mvn -version too, because Maven must use the intended JDK.
Verify Java and the compiler
java -version
javac -version
Windows CMD: current terminal versus permanent settings
Check your real JDK location in File Explorer first. The path below is an example. If your folder has a patch version or vendor name, use that complete folder name.
Windows CMD · set for this terminal and verify
set "JAVA_HOME=C:\Program Files\Java\jdk-21"
echo %JAVA_HOME%
dir "%JAVA_HOME%\bin\javac.exe"
java -version
javac -version
mvn -version
java -version can work through Path even when JAVA_HOME is unset. Compare Maven's reported Java runtime too. To save JAVA_HOME for future terminals, use the user Environment Variables editor or this CMD command with your actual path:
Windows CMD · save for future terminals
setx JAVA_HOME "C:\Program Files\Java\jdk-21"
setx does not update the current terminal. Close and reopen it; restart VS Code if its terminals still inherit old values. Use set for the current CMD window. In the Path editor, preserve existing entries and add %JAVA_HOME%\bin; do not replace the entire Path. A Tomcat Windows service may have its own Java configuration: check its service configuration if it uses a different JDK.
For macOS/Linux JAVA_HOME commands, open the Maven installation guide.
Maven is the build tool for this Java project. It reads pom.xml, downloads dependencies, compiles Java, runs tests, and packages a WAR for Tomcat. You install Maven once on your computer; you run build commands from each project's folder containing pom.xml.
This lab uses JDK 21 and Maven 3.9 or later in the Maven 3 series. The examples below use Maven 3.10.0, the recommended stable release checked on 15 October 2026. If the download page offers a newer stable Maven 3 release, use its actual folder name throughout these instructions. Select the binary archive, rather than the source archive or a Maven 4 preview.
Windows: download and extract
Open the official Apache Maven download page. Under the stable Maven 3 release, download Binary zip archive, for example apache-maven-3.10.0-bin.zip.
In File Explorer, right-click the ZIP and choose Extract All. Put the extracted folder in C:\tools. Open it and confirm that C:\tools\apache-maven-3.10.0\bin\mvn.cmd exists. Do not run Maven from inside the ZIP. If extraction adds an extra parent folder, use the actual directory that contains bin.
Locate your JDK 21 installation. Its root contains both bin\java.exe and bin\javac.exe. For example, it might be C:\Program Files\Java\jdk-21; your vendor and version may use a different directory.
Windows: configure JAVA_HOME and Path permanently
Open Start, search for Edit environment variables for your account, and open it.
Under User variables, create or edit JAVA_HOME. Set its value to your actual JDK 21 root directory, for example C:\Program Files\Java\jdk-21. Do not include \bin or quotation marks in the value.
Select the user variable Path → Edit → New. Add the two separate entries shown below. Keep all existing Path entries. If an older Java entry appears earlier in the effective Path, check the resolved executable during verification.
Click OK in each dialog. Close all terminal windows, then open a new PowerShell. If using VS Code's integrated terminal, restart VS Code so it inherits the updated environment.
Two separate entries in the Windows Path editor
%JAVA_HOME%\bin
C:\tools\apache-maven-3.10.0\bin
JAVA_HOME tells Java tools which JDK to use; Path lets the terminal locate commands such as mvn from any folder. Setting MAVEN_HOME alone does not make mvn available: Maven's actual bin folder must be in Path. If you use MAVEN_HOME, point it to the Maven root, not bin. A separate MAVEN_HOME variable is not required for these steps. If C:\tools is unavailable, extract Maven into a writable folder under your user account and add that folder's actual bin path instead.
The Java commands should report version 21. Maven should report its version and the Java runtime it uses. Compare the output with this illustrative example; vendor names, patch versions, and paths will vary.
On Linux, use your JDK's actual location. The following example is common on Ubuntu with an x86-64 OpenJDK 21 package; adjust it if your installation differs:
To keep these settings after closing Terminal, add the appropriate JAVA_HOME line and both export PATH lines to your shell configuration file: usually ~/.zshrc for Zsh or ~/.bashrc for Bash. Bash login shells may use ~/.bash_profile; place the lines in the file your terminal loads. Open a new Terminal and verify:
If Homebrew is already installed on macOS, brew install maven is an alternative to the manual archive installation. Choose one installation method and still check that Maven uses JDK 21.
Try your first Maven build
Download and extract the complete tutorial bundle, open the extracted folder, then open a terminal inside projects/todo-servlet-jsp. Confirm that pom.xml is in the current directory: use Get-ChildItem pom.xml in PowerShell or ls pom.xml on macOS/Linux. Run:
From projects/todo-servlet-jsp · all platforms
mvn clean test package
Command part
What it does
clean
Removes the previous build's target directory.
test
Compiles the source and runs available tests. Maven may report no tests when a project has none.
package
Packages this web application as a WAR. The package phase also includes the earlier compile and test phases, so mvn clean package is sufficient for a normal build.
The first build needs internet access to download plugins and dependencies. A successful build ends with BUILD SUCCESS and produces target/todo-servlet-jsp.war. The WAR is not a running server: continue to Build & deploy to deploy it to Tomcat on port 8083. When following the manual without the bundle, create the files in Complete source before running this build.
Maven installation troubleshooting
What you see
How to fix it
mvn is not recognized / command not found
Check that Maven's bin folder contains mvn.cmd (Windows) or mvn (macOS/Linux), add that exact bin folder to Path, and open a new terminal. On Windows use where.exe mvn to see which installation is found.
JAVA_HOME is not defined correctly
Set JAVA_HOME to the installed JDK root. It must contain bin/java and bin/javac. Do not point it to the bin folder or a standalone JRE.
Maven reports the wrong Java version
Correct JAVA_HOME and the Java Path entries. Restart the terminal or IDE and compare java -version, javac -version, and mvn -version.
release version 21 not supported
The compiler used by the build is older than JDK 21. Verify Maven's Java runtime and any IDE-specific Maven runner JDK or configured toolchain.
There is no POM in this directory
Change to the extracted project's projects/todo-servlet-jsp directory, where pom.xml is located. Running mvn -version does not require a project; building does.
Dependencies cannot be downloaded
Check the internet connection and the repository URL in the error. A managed network may require proxy settings in ~/.m2/settings.xml (Windows: %USERPROFILE%\.m2\settings.xml). Follow your network administrator's settings; do not disable certificate checks.
Checkpoint: In a newly opened terminal, Maven runs without a full path, reports JDK 21, and builds the supplied project successfully. Official references: Maven installation, Maven configuration.
The download example path is not automatically created on your computer. The Windows installer commonly uses C:\Program Files\Apache Software Foundation\Tomcat 10.1; a ZIP uses your extraction folder. Look for bin, conf, logs, and webapps. Do not point CATALINA_HOME at the downloaded ZIP, the bin directory, or your Java project.
Windows CMD · verify your actual Tomcat folder
set "CATALINA_HOME=C:\Program Files\Apache Software Foundation\Tomcat 10.1"
echo %CATALINA_HOME%
dir "%CATALINA_HOME%\webapps"
dir "%CATALINA_HOME%\bin"
If CMD prints %CATALINA_HOME% literally, the variable is unset. If dir reports the path cannot be found, correct the folder before running a startup script. This set lasts for the current CMD window; the Environment Variables editor can save it for later windows.
Windows service or ZIP startup: choose one
A Windows service may already start Tomcat automatically. Press Win+R, enter services.msc, and inspect the Apache Tomcat service. If it is Running, deploy to that instance instead of launching catalina.bat again. Use the service's Start/Stop controls for that instance; use Ctrl+C only for a foreground process you launched. If your installation has no catalina.bat, use its service startup method. For a ZIP installation, running catalina.bat in CMD keeps diagnostics visible; double-clicking startup.bat launches separately.
Default 8080 versus this lab's 8083
Use the HTTP Connector port from this instance's conf/server.xml. The next example configures 8083 for the lab. If you keep 8080, replace 8083 with 8080 in all application URLs. Stop and restart that same instance after editing server.xml. If you see Address already in use, first check whether Tomcat is already running; do not launch a second copy.
Use Tomcat 10.1 for this Jakarta Servlet 6.0 / JSP 3.1 project.
Download the Core ZIP distribution of Tomcat 10.1 from Apache’s official download page. Use 10.1.60 for the pinned lab, or a compatible newer 10.1 maintenance release when the pinned version is no longer listed.
Extract it to a folder such as C:\tools\apache-tomcat-10.1.60. Use your actual extracted directory in later commands.
Open conf/server.xml in that Tomcat folder. Change the HTTP Connector’s port from 8080 to 8083 so this lesson can coexist with the Spring labs.
Existing server.xml Connector · edit port, keep other supplied settings
Tomcat’s default shutdown listener, if enabled, is a separate port. If another Tomcat is already using its shutdown port, use a dedicated available shutdown port or set the Server element’s port to -1 and stop the foreground server with Ctrl+C. The HTTP Connector port is the one the browser uses.
For a quick container check, set JAVA_HOME in the launch terminal, run bin/catalina.bat run on Windows or bash bin/catalina.sh run on Linux/macOS, and visit http://localhost:8083/. Stop it with Ctrl+C before copying the project WAR. The foreground command makes server errors visible without installing a Windows service.
Follow this sequence in macOS Terminal or VS Code's Zsh terminal: install JDK 21, select Java, install Maven, extract Tomcat 10.1, then build and deploy. The archive method below works without Homebrew. Installation commands here are for the Mac; do not copy Windows set, setx, %JAVA_HOME%, or .bat commands.
arm64 normally indicates an Apple Silicon terminal; x86_64 indicates Intel execution and can also appear under Rosetta on an Apple Silicon Mac. Check Apple menu → About This Mac to confirm the actual chip. Choose a native macOS AArch64/ARM64 JDK for Apple Silicon or x64 for Intel. The same Java source and WAR can be used on either architecture. The terminal's % or $ prompt is not part of a command.
2. Install JDK 21 and set JAVA_HOME
Open Temurin downloads. Choose version 21, operating system macOS, your chip architecture, and JDK. Download the PKG installer, open it, and follow its installation screens.
List the JDKs macOS can find. The output should include a JDK 21 entry. A typical Java home ends in .jdk/Contents/Home; neither the .jdk wrapper nor its bin directory is JAVA_HOME.
Checkpoint: the compiler check prints JDK compiler found; Java and javac report 21. If java_home says it cannot locate a runtime, install the JDK first and repeat the list command. Do not continue with an empty JAVA_HOME. A Homebrew-only openjdk formula may need its documented registration steps before java_home discovers it; the PKG route avoids that extra setup.
3. Keep the settings after closing Terminal
An export changes the current shell and child processes. To keep it, edit the startup file your shell reads. The instructions below assume a standard Zsh account without a custom ZDOTDIR.
Zsh · open your existing configuration for editing
nano "$HOME/.zshrc"
Add these two lines once, keeping all existing content. Do not overwrite the file or put spaces around the equals sign:
Interactive Zsh shells; useful for Terminal and integrated-terminal settings.
~/.zprofile
Login Zsh shells. Homebrew's installer may ask you to add shellenv here.
~/.bash_profile
Bash login shells. If you deliberately use Bash, put exports here; interactive non-login Bash uses .bashrc.
Use the file for your actual shell; editing .bash_profile does not configure Zsh. Start a new terminal and check again. If VS Code still sees old values, fully quit and reopen it, select the Zsh profile and create a new terminal. An IDE's Java/Maven runner can also have its own JDK setting.
4. Install Maven: choose one method
Method A: binary archive, without Homebrew
Download apache-maven-3.10.0-bin.tar.gz from Apache Maven into Downloads. Use the actual version's filename if it differs. Extract into a folder you own:
Add the same Maven Path line to .zshrc after the JAVA_HOME line, then source the file. Preserve :$PATH, which retains other commands. If Finder already extracted the archive, move that extracted Maven directory into tools instead; do not extract a nonexistent archive again.
Method B: Homebrew, if supported on your Mac
If Homebrew is not installed, use the instructions at brew.sh and complete its printed Next steps. For older macOS or unsupported Intel configurations, the manual archive route above is simpler than repairing an unsupported Homebrew installation.
If brew is installed but not found, run only the block matching its actual location. You can check the files in Finder or with ls. These examples enable the existing installation; they do not install Homebrew.
Apple Silicon · usual Homebrew location
eval "$(/opt/homebrew/bin/brew shellenv)"
Intel · usual Homebrew location
eval "$(/usr/local/bin/brew shellenv)"
Add the matching shellenv line once to ~/.zprofile as directed by the installer; retain your existing file. Then install Maven:
Check Maven's reported Java runtime even if Homebrew also installed another JDK. Avoid adding both Maven installations to Path; type -a mvn reveals competing commands. Maven must be callable in a newly opened terminal before proceeding.
5. Extract Tomcat in a writable folder
From Tomcat downloads, choose the 10.1 Core tar.gz binary. This example uses 10.1.61; if your compatible 10.1 download differs, use its exact archive and extracted folder names. A Windows service installer or .bat file is not the Mac distribution.
macOS · extract and verify Tomcat
mkdir -p "$HOME/tools"
tar -xzf "$HOME/Downloads/apache-tomcat-10.1.61.tar.gz" -C "$HOME/tools"
export CATALINA_HOME="$HOME/tools/apache-tomcat-10.1.61"
ls "$CATALINA_HOME/bin/catalina.sh"
ls "$CATALINA_HOME/webapps"
Add the CATALINA_HOME export once to .zshrc if you want to keep it. Quote paths with spaces. Use "$HOME/tools", not "~/tools": a tilde inside quotes is not expanded as a home directory. This lab uses CATALINA_BASE equal to CATALINA_HOME; advanced multi-instance configuration can separate them.
Start only if another Tomcat is not already using your chosen port. For this shell, use the selected JDK rather than a leftover JRE_HOME:
macOS · foreground Tomcat startup
unset JRE_HOME
bash "$CATALINA_HOME/bin/catalina.sh" run
Keep this terminal open. Open localhost:8080 unless you configured 8083. Stop this foreground process with Ctrl+C. If a direct ./catalina.sh invocation reports permission denied, invoking it through bash avoids needing an execute bit; first check the file is the actual downloaded shell script.
6. Build, copy the WAR, and open your application
Extract the todo project ZIP. In Terminal, type cd with a trailing space, drag the folder containing pom.xml from Finder into the terminal, then press Enter. Alternatively type its quoted absolute path. This avoids guessing a Downloads or Desktop path. Stop the foreground Tomcat before the copy in this sequence.
macOS · in the project folder containing pom.xml
pwd
ls pom.xml
mvn -version
mvn clean package
ls -lh target/todo-servlet-jsp.war
cp target/todo-servlet-jsp.war "$CATALINA_HOME/webapps/todo-servlet-jsp.war"
ls -lh "$CATALINA_HOME/webapps/todo-servlet-jsp.war"
bash "$CATALINA_HOME/bin/catalina.sh" run
Run copy/start only after BUILD SUCCESS and a valid CATALINA_HOME. Open the welcome page on 8080 or 8083 if configured. Expect Java web basics, then test the greeting and session todos. If a different Tomcat is already running with auto-deployment, use that instance instead of starting another one.
For the smaller Hello World starter, substitute hello-servlet.war and open /hello-servlet/. Do not mix the two application filenames or URL paths.
7. MacBook troubleshooting checklist
Symptom
What to inspect
Unable to locate a Java Runtime
Install a matching JDK PKG, run java_home -V, then select 21. A downloaded installer file alone is not an installed JDK.
Bad CPU type / incompatible architecture
Compare the Mac chip, terminal architecture, and JDK download. An Apple Silicon Mac running an Intel terminal may show x86_64. Prefer a consistent native toolchain.
JAVA_HOME is invalid or Maven uses another Java
Check echo, java_home -V, javac -version, and mvn -version. JAVA_HOME should end in Contents/Home for a normal macOS PKG installation. Check older exports and any JRE_HOME used by Tomcat.
zsh: command not found: mvn or brew
Check its bin location and Path. Run type -a mvn or type -a brew. Add the correct export or brew shellenv to the appropriate startup file and open a new terminal.
Settings disappear in a new terminal
Exports were entered only in one window, or the wrong startup file was edited. Check .zshrc versus .zprofile, ZDOTDIR if customized, and the actual shell selected in VS Code.
export: not valid / unmatched quote
Use plain straight quotes and no spaces around =. Copy from the code block rather than retyping formatted prose. Check the last lines edited in your shell file.
ls, cp or other commands stop working after a Path edit
You may have replaced Path instead of extending it. Open a fresh terminal if the bad change was temporary; correct the saved export using a full-path editor such as /usr/bin/nano and preserve :$PATH.
File or directory not found / cannot stat
Check the actual extracted version and whether Finder already unpacked the archive. Use pwd and ls. Quote spaces; do not use Windows drive paths or quoted ~/... paths.
Permission denied while copying or installing
Use a tools folder under your own home directory for this archive-based lab. Inspect ownership rather than using sudo for every Maven command or broad chmod changes. Homebrew formula installs normally run as the Homebrew owner.
macOS refuses to open a JDK installer
Confirm the download came from the official vendor, matches your architecture, and supports your macOS version. Follow the vendor's installation guidance; obtain a fresh supported installer when appropriate.
Address already in use
Use the read-only port check below. Identify the process before stopping it. Reuse an existing Tomcat, stop your own foreground instance, or select another Connector port.
Tomcat answers but the application returns 404
Confirm the correct WAR is in the running instance's webapps directory and deployment succeeded. Inspect logs/localhost and logs/catalina files. Check port, filename and application context path.
Works in Terminal but not VS Code
Restart VS Code, choose the same shell profile, and compare command -v mvn and mvn -version. Configure the IDE's Java runtime separately if its extension uses a different JDK.
macOS · read-only diagnosis
type -a java
type -a mvn
echo "$JAVA_HOME"
echo "$CATALINA_HOME"
printf '%s\n' "$PATH" | tr ':' '\n'
lsof -nP -iTCP:8080 -sTCP:LISTEN
Use 8083 in lsof if that is your configured port. No listener can mean Tomcat is stopped; process visibility can also depend on permissions. After correcting settings, recheck java, javac, mvn and the application URL in a fresh terminal. Do not run Windows tasklist or services.msc commands on macOS.