Java Academy

Master Java Development

Menu

Core Java

Servlets

JDBC

Microservices

Build Tools

APIs

Servlets · Lesson 2

Servlet Lifecycle

Understanding the Servlet Lifecycle

The servlet lifecycle describes the key phases a servlet goes through from creation to destruction. A servlet container manages this lifecycle: initialization (happens once), request handling (happens many times), and destruction (happens once at shutdown). Understanding this lifecycle helps you implement initialization logic efficiently and ensure proper resource cleanup.

Initialization: The init() Method

When the servlet container starts or receives the first request for a servlet, it calls the init() method once. Use init() to perform one-time setup: loading configuration files, initializing database connections, preparing resources that will be reused across many requests. This is much more efficient than initializing per-request.

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;

public class DatabaseServlet extends HttpServlet {
    private Connection dbConnection;
    
    @Override
    public void init() throws ServletException {
        // Called once when servlet is created
        try {
            // Initialize database connection
            Class.forName("com.mysql.jdbc.Driver");
            dbConnection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydb", "root", "password"
            );
            System.out.println("Database connection initialized");
        } catch (Exception e) {
            throw new ServletException("Failed to initialize database", e);
        }
    }
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // Use the initialized connection
        if (dbConnection != null) {
            // Query database using connection
            response.getWriter().println("Database connected!");
        }
    }
}

Request Handling: Service Request and Response

After initialization, the servlet enters the request-response phase. For each HTTP request, the container calls a method like doGet(), doPost(), doPut(), or doDelete(). Each request is typically handled by a separate thread, so the servlet must be thread-safe if it accesses shared resources. This phase continues until the servlet is shut down.

public class CounterServlet extends HttpServlet {
    private int requestCount = 0;  // Shared state
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // Problem: requestCount is not thread-safe!
        // Multiple threads can read/modify simultaneously
        requestCount++;
        
        response.setContentType("text/plain");
        response.getWriter().println("Total requests: " + requestCount);
    }
}

// Better: Thread-safe approach
public class ThreadSafeCounterServlet extends HttpServlet {
    private volatile int requestCount = 0;  // volatile helps, but sync is better
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        synchronized (this) {  // Ensure thread safety
            requestCount++;
        }
        
        response.setContentType("text/plain");
        response.getWriter().println("Total requests: " + requestCount);
    }
}

Destruction: The destroy() Method

When the servlet container shuts down (or the application is undeployed), it calls the destroy() method once. Use destroy() to clean up resources: close database connections, release file handles, stop background threads. Proper cleanup prevents resource leaks.

public class ResourcefulServlet extends HttpServlet {
    private Connection dbConnection;
    private FileWriter logFile;
    
    @Override
    public void init() throws ServletException {
        try {
            // Initialize resources
            dbConnection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "pass");
            logFile = new FileWriter("servlet.log", true);
            logFile.write("Servlet initialized\n");
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            // Use resources
            logFile.write("Request received\n");
            logFile.flush();
        } catch (IOException e) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }
    
    @Override
    public void destroy() {
        // Clean up resources
        try {
            if (dbConnection != null && !dbConnection.isClosed()) {
                dbConnection.close();
                System.out.println("Database connection closed");
            }
            if (logFile != null) {
                logFile.write("Servlet destroyed\n");
                logFile.close();
                System.out.println("Log file closed");
            }
        } catch (Exception e) {
            System.err.println("Error during destroy: " + e.getMessage());
        }
    }
}

Servlet Container and Context

The servlet container (Tomcat, Jetty) manages multiple servlets. The ServletContext represents the entire web application and provides access to shared resources like application attributes, init parameters, and the file system. You can access it via getServletContext().

public class ContextServlet extends HttpServlet {
    @Override
    public void init() throws ServletException {
        ServletContext context = getServletContext();
        
        // Get init parameters (from web.xml or annotations)
        String appName = context.getInitParameter("appName");
        String version = context.getInitParameter("version");
        
        // Store application-wide data
        context.setAttribute("applicationName", appName);
        context.setAttribute("startTime", System.currentTimeMillis());
    }
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        ServletContext context = getServletContext();
        
        // Retrieve application-wide data
        String appName = (String) context.getAttribute("applicationName");
        Long startTime = (Long) context.getAttribute("startTime");
        
        response.setContentType("text/plain");
        response.getWriter().println("App: " + appName);
        response.getWriter().println("Uptime: " + (System.currentTimeMillis() - startTime) + " ms");
    }
}
Lifecycle PhaseMethodCalledPurpose
Initializationinit()Once, at startupSet up resources (DB, files)
Request HandlingdoGet(), doPost(), etc.Many timesProcess HTTP requests
Destructiondestroy()Once, at shutdownClean up resources

Common Mistake: Not making servlets thread-safe. Each request is handled by a different thread, so shared fields can cause race conditions. Either make fields immutable, use synchronized, or use request-scoped objects (stored in request attributes). Never assume only one request runs at a time.

Best Practices for Servlet Lifecycle

  • Initialize expensive resources in init(), not in request handlers
  • Always clean up in destroy(): close connections, files, streams
  • Use volatile or synchronized for shared mutable state
  • Prefer immutable objects: they're inherently thread-safe
  • Use ServletContext for application-wide data shared across servlets
  • Handle exceptions in init() and propagate as ServletException
  • Test shutdown scenarios: verify resources are properly released
  • Avoid long-running operations in request handlers: offload to background tasks

Real-World Example: Database-Backed Web Application

A servlet-based application initializes a database connection pool in init() once at startup. Each incoming request is handled by a separate thread that borrows a connection from the pool (thread-safe), executes a query, and returns the connection. The destroy() method closes all connections in the pool when the application shuts down. By managing the lifecycle carefully, the application efficiently serves hundreds of concurrent users without leaking resources or crashing on shutdown.

Quiz Check

Quick Knowledge Test

Loading quiz questions...