Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 21 – Jakarta EE web: Servlets and Tomcat

Your first servlet, web.xml and the WAR layout

Write a servlet, build it into a WAR with Maven, map its URLs with @WebServlet or web.xml, and see why Tomcat never serves anything under WEB-INF.

  • Beginner
  • 26 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 9 of 26 minutes free
Free: The WAR layout · Your first servlet, built with Maven · @WebServlet or web.xml. With a pass: 3 in-depth parts and 3 interview answers, about 17 minutes.

What you will learn

  • Build a WAR with Maven against the provided-scope Servlet 6.1 API
  • Map servlets with @WebServlet or web.xml and predict which URL pattern wins
  • Explain WEB-INF, WEB-INF/classes and WEB-INF/lib and why WEB-INF is never served

Before you start

A servlet is a Java class that extends HttpServlet and overrides a method per HTTP method, such as doGet for GET requests. You never call it yourself: the container creates the servlet, and whenever a request’s URL matches the servlet’s URL pattern, it calls the matching method with a request object to read and a response object to write. To deploy servlets you package them as a WAR, a web application archive: a ZIP file whose top level holds files that are served as they are, and whose WEB-INF folder holds the compiled classes in WEB-INF/classes, library JARs in WEB-INF/lib and the optional deployment descriptor, web.xml. Nothing under WEB-INF is ever sent to a browser. A servlet gets its URL from the @WebServlet annotation or from a mapping in web.xml. This lesson builds your first servlet with Maven, deploys the WAR to Tomcat 11 and checks each of those rules against what Tomcat does.

The WAR layout

A WAR file: files at its top level are served, while META-INF and WEB-INF, with web.xml, classes and lib, never are.clock.warcontext path /clockTop level: servedindex.html, css/, images/at /clock/index.html …META-INF/MANIFEST.MF, build detailsnever served: 404WEB-INF/never served: 404web.xmldeployment descriptor,optionalclasses/your compiled classes,loaded firstlib/the JAR filesyour code needs

What goes where in a WAR, and which parts the container serves

Text description of the diagram

The diagram shows clock.war, a ZIP file whose name gives the application its context path, /clock. It holds three parts, from top to bottom.

1. The top level holds files that the container serves as they are, such as index.html, a css folder and images. A request for /clock/index.html returns index.html. 2. META-INF holds MANIFEST.MF and details that build tools add. The container never serves it and answers 404. 3. WEB-INF is never served either, and requests for anything in it get 404. It holds three things: web.xml, the optional deployment descriptor; classes, the folder of your compiled classes, which the application's class loader reads first; and lib, the folder of the JAR files your code needs.

The WAR layout comes from the Servlet specification, so every container reads a WAR the same way:

Path in the WAR What it holds Served to browsers?
index.html, css/, images/ Static files and JSPs Yes, at /clock/index.html and so on
WEB-INF/web.xml The deployment descriptor: servlets, mappings, settings Never: 404
WEB-INF/classes/ Your compiled classes, in package folders Never: 404
WEB-INF/lib/ JAR files your code needs, loaded after classes Never: 404
META-INF/ MANIFEST.MF and details added by build tools Never: 404

The specification makes the privacy of WEB-INF a rule: no file in it may be served directly to a client, and a request for one gets 404. Your servlet code can still read those files, so configuration that the server needs and visitors must not see belongs there. The file name decides where the application lives: Tomcat deploys clock.war at the context path /clock.

Your first servlet, built with Maven

Your first servlet answers GET requests for /now with the time of day in India:

clock/src/main/java/com/example/clock/NowServlet.java Java · clock/src/main/java/com/example/clock/NowServlet.java
package com.example.clock;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/** Answers GET requests for /now with the time of day in India. */
@WebServlet("/now")
public class NowServlet extends HttpServlet {
    private static final ZoneId INDIA = ZoneId.of("Asia/Kolkata");
    private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss");

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ZonedDateTime now = ZonedDateTime.now(INDIA);
        response.setContentType("text/plain;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.printf("Time in %s: %s (UTC%s)%n", INDIA, now.format(TIME), now.getOffset());
        out.printf("Answered by the servlet named %s, mapped to %s%n", getServletName(), request.getHttpServletMapping().getPattern());
    }
}

