Servlets & JSP

The foundation of Java web development — what Spring MVC is built on, and why understanding it matters even if you never write one directly

← Back to Index

What is a Servlet — and Why Does a Spring Developer Need to Know?

A Servlet is a Java class that receives an HTTP request and produces an HTTP response. That's the entire contract — HttpServletRequest in, HttpServletResponse out. The Servlet specification (now jakarta.servlet.*) defines this contract; Tomcat, Jetty, and Undertow are implementations that run servlets inside a Servlet Container.

The reason a Spring developer needs to understand this: Spring MVC's DispatcherServlet is itself a Servlet — it implements HttpServlet and is registered in a Servlet Container exactly like any other servlet. Every HTTP request your Spring Boot application handles goes through this chain:

/*
 *  Browser
 *      │ HTTP request
 *      ▼
 *  Tomcat (Servlet Container)
 *      │ invokes the registered Servlet for this URL
 *      ▼
 *  DispatcherServlet  ←  this IS a Servlet (extends HttpServlet)
 *      │ Spring's own routing logic
 *      ▼
 *  @RestController / @Controller method
 *      │ your code
 *      ▼
 *  HTTP response back to browser
 *
 * Understanding Servlets = understanding what's running underneath Spring.
 */
When you actually write a raw Servlet today

In new Spring Boot projects, you almost never write a raw HttpServlet subclass — Spring MVC handles all routing. The cases where raw Servlets still appear: integrating a third-party library that registers its own servlet (e.g. H2 Console, Swagger UI embeds), legacy codebases pre-Spring MVC, and Jakarta EE projects on WildFly or Payara that don't use Spring at all. The concepts transfer directly — request/response, lifecycle, filters — even if you write them through Spring abstractions.

The Servlet Lifecycle

/*
 *  Container startup
 *      │
 *      ▼
 *  new HelloServlet()          ← container calls the no-arg constructor
 *      │
 *      ▼
 *  init(ServletConfig config)  ← called ONCE, before any request is handled
 *      │                          override to set up resources (DB connections, etc.)
 *      ▼
 *  ┌──────────────────────────────────────────────┐
 *  │  For each incoming request (concurrent):      │
 *  │                                              │
 *  │  service(request, response)                   │
 *  │      ├── doGet()    ← GET requests            │
 *  │      ├── doPost()   ← POST requests           │
 *  │      ├── doPut()    ← PUT requests            │
 *  │      ├── doDelete() ← DELETE requests         │
 *  │      └── ...                                  │
 *  └──────────────────────────────────────────────┘
 *      │
 *      ▼
 *  destroy()                   ← called ONCE when container shuts down
 *                                 override to release resources
 */
Servlets are singletons — instance variables are a race condition

The container creates one servlet instance and routes all concurrent requests to it across multiple threads simultaneously. Any instance variable on a servlet is shared across all requests — a classic race condition waiting to happen. The rule: servlets must have no mutable instance state. Everything request-specific lives in local variables inside the handler method, or in the HttpSession. This is the same reason Spring's @Controller beans are singletons by default and must also be stateless.

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

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    // ✅ Safe: set once in init(), never changed — effectively final
    private String greeting;

    // ❌ WRONG: mutable instance variable — race condition under concurrent requests
    // private int requestCount = 0;

    @Override
    public void init() {
        greeting = getServletConfig().getInitParameter("greeting");  // read-only after init
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String name = request.getParameter("name");  // ✅ Safe: local variable, per-request

        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().println(greeting + ", " + name);
    }

    @Override
    public void destroy() {
        // release any resources opened in init()
    }
}

HttpServletRequest and HttpServletResponse

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    // ── Reading the request ───────────────────────────────────────────────
    String name   = request.getParameter("name");        // query param or form field
    String auth   = request.getHeader("Authorization");  // request header
    String method = request.getMethod();                  // "GET", "POST", etc.
    String uri    = request.getRequestURI();              // "/myapp/users"
    String body   = request.getReader().lines()           // raw body as string
        .collect(java.util.stream.Collectors.joining());

    // Attributes (server-side data attached to THIS request, not from client)
    request.setAttribute("user", currentUser);
    Object user = request.getAttribute("user");

    // ── Building the response ─────────────────────────────────────────────
    response.setStatus(HttpServletResponse.SC_CREATED);         // 201
    response.setContentType("application/json;charset=UTF-8");
    response.setHeader("Location", "/api/users/123");

    response.getWriter().println("{\"id\":123,\"name\":\"" + name + "\"}");

    // Forward vs Redirect — different things, different use cases
    request.getRequestDispatcher("/WEB-INF/views/success.jsp")
           .forward(request, response);   // server-side: same request, same URL in browser

    response.sendRedirect("/dashboard");  // client-side: new GET request, URL changes
}
forward() vs sendRedirect() — the difference that matters for POST/Redirect/GET

forward() transfers control to another resource (another servlet or JSP) inside the server — the browser's URL bar never changes, and the original request object is passed along. sendRedirect() sends a 302 response to the browser, which then makes a brand-new GET request to the given URL. The classic pattern for form submissions is POST → process → redirect → GET (the "Post/Redirect/Get" pattern), specifically to prevent the browser from resubmitting the form on refresh. Spring MVC's "redirect:/path" return value does exactly this.

