Sessions, Cookies & Filters

Web Listeners

18 min Lesson 8 of 13

Web Listeners

Servlet filters intercept individual requests. But many cross-cutting concerns — startup initialisation, connection-pool warm-up, metrics counters, audit trails — have nothing to do with any single request. They care about lifecycle events: the application started, a session was created, a request attribute was removed. That is exactly what web listeners are for.

The Servlet specification (Jakarta EE 10, package jakarta.servlet) defines eight listener interfaces organised into three scopes: context (the whole application), session, and request. You implement the interface, annotate the class with @WebListener, and the container calls your methods at the right moments — no wiring, no XML (unless you prefer it).

Context Listeners — Application Scope

ServletContextListener is the most important listener you will write. Its two methods bracket the entire lifetime of the web application:

  • contextInitialized(ServletContextEvent) — called once after the container finishes loading the app but before any request is processed. Use it for one-time startup work.
  • contextDestroyed(ServletContextEvent) — called once when the container is shutting the app down (undeploy, server restart). Use it to release resources gracefully.
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletContextListener; import jakarta.servlet.annotation.WebListener; import javax.sql.DataSource; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; @WebListener public class AppLifecycleListener implements ServletContextListener { private HikariDataSource pool; @Override public void contextInitialized(ServletContextEvent sce) { // Build the connection pool once at startup HikariConfig cfg = new HikariConfig(); cfg.setJdbcUrl(System.getenv("DB_URL")); cfg.setUsername(System.getenv("DB_USER")); cfg.setPassword(System.getenv("DB_PASS")); cfg.setMaximumPoolSize(10); pool = new HikariDataSource(cfg); // Stash it in the context so every servlet can reach it ServletContext ctx = sce.getServletContext(); ctx.setAttribute("dataSource", pool); ctx.log("Connection pool initialised: " + pool.getPoolName()); } @Override public void contextDestroyed(ServletContextEvent sce) { if (pool != null && !pool.isClosed()) { pool.close(); // returns all connections, shuts pool threads sce.getServletContext().log("Connection pool closed."); } } }

Any servlet can now retrieve the pool with (DataSource) request.getServletContext().getAttribute("dataSource") — no static singletons, no dependency-injection framework required for this pattern.

Why not a static singleton? Storing the pool in ServletContext ties its lifecycle to the application rather than the JVM. When the app is redeployed without restarting the server, contextDestroyed runs first, closing the pool cleanly. A static field would outlive the context and leak connections to the old class loader.

ServletContextAttributeListener is the companion interface: it fires attributeAdded, attributeReplaced, and attributeRemoved whenever anyone calls setAttribute / removeAttribute on the context. It is useful for auditing or reacting to configuration changes at runtime.

Session Listeners — Session Scope

HttpSessionListener tracks session birth and death across all users:

  • sessionCreated(HttpSessionEvent) — fired when request.getSession(true) creates a brand-new session.
  • sessionDestroyed(HttpSessionEvent) — fired when a session is invalidated (session.invalidate()) or times out.
import jakarta.servlet.annotation.WebListener; import jakarta.servlet.http.HttpSessionEvent; import jakarta.servlet.http.HttpSessionListener; import java.util.concurrent.atomic.AtomicInteger; @WebListener public class ActiveSessionCounter implements HttpSessionListener { // Thread-safe counter shared across all requests private static final AtomicInteger ACTIVE = new AtomicInteger(0); @Override public void sessionCreated(HttpSessionEvent se) { int count = ACTIVE.incrementAndGet(); se.getSession().getServletContext() .log("Session created. Active sessions: " + count); } @Override public void sessionDestroyed(HttpSessionEvent se) { int count = ACTIVE.decrementAndGet(); se.getSession().getServletContext() .log("Session destroyed. Active sessions: " + count); } public static int getActiveCount() { return ACTIVE.get(); } }
Do not read session attributes in sessionDestroyed when the session timed out. By the time the container calls sessionDestroyed for a timed-out session, the session attributes may already have been cleared. Read the attributes you need in sessionCreated (or store them elsewhere first) if you want to use them at destruction time. When session.invalidate() is called explicitly the attributes are still present at the point of the call.

HttpSessionAttributeListener fires on attribute changes within any session — useful for tracking when a user object is placed into or removed from a session (login/logout audit trail).

HttpSessionBindingListener is different: it is implemented on the value object itself (e.g., your User class), not on a separate listener class. When an instance implementing this interface is set as a session attribute, the container calls valueBound on it; when it is removed, valueUnbound is called. This lets the object manage its own cleanup without a global listener scanning every session.

HttpSessionActivationListener supports passivation (serialising sessions to disk or another node). sessionWillPassivate is called before serialisation; sessionDidActivate after deserialisation. Implement this on any session attribute that holds a non-serialisable resource (e.g., a live socket) so it can release and re-acquire it correctly.

Request Listeners — Request Scope

ServletRequestListener is the request-scoped equivalent of ServletContextListener. Its methods surround every single HTTP request that enters the application:

import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletRequestEvent; import jakarta.servlet.ServletRequestListener; import jakarta.servlet.annotation.WebListener; import jakarta.servlet.http.HttpServletRequest; @WebListener public class RequestTimingListener implements ServletRequestListener { private static final String START_ATTR = "req.startNanos"; @Override public void requestInitialized(ServletRequestEvent sre) { sre.getServletRequest().setAttribute(START_ATTR, System.nanoTime()); } @Override public void requestDestroyed(ServletRequestEvent sre) { ServletRequest req = sre.getServletRequest(); Long start = (Long) req.getAttribute(START_ATTR); if (start == null) return; long ms = (System.nanoTime() - start) / 1_000_000; String uri = ((HttpServletRequest) req).getRequestURI(); req.getServletContext().log(uri + " completed in " + ms + " ms"); } }

Unlike a filter, this listener does not sit in the processing chain, so it cannot block or modify the request. Its role is purely observational — telemetry, per-request MDC (Mapped Diagnostic Context) setup for logging, and similar cross-cutting concerns that should not affect the response.

Choosing the Right Listener

  • Startup / shutdown work (pool init, cache pre-warm, scheduled job start/stop) → ServletContextListener
  • Counting active users, enforcing concurrent-login limitsHttpSessionListener
  • Audit trail for login/logoutHttpSessionAttributeListener or HttpSessionBindingListener
  • Passivating clustered sessionsHttpSessionActivationListener
  • Per-request telemetry, MDC setupServletRequestListener

Listener Ordering and Thread Safety

Multiple listeners of the same type are called in the order the container discovers them (annotation-based discovery order is unspecified; use web.xml <listener> entries if ordering matters). contextInitialized and contextDestroyed are called on a single thread; session and request listeners may be called concurrently across many threads. Any mutable state shared between listener invocations must be thread-safe — use AtomicInteger, ConcurrentHashMap, or synchronized blocks accordingly.

In Spring Boot you do not need @WebListener — register listeners as Spring beans using @Bean methods that return EventListener registrations, or implement the relevant interface and annotate with @Component. Spring wraps the Servlet container events in its own application-event system, so you can also listen for ApplicationReadyEvent or ContextClosedEvent instead of the raw Servlet listener interfaces. But when you work with plain Jakarta EE containers (Tomcat, Jetty, WildFly without Spring), the raw listener interfaces are exactly what you reach for.

Summary

Web listeners give you lifecycle hooks at three scopes. Use ServletContextListener to initialise and tear down application-wide resources at startup and shutdown — connection pools, caches, background threads. Use HttpSessionListener to count sessions, enforce limits, or keep an audit log of logins and logouts. Use ServletRequestListener for observability: timing, per-request logging context, and metrics that should not alter the response. The right listener used in the right scope is far cleaner than scattering the same logic across every servlet or filter.