@WebServlet("/now") declares the servlet and maps it in one line. setContentType comes before getWriter, so that the writer encodes the text in UTF-8. The POM is a normal Maven POM with two differences from a JAR project:

clock/pom.xml XML · clock/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- The clock application: one servlet, packaged as a WAR for Tomcat 11. -->
<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>clock</artifactId>
  <version>1.0.0</version>
  <!-- war: package builds target/clock.war instead of a JAR. -->
  <packaging>war</packaging>

  <properties>
    <maven.compiler.release>25</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!-- The Servlet 6.1 API to compile against. Scope provided: Tomcat supplies it at run time, so it stays out of
         WEB-INF/lib. -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.1.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <!-- The WAR's name, and so the application's context path: /clock. -->
    <finalName>clock</finalName>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.5.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.5.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.16.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.6.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.5.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>3.2.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>3.2.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

<packaging>war</packaging> makes mvn package run the WAR plugin, which copies src/main/webapp to the top of the WAR and the compiled classes to WEB-INF/classes. The Servlet API has scope provided: Maven compiles your first servlet against jakarta.servlet-api 6.1.0 but leaves the JAR out of the WAR, because Tomcat supplies the API itself at run time. The application’s web.xml declares the same class a second time, under the name ist-clock, and maps that declaration to /ist:

clock/src/main/webapp/WEB-INF/web.xml XML · clock/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- The deployment descriptor. Annotations already map NowServlet to /now; this file declares the same class a second
     time, under its own name, and maps that declaration to /ist. -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
         version="6.1">
  <servlet>
    <servlet-name>ist-clock</servlet-name>
    <servlet-class>com.example.clock.NowServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>ist-clock</servlet-name>
    <url-pattern>/ist</url-pattern>
  </servlet-mapping>
</web-app>

The session builds the WAR, lists it, deploys it by copying it into Tomcat’s webapps folder and makes five requests.

Build, list, deploy and request the clock WAR Python · build_session.py
from session import run

# Builds the clock WAR with Maven, lists it, deploys it to a fresh Tomcat base on 127.0.0.1:28520 and requests its
# servlet through both mappings, its static page and two files under WEB-INF.
run(
    """
cd clock && mvn -q package
cd clock && jar tf target/clock.war
sh new-base.sh base 28520
cp clock/target/clock.war base/webapps/
$CATALINA_HOME/bin/startup.sh
curl -s http://127.0.0.1:28520/clock/now
curl -s http://127.0.0.1:28520/clock/ist
curl -s -o /dev/null -w "%{http_code} %{content_type}\\n" http://127.0.0.1:28520/clock/
curl -s -o /dev/null -w "%{http_code}\\n" http://127.0.0.1:28520/clock/WEB-INF/web.xml
curl -s -o /dev/null -w "%{http_code}\\n" http://127.0.0.1:28520/clock/WEB-INF/classes/com/example/clock/NowServlet.class
grep -E "Deploying web application archive|has finished" base/logs/catalina.out
$CATALINA_HOME/bin/shutdown.sh
""",
    port=28520,
    show=("java", "maven", "tomcat"),
)

Output

# openjdk version "25.0.4.1" LTS · Apache Maven 3.10.0 · Apache Tomcat 11.0.26
$ cd clock && mvn -q package
$ cd clock && jar tf target/clock.war
META-INF/
META-INF/MANIFEST.MF
WEB-INF/
WEB-INF/classes/
WEB-INF/classes/com/
WEB-INF/classes/com/example/
WEB-INF/classes/com/example/clock/
WEB-INF/classes/com/example/clock/NowServlet.class
WEB-INF/web.xml
index.html
META-INF/maven/com.example/clock/pom.xml
META-INF/maven/com.example/clock/pom.properties
$ sh new-base.sh base 28520
$ cp clock/target/clock.war base/webapps/
$ $CATALINA_HOME/bin/startup.sh
Tomcat started.
$ curl -s http://127.0.0.1:28520/clock/now
Time in Asia/Kolkata: 23:53:00 (UTC+05:30)
Answered by the servlet named com.example.clock.NowServlet, mapped to /now
$ curl -s http://127.0.0.1:28520/clock/ist
Time in Asia/Kolkata: 23:53:00 (UTC+05:30)
Answered by the servlet named ist-clock, mapped to /ist
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://127.0.0.1:28520/clock/
200 text/html
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:28520/clock/WEB-INF/web.xml
404
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:28520/clock/WEB-INF/classes/com/example/clock/NowServlet.class
404
$ grep -E "Deploying web application archive|has finished" base/logs/catalina.out
… INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive [base/webapps/clock.war]
… INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [base/webapps/clock.war] has finished in […] ms
$ $CATALINA_HOME/bin/shutdown.sh