Filters — Cross-Cutting Concerns Before the Servlet

A Filter intercepts requests before they reach any servlet — and responses on the way back out. The filter chain runs in declared order; each filter calls chain.doFilter() to pass control to the next filter or the servlet, or it can short-circuit by writing directly to the response without calling chain.doFilter().

@WebFilter("/*")  // applies to every URL
public class RequestLoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                          FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpReq = (HttpServletRequest) req;
        long start = System.currentTimeMillis();

        chain.doFilter(req, res);   // ← hand off to the next filter or the servlet

        long elapsed = System.currentTimeMillis() - start;
        log.info("{} {} → {}ms", httpReq.getMethod(), httpReq.getRequestURI(), elapsed);
    }
}

// Auth filter — short-circuits if token is missing, never calls chain.doFilter()
@WebFilter("/api/*")
public class AuthFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                          FilterChain chain)
            throws IOException, ServletException {

        String auth = ((HttpServletRequest) req).getHeader("Authorization");
        if (auth == null || !auth.startsWith("Bearer ")) {
            ((HttpServletResponse) res).sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;  // short-circuit — servlet never runs
        }
        chain.doFilter(req, res);
    }
}
This is exactly how Spring Security's filter chain works

Spring Security's entire mechanism — covered in Spring Security — is a chain of Filter implementations registered in front of DispatcherServlet. DelegatingFilterProxy bridges the Servlet API and the Spring container so that Spring-managed beans can participate in the filter chain. Understanding raw Filter here makes the Security filter chain architecture immediately readable.

JSP — Historical Context, Not a Recommendation

JSP (JavaServer Pages) allowed embedding Java code directly inside HTML, so the server could render dynamic content without routing through a servlet for every page. It was the dominant server-side rendering technology in Java from the late 1990s through the mid-2000s.

<!-- users.jsp — a realistic example showing WHY JSP fell out of favour -->
<%@ page import="java.util.List" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html><body>
<h1>Users</h1>

<%-- Scriptlet (avoid) — mixes Java and HTML, untestable, unmaintainable --%>
<%
    List users = (List) request.getAttribute("users");
    for (Object u : users) {
        out.println("<p>" + u + "</p>");  // XSS risk: no escaping
    }
%>

<%-- JSTL (the "clean" JSP way) — still JSP, but at least no scriptlets --%>
<c:forEach var="user" items="${users}">
    <p><c:out value="${user.name}"/></p>  <%-- c:out escapes HTML --%>
</c:forEach>

</body></html>
Why JSP lost — and what replaced it

JSP's fundamental problem is mixing concerns that don't belong together: presentation logic (HTML structure) and Java code in the same file. This made pages hard to test (no unit testing of JSP), hard to maintain (designers couldn't touch files with Java in them), and prone to XSS when developers forgot to escape output. Three things killed it in practice:

  • Thymeleaf (Spring's default today) — pure HTML templates that work in a browser directly, no Java code in the template, natural templating syntax with th:* attributes
  • Single-Page Applications (React, Vue, Angular) — the server returns JSON, the browser renders HTML entirely client-side; JSP becomes irrelevant
  • Freemarker / Mustache — logic-less templating that enforces separation of concerns at the language level

You will encounter JSP in legacy systems. You should understand how it works, recognise the XSS risks of raw scriptlets, and know how to migrate it — not write new JSP in 2025.

The MVC Pattern with Servlets + JSP

/*
 * Classic Servlet + JSP MVC — the pattern that Spring MVC formalized:
 *
 *  Browser → GET /users
 *                │
 *                ▼
 *  UserListServlet.doGet()          ← Controller: fetch data, put in request scope
 *      userService.findAll()
 *      request.setAttribute("users", users)
 *      request.getRequestDispatcher("/WEB-INF/views/users.jsp").forward(req, res)
 *                │
 *                ▼
 *  users.jsp                        ← View: render the data, no business logic
 *      ${users} — display the list
 *
 * Spring MVC is exactly this pattern, with DispatcherServlet as the single
 * front controller instead of one servlet per URL, and Thymeleaf instead of JSP.
 */

@WebServlet("/users")
public class UserListServlet extends HttpServlet {

    private UserService userService;  // can't @Inject here without CDI — see CDI page

    @Override
    public void init() {
        userService = new UserService();  // manual wiring without a DI container
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        req.setAttribute("users", userService.findAll());
        req.getRequestDispatcher("/WEB-INF/views/users.jsp").forward(req, res);
    }
}

Session Management

HTTP is stateless — each request carries no memory of previous ones. The Servlet API's HttpSession is the container-managed mechanism for maintaining state across requests for the same user, backed by a session ID stored in a cookie (JSESSIONID by default).

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

    if (authenticate(req.getParameter("username"), req.getParameter("password"))) {
        // getSession(true) = create if absent; getSession(false) = return null if absent
        HttpSession session = req.getSession(true);
        session.setAttribute("username", req.getParameter("username"));
        session.setMaxInactiveInterval(30 * 60);  // 30 minutes; -1 = never expire
        res.sendRedirect("/dashboard");           // PRG pattern — prevents form resubmission
    } else {
        res.sendRedirect("/login?error=invalid");
    }
}

// Logout — always invalidate, never just remove attributes
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

    HttpSession session = req.getSession(false);  // false = don't create if absent
    if (session != null) {
        session.invalidate();  // destroys the session — all attributes gone, ID invalidated
    }
    res.sendRedirect("/login");
}
Sessions and stateless REST APIs don't mix

