Java Web & Servlets Fundamentals

Your First Servlet

18 min Lesson 3 of 13

Your First Servlet

A servlet is a Java class that lives inside a web container (like Tomcat or Jetty), receives HTTP requests, and writes HTTP responses. You already know how to design classes, handle exceptions, and structure Java programs — a servlet is just a specialised class that plugs into the container's infrastructure. This lesson walks through exactly what that means, from the minimal class structure to the annotations that register the servlet with the container.

The HttpServlet Base Class

Every HTTP-handling servlet extends jakarta.servlet.http.HttpServlet. This abstract class is part of the Jakarta Servlet specification (formerly javax.servlet) and provides the framework you override rather than build from scratch. The class hierarchy looks like this:

java.lang.Object └── jakarta.servlet.GenericServlet (implements Servlet, ServletConfig, Serializable) └── jakarta.servlet.http.HttpServlet └── your.package.HelloServlet (your code goes here)

GenericServlet handles the wiring to the container: it stores the ServletConfig, implements getServletName(), and provides default no-op implementations of the lifecycle methods. HttpServlet adds HTTP-specific dispatch: its service() method inspects the request method (GET, POST, PUT, etc.) and calls the appropriate doXxx() method on your subclass. You never call service() directly — the container does.

Jakarta vs javax: Since Jakarta EE 9 (2020), the package prefix changed from javax.servlet to jakarta.servlet. Tomcat 10+ and any Jakarta EE 10/11 server uses the jakarta.* namespace. Older Tomcat 9 / Java EE 8 servers still use javax.*. Always check which version your container targets and import accordingly.

The @WebServlet Annotation

Before annotations existed, you had to declare every servlet in web.xml with verbose XML. Since Servlet 3.0, the @WebServlet annotation on the class itself is the preferred approach — it keeps the URL mapping next to the code that handles it.