This output changes from run to run: The time of day in the two answers changes from run to run.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_session.py

The listing is the WAR layout from the diagram: NowServlet.class under WEB-INF/classes in its package folders, web.xml in WEB-INF, index.html at the top, and no WEB-INF/lib, because the only dependency is provided. Both URLs answered, from two servlets of one class, named com.example.clock.NowServlet (the default name of an annotated servlet is its class name) and ist-clock. The static page answered 200 at /clock/, while web.xml and the class file itself answered 404.

@WebServlet or web.xml

Both declare a servlet and its URL patterns, and an application can use both, as this one does.

@WebServlet on the class <servlet> and <servlet-mapping> in web.xml
Where the mapping lives Next to the code In one file for the whole application
Changing a URL Recompile the class Edit the file; no compiling
One class at two URLs with different settings Not possible with one annotation One <servlet> element per declaration
Seeing every URL at once Search the code Read one file

@WebServlet suits small applications and servlets that one team owns. A web.xml suits settings that operators change, such as initialisation parameters, and applications that want every URL listed in one place. When web.xml sets metadata-complete="true" on its root element, it becomes the only source, and Tomcat skips scanning the classes for annotations, which also shortens start-up.

Either way, a URL pattern takes one of five forms. An exact pattern such as /students matches one path. A path pattern such as /api/* matches everything below a prefix. An extension pattern such as *.csv matches by the end of the path. The pattern / names the default servlet, and the empty string matches the context root alone.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 The clock application is deployed at /clock. What does Tomcat answer to GET /clock/WEB-INF/web.xml?

    Choose one answer.

    Show the answer to question 1

    Answer: 404, because no file under WEB-INF may be served to a client

    The Servlet specification forbids serving anything under WEB-INF and asks for a 404, so a client cannot even tell whether the file exists. The recorded session got 404 for web.xml and for the class file. Servlet code can still read those files through the ServletContext.

  2. Question 2 of 4 Why does the clock POM give jakarta.servlet-api the scope provided?

    Choose one answer.

    Show the answer to question 2

    Answer: Tomcat supplies the Servlet API at run time, so the WAR only needs it to compile

    A provided dependency is on the compile class path but is left out of the package, because the environment the code runs in brings it. The listing of clock.war has no WEB-INF/lib at all. The API you run on is then always the container's own, which is why you compile against the oldest version you deploy to.

  3. Question 3 of 4 A web.xml maps three servlets like this. Which one answers GET /shop/api/export.csv, if the application is deployed at /shop?

    Read the code, then choose one answer.

    <servlet-mapping><servlet-name>api</servlet-name><url-pattern>/api/*</url-pattern></servlet-mapping>
    <servlet-mapping><servlet-name>csv</servlet-name><url-pattern>*.csv</url-pattern></servlet-mapping>
    <servlet-mapping><servlet-name>fallback</servlet-name><url-pattern>/</url-pattern></servlet-mapping>
    Show the answer to question 3

    Answer: api, because a path prefix is tried before an extension

    The container tries an exact match, then the longest path prefix, then the extension, then the default. /api/* matches /api/export.csv, so the extension mapping is never consulted, as the recorded probe showed for /api/users.csv. Deployment fails only when one pattern is mapped to two servlets, not when two patterns could match one path.

  4. Question 4 of 4 NowServlet carries @WebServlet("/now"), and web.xml also declares the class under the name ist-clock with the URL /ist. How many NowServlet objects does Tomcat create?

    Choose one answer.

    Show the answer to question 4

    Answer: Two, one for each declaration, with different names

    The specification says that when a class is declared in the deployment descriptor under another name, the container must create a new instance. The recorded answers came from servlets named com.example.clock.NowServlet and ist-clock. Each declaration has its own instance, its own name and its own initialisation parameters.

Which URL pattern wins, tested on eighteen paths Pro: opens with a Pro, Premium or Ultimate pass

A servlet’s URL can be an exact path, a path prefix ending in /*, an extension such as *.csv, the default / or the empty string for the context root. This part maps one probe servlet with all six kinds and records which declaration answered eighteen requests, so that you can predict the answer before you deploy.

In this part:

  • The probe: six declarations of one servlet, built by hand
  • The four rules, worked through the results
  • Why /* hides your files and JSPs, reproduced

3 runnable examples (Java, Python, XML) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Deployment failures reproduced, and where Tomcat logs their cause Pro: opens with a Pro, Premium or Ultimate pass

Two common mistakes produce a 404 with nothing in the console: a servlet whose constructor throws, and two servlets mapped to one URL. This part reproduces both next to a fixed version, shows which log file holds each cause, and makes Tomcat report a broken servlet when it deploys instead of when a user first calls it.

In this part:

  • Five applications, two mistakes
  • Reading the results
  • A check list for an unexplained 404

2 runnable examples (Python, XML (context.xml), XML (web.xml), broken/…/ReportServlet.java, early/…/ReportServlet.java, fixed/…/ReportServlet.java) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Exercise: an Asia/Kolkata clock that answers in text or JSON Pro: opens with a Pro, Premium or Ultimate pass

Write a servlet that answers with the date and time in India, as text by default and as JSON when the client asks for it, with a clock you can fix for testing. Five sample checks and a deployment show when it is done.

In this part:

  • The task and the starting point
  • A solution, checked and deployed

5 runnable examples (Java, Python) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher · Warm-up

    What is a WAR file, and why are files under WEB-INF never served to browsers?

    A WAR, a web application archive, is a ZIP file with the layout that the Servlet specification defines. Its top level is the document root: pages, style sheets, images and JSPs that the container serves under the application's context path, which Tomcat takes from the file name. WEB-INF holds what the application needs but visitors must not see: web.xml, the deployment descriptor; classes, the compiled classes; and lib, the library JARs, which load after classes. The specification forbids serving any file under WEB-INF and requires a 404, so configuration, compiled code and libraries cannot be downloaded and studied. Servlet code can still read those files through ServletContext.getResource and getResourceAsStream, and a servlet can forward to a JSP kept under WEB-INF, so that the page is reachable only through its controller.

  2. Mid to senior

    A WAR built against jakarta.servlet-api 6.1.0 passes every test and runs on Tomcat 11. On a server that still runs Tomcat 10.1 it deploys without errors, but one endpoint fails with NoSuchMethodError for HttpServletResponse.sendRedirect(String, int). Why, and how do you stop this reaching production?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    An application maps one servlet to /api/, a second to .json and a third to /. Which servlet answers /api/orders.json, /reports/sales.json and /api? A teammate wants every .json file, including those under /api, served by the second servlet. What do you advise?

    Model answer, Pro. First step and follow-ups

  4. Fresher to mid

    After a release, GET /orders/export answered 500 once and has answered 404 ever since, while the rest of the application works and catalina.out shows a normal deployment. What happened, where is the evidence, and how do you make this kind of failure visible at deployment next time?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A servlet extends HttpServlet; the container creates it and calls doGet and the other methods for requests that match its URL pattern.
  • A WAR has served files at its top level and private files in WEB-INF: the web.xml descriptor, classes and lib.
  • Build WARs with <packaging>war</packaging> and the Servlet API in scope provided, because the container supplies it.
  • Map servlets with @WebServlet next to the code or in web.xml in one place; one class declared twice becomes two servlets.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.