What is a Servlet Container?
A Servlet Container (also called a "Servlet Engine" or "Web Container") is the runtime environment that manages the lifecycle and execution of Java Servlets. It's the essential component that transforms your Java code into a web application capable of handling HTTP requests and generating dynamic responses.
Understanding servlet containers is fundamental to Java web development because every Java web framework—from raw servlets to Spring Boot—ultimately relies on this core technology.
Why Do Servlet Containers Exist?
In the early days of web development, creating dynamic web applications in Java was challenging. You needed to:
- Handle raw TCP/IP connections - Managing socket programming for HTTP
- Parse HTTP requests - Manually reading headers, query parameters, form data
- Manage concurrent users - Creating and managing threads for each request
- Handle sessions - Tracking users across stateless HTTP requests
- Manage security - Authentication, authorization, SSL/TLS
- Deploy and update code - Hot-deploying without server restarts
The servlet specification and servlet containers emerged to provide a standardized solution, letting developers focus on business logic instead of plumbing.
The Restaurant Kitchen Analogy:
Think of a servlet container like the kitchen management system in a restaurant:
- Takes orders from waiters → Receives HTTP requests
- Routes orders to the right chef station → Routes requests to the right servlet
- Manages multiple orders simultaneously → Handles concurrent requests with threads
- Provides cooking tools and ingredients → Provides APIs (request, response, session objects)
- Ensures dishes are prepared correctly → Manages servlet lifecycle (init, service, destroy)
What is a Servlet?
Before diving deeper into containers, let's understand what they contain. A Servlet is a Java class that handles HTTP requests and generates responses. It's the fundamental building block of Java web applications.
A Simple Servlet Example
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.*;
// A servlet is just a Java class that extends HttpServlet
public class HelloServlet extends HttpServlet {
// Called once when the servlet is first loaded
@Override
public void init() throws ServletException {
System.out.println("HelloServlet initialized!");
// Initialize resources: database connections, load config, etc.
}
// Called for every GET request
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Get data from the request
String name = request.getParameter("name");
if (name == null) name = "World";
// Set response type
response.setContentType("text/html");
// Write the response
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>Hello, " + name + "!</h1>");
out.println("</body></html>");
}
// Called once when the servlet is being unloaded
@Override
public void destroy() {
System.out.println("HelloServlet destroyed!");
// Clean up resources: close connections, release memory
}
}
The Servlet Lifecycle
The servlet container manages the complete lifecycle of every servlet:
- Loading: Container loads the servlet class into memory (on first request or at startup)
- Instantiation: Container creates exactly ONE instance of the servlet class
- Initialization: Container calls
init()method once—set up resources here - Service: Container calls
service()(doGet/doPost) for EVERY request (multi-threaded!) - Destruction: Container calls
destroy()once before unloading—cleanup here
The servlet container creates only one instance of each servlet, but calls its service methods from multiple threads simultaneously. This means:
- Servlets must be thread-safe
- Never store request-specific data in instance variables
- Use local variables or synchronized blocks for shared mutable state
// BAD - NOT thread-safe!
public class UnsafeServlet extends HttpServlet {
private String username; // Instance variable shared by all threads!
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
username = req.getParameter("user"); // Thread A writes "Alice"
// ... meanwhile Thread B writes "Bob"
resp.getWriter().println("Hello, " + username); // Could print wrong name!
}
}
// GOOD - Thread-safe
public class SafeServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String username = req.getParameter("user"); // Local variable - thread-safe
resp.getWriter().println("Hello, " + username);
}
}
What Does a Servlet Container Do?
The servlet container provides a rich set of services that your application relies on.
1. HTTP Protocol Handling
// You write this simple code...
String name = request.getParameter("name");
// The container already did all of this:
// 1. Opened a TCP socket on port 8080
// 2. Read raw bytes from the network
// 3. Parsed the HTTP request line: "GET /hello?name=John HTTP/1.1"
// 4. Parsed all headers: Content-Type, Cookie, Accept, etc.
// 5. Decoded URL-encoded parameters (%20 → space)
// 6. Created the HttpServletRequest object wrapping all this data
// 7. Created a thread to handle this request
// 8. Found and invoked your servlet's doGet method
2. Request Routing (URL Mapping)
// In web.xml or using annotations
@WebServlet("/products/*") // Matches /products, /products/123
@WebServlet("*.do") // Matches /login.do, /checkout.do
@WebServlet("/api/v1/users") // Matches exactly this URL
@WebServlet("") // Default servlet for the context root
// URL Pattern Matching Rules (in order of precedence):
// 1. Exact match: /catalog/products
// 2. Path match: /catalog/*
// 3. Extension match: *.jsp
// 4. Default: /
3. Thread Management
// Tomcat's default thread pool configuration (server.xml)
<Connector port="8080" protocol="HTTP/1.1"
maxThreads="200" <!-- Maximum concurrent requests -->
minSpareThreads="10" <!-- Threads kept ready -->
acceptCount="100" <!-- Queue size when all threads busy -->
connectionTimeout="20000" />
4. Session Management
// Getting/creating a session - container does the heavy lifting
HttpSession session = request.getSession(); // true = create if doesn't exist
// Behind the scenes, the container:
// 1. Checks for JSESSIONID cookie in the request
// 2. If found, looks up existing session in memory
// 3. If not found or expired, creates new session
// 4. Generates unique session ID (cryptographically random)
// 5. Sets JSESSIONID cookie in response
// 6. Returns HttpSession object to your code
// Store data in session
session.setAttribute("user", currentUser);
session.setAttribute("cart", shoppingCart);
// Retrieve data later (even in different servlet)
User user = (User) session.getAttribute("user");
Filters and Listeners
Beyond servlets, containers support two other powerful components.
Servlet Filters
Filters intercept requests before they reach servlets and responses after servlets generate them. They're perfect for cross-cutting concerns.
import jakarta.servlet.*;
import jakarta.servlet.annotation.*;
@WebFilter("/*") // Apply to all URLs
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
long startTime = System.currentTimeMillis();
// Log before processing
System.out.println("Request: " + req.getMethod() + " " + req.getRequestURI());
// Pass to next filter or servlet
chain.doFilter(request, response);
// Log after processing
long duration = System.currentTimeMillis() - startTime;
System.out.println("Response completed in " + duration + "ms");
}
}
Common uses for filters:
- Authentication/Authorization - Check login status before protected resources
- Logging - Log all requests and responses
- Compression - GZIP compress responses
- Character Encoding - Set UTF-8 for all requests
- CORS Headers - Add cross-origin headers to responses
Servlet Listeners
Listeners respond to lifecycle events in the container:
@WebListener
public class AppLifecycleListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// Called when application starts
System.out.println("Application starting up!");
// Initialize connection pools, load config, start background services
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// Called when application shuts down
System.out.println("Application shutting down!");
// Cleanup resources, close connections, stop threads
}
}
Popular Servlet Containers
| Container | Type | Best For | Key Features |
|---|---|---|---|
| Apache Tomcat | Servlet Container | Most Java web apps | Industry standard, widely documented |
| Eclipse Jetty | Servlet Container | Embedded use, microservices | Small footprint, fast startup |
| Undertow | Servlet Container | High performance | Non-blocking I/O, embedded in WildFly |
| GlassFish | Full App Server | Jakarta EE reference | Reference implementation |
| WildFly | Full App Server | Enterprise applications | Modular, Jakarta EE certified |
Embedded Servlet Containers
Modern frameworks like Spring Boot use embedded servlet containers—the container runs inside your application rather than your application inside the container.
// Spring Boot - embedded Tomcat by default
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- Includes embedded Tomcat -->
</dependency>
// To switch to Jetty:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Why Embedded Containers?
- Simpler Deployment: Just copy one JAR file, run with
java -jar - Version Control: Container version is part of your project dependencies
- Development Parity: Same container in dev, test, and production
- Cloud-Native: Perfect for Docker containers and Kubernetes
The javax to jakarta Migration
In 2017, Oracle transferred Java EE to the Eclipse Foundation, creating Jakarta EE. Due to Oracle's trademark on "Java," all packages changed:
| Servlet Version | Java Version | Package |
|---|---|---|
| Servlet 4.0 and earlier | Java 8+ | javax.servlet |
| Servlet 5.0+ | Java 8+ | jakarta.servlet |
When upgrading to Tomcat 10+, Spring Boot 3+, or Jakarta EE 9+, you must update all imports from javax to jakarta.
Summary
- Servlet Container: The runtime environment that manages servlet lifecycle and handles HTTP requests
- Single Instance, Multiple Threads: Only one servlet instance handles all requests concurrently
- Lifecycle: init() once → service() many times → destroy() once
- Filters: Intercept requests/responses for cross-cutting concerns
- Listeners: React to lifecycle events in the container
- Embedded Containers: Modern approach where container is inside your app
- Jakarta Migration: javax.* packages became jakarta.* in Servlet 5.0+
- Thread Safety: Always use local variables for request data