package com.example.web; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet(name = "helloServlet", urlPatterns = "/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); try (PrintWriter out = response.getWriter()) { out.println("<!DOCTYPE html>"); out.println("<html><body>"); out.println("<h1>Hello from a Servlet!</h1>"); out.println("</body></html>"); } } }

Let's unpack the annotation attributes:

  • name — a logical identifier used internally by the container (and by getServletName()). Optional; defaults to the fully-qualified class name.
  • urlPatterns — one or more URL patterns this servlet handles. The pattern /hello matches the path http://host:port/yourApp/hello exactly. You can also use prefix patterns (/api/*) or extension mappings (*.do).
  • The shorthand @WebServlet("/hello") (single string) is equivalent to urlPatterns = "/hello" and is the most common form in practice.
Context root vs URL pattern: The pattern you write in @WebServlet is relative to the application's context root, not the server root. If you deploy myapp.war, a pattern of /hello resolves to /myapp/hello. During development with an IDE or Maven plugin you often set the context root to / to keep URLs short.

The doGet Method in Detail

The container calls doGet when it receives an HTTP GET request matching the servlet's URL pattern. The two parameters give you everything you need to handle the request:

  • HttpServletRequest request — wraps the incoming HTTP message. Gives you access to URL parameters, headers, the request body, cookies, session, locale, and the client's IP address.
  • HttpServletResponse response — wraps the outgoing HTTP message. Lets you set the status code, response headers, and write the response body through either a PrintWriter (text) or a ServletOutputStream (binary).

The method signature declares throws IOException. HttpServlet.doGet also declares throws ServletException — you can add that too, but you must import jakarta.servlet.ServletException. A realistic override looks like this:

@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. Read input String name = request.getParameter("name"); // ?name=Alice from the URL if (name == null || name.isBlank()) { name = "World"; } // 2. Business logic (call a service, query a DB, etc.) String greeting = "Hello, " + name + "!"; // 3. Write output response.setContentType("text/html;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_OK); // 200 — optional; it is the default try (PrintWriter out = response.getWriter()) { out.printf("<!DOCTYPE html><html><body><p>%s</p></body></html>%n", greeting); } }

Notice the three-step pattern: read input → apply logic → write output. Keeping this separation makes the servlet easy to test: business logic belongs in plain Java service classes, not inside the servlet itself. The servlet's only job is translating HTTP into method calls and method results back into HTTP.

Reading the Request Object

The HttpServletRequest interface exposes the full HTTP request. The most commonly used methods in a doGet handler:

  • request.getParameter("key") — returns the first value of a query-string or form parameter, or null if absent.
  • request.getParameterValues("key") — returns all values for a multi-valued parameter (e.g. checkboxes) as a String[].
  • request.getHeader("Accept-Language") — returns a named request header.
  • request.getMethod() — returns "GET", "POST", etc.
  • request.getRequestURI() — the path portion of the URL, e.g. /myapp/hello.
  • request.getAttribute("key") / request.setAttribute("key", value) — request-scoped storage used when forwarding to a JSP or another servlet.
Never trust user input. getParameter() returns raw strings from the browser. Always validate, sanitise, and HTML-encode any value you embed in the response. Failing to do so is the root cause of Cross-Site Scripting (XSS) vulnerabilities. At minimum, escape <, >, and & before writing user-supplied strings into HTML output.

Writing the Response Object

Before you write the response body you must call response.setContentType(). This sets the Content-Type HTTP header so the browser knows how to interpret the bytes. Common values:

  • "text/html;charset=UTF-8" — an HTML page.
  • "application/json;charset=UTF-8" — a JSON API response.
  • "application/octet-stream" — a binary file download.

You must set the content type before calling getWriter() or getOutputStream(). Once you start writing the body the headers are committed (sent to the client) and can no longer be changed.

For text responses use response.getWriter() which returns a PrintWriter. For binary responses (images, PDFs, ZIP files) use response.getOutputStream() which returns a ServletOutputStream. You can only obtain one of the two per request — calling both throws an IllegalStateException.

A Complete, Runnable Example

Here is a self-contained servlet that greets a visitor by name and demonstrates all the concepts above in a realistic way:

package com.example.web; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet("/greet") public class GreetServlet extends HttpServlet { // The container instantiates this class once; doGet is called per request. // Instance variables must be thread-safe — prefer stateless servlets. @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); if (name == null || name.isBlank()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing required parameter: name"); return; // sendError commits the response — stop here } // Sanitise: a real app would use a library like OWASP Java Encoder String safeName = name.replace("&", "&amp;") .replace("<", "&lt;") .replace(">", "&gt;"); response.setContentType("text/html;charset=UTF-8"); try (PrintWriter out = response.getWriter()) { out.println("<!DOCTYPE html>"); out.println("<html lang=\"en\"><head><meta charset=\"UTF-8\">"); out.println("<title>Greeting</title></head><body>"); out.printf( "<h1>Hello, %s!</h1>%n", safeName); out.println("</body></html>"); } } }

A GET request to /greet?name=Alice produces a 200 OK HTML page saying "Hello, Alice!". A request without the parameter receives a 400 Bad Request error response — handled by the container's default error page.

What the Container Does for You

It is worth being explicit about the work the container (Tomcat, Jetty, WildFly, etc.) performs automatically:

  • Parses the raw TCP bytes into an HttpServletRequest object.
  • Instantiates your servlet class once and calls init().
  • Allocates a thread from its pool for each incoming request and calls your doGet on that thread.
  • Constructs the HTTP response headers from what you set on the HttpServletResponse and serialises the body to the socket.
  • Calls destroy() when the application is undeployed.

This division of labour — container handles the protocol plumbing, your code handles the application logic — is the core contract of the Servlet specification and the foundation of every Java web framework built on top of it.

Summary

A servlet is a class that extends HttpServlet and overrides the doXxx methods matching the HTTP verbs it handles. The @WebServlet annotation registers it with the container and declares the URL pattern. The HttpServletRequest parameter exposes every detail of the incoming request; the HttpServletResponse parameter lets you control every detail of the reply. Keep servlet code thin — read, delegate, respond — and put real logic in plain Java classes. In the next lesson you will see the full lifecycle: how the container creates, initialises, and eventually destroys a servlet.