HttpSession is a server-side state store — it breaks horizontal scaling without sticky sessions or a shared session store (Redis, Hazelcast). This is precisely why REST APIs prefer stateless authentication (JWT bearer tokens) over session-based auth. The Spring Security page covers both approaches and explains exactly when to use each. Sessions remain appropriate for traditional server-rendered web applications; they are an anti-pattern in REST APIs.

WebSocket — Bidirectional Communication from the Servlet Container

The WebSocket spec (jakarta.websocket.*) is part of the same Servlet container infrastructure. A WebSocket endpoint is deployed alongside servlets in the same WAR and managed by the same container — which is why it belongs in this section rather than a separate page.

import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;

@ServerEndpoint("/ws/chat")  // URL: ws://host/app/ws/chat
public class ChatEndpoint {

    @OnOpen
    public void onOpen(Session session) {
        log.info("Client connected: {}", session.getId());
    }

    @OnMessage
    public void onMessage(String message, Session session)
            throws IOException {
        // Echo back to the same client
        session.getBasicRemote().sendText("Echo: " + message);
    }

    @OnError
    public void onError(Throwable error, Session session) {
        log.error("WebSocket error on session {}", session.getId(), error);
    }

    @OnClose
    public void onClose(Session session, CloseReason reason) {
        log.info("Connection closed: {} — {}", session.getId(), reason.getReasonPhrase());
    }
}
WebSocket in Spring Boot uses the same @ServerEndpoint, differently wired

Spring Boot supports @ServerEndpoint directly — just declare a ServerEndpointExporter bean to register Jakarta WebSocket endpoints with the embedded container. Spring also provides its own higher-level WebSocket + STOMP abstraction (@MessageMapping) for publish-subscribe messaging patterns. Both approaches ultimately use the same Servlet container WebSocket infrastructure covered here.

Interview Questions

🎓 Junior level

Q: What is a Servlet, and what is its relationship with Spring MVC?
A Servlet is a Java class that receives an HTTP request and produces an HTTP response. Spring MVC's DispatcherServlet is itself a Servlet — it implements HttpServlet and is registered in a Servlet Container (Tomcat, Jetty). Every HTTP request in a Spring Boot application flows through DispatcherServlet before reaching any @Controller method.

Q: What is the difference between forward() and sendRedirect()?
forward() transfers control to another server-side resource without the browser knowing — the URL in the address bar doesn't change, and the original request object is reused. sendRedirect() sends a 302 to the browser, which makes a brand-new GET request — the URL changes. The Post/Redirect/Get pattern uses sendRedirect() after a POST to prevent form resubmission on browser refresh.

Q: Why shouldn't servlets have mutable instance variables?
The container creates one servlet instance and routes all concurrent requests to it across multiple threads simultaneously. A mutable instance variable is shared across all those threads without synchronisation — a race condition. Request-specific data must live in local variables inside the handler method, never in fields.

🔥 Senior level

Q: Where does a Servlet Filter run relative to DispatcherServlet, and why does that matter for Spring Security?
A Filter runs at the Servlet Container level, before DispatcherServlet ever executes. Spring Security registers its entire security mechanism as a filter chain (DelegatingFilterProxyFilterChainProxy) precisely because it needs to intercept requests before Spring's routing logic runs — so that unauthenticated requests never reach DispatcherServlet at all. A Spring HandlerInterceptor, by contrast, runs inside DispatcherServlet's pipeline and can't prevent the request from reaching the dispatcher.

Q: Why is HttpSession an anti-pattern for REST APIs, and what's the correct alternative?
HttpSession is server-side state — it exists on one JVM instance. Under horizontal scaling, a request hitting a different server finds no session unless you use sticky sessions (which defeats the purpose of load balancing) or a shared external session store (Redis, adding operational complexity). REST's statelessness constraint says each request must carry all the context the server needs — which is exactly what JWT bearer tokens provide: the user's identity and claims travel in the Authorization header on every request, no server-side state required.

Q: Why did JSP fall out of favour, and what replaced it?
JSP mixed presentation (HTML) with Java code in the same file, making it untestable, unmaintainable for UI designers, and prone to XSS when developers forgot to escape output. Three things replaced it: Thymeleaf (pure HTML templates, no Java in the file), Single-Page Applications (server returns JSON, browser renders HTML — JSP irrelevant), and logic-less templating engines (Freemarker, Mustache) that enforce separation of concerns at the language level.