Filters and Listeners: Cross Cutting the Right Way. Servlets from a practitioner view with lessons you can reuse.
The Problem We Keep Repeating
Every Java web app I see starts clean and ends with controllers doing everything. Authentication checks. Log noise. Timing. A bit of compression. Some cookie dance. It works, sure, but you pay for it with duplicated code and bugs that keep coming back. With Servlet 3.0 landing in the stacks we use today like Tomcat 7, GlassFish 3, and Jetty 7, we have a boring but powerful answer sitting right there: filters and listeners. They are the right place for cross cutting work that does not belong in your business code.
Think of filters as a gate that every request walks through and listeners as little ears that react to app start, stop, new request, new session, and similar life cycle events. Use them and your controllers can breathe again.
Three Cases You Can Ship This Week
Case 1. Login checks without touching controllers
Stop sprinkling isLoggedIn across ten servlets. Put it in one filter and be done. With Servlet 3.0 you can use annotations and skip web xml if you want.
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.*;
@WebFilter(urlPatterns = {"/app/*"})
public class AuthFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws java.io.IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
boolean loggedIn = session != null && session.getAttribute("user") != null;
boolean loginRequest = request.getRequestURI().endsWith("/login");
if (loggedIn || loginRequest) {
chain.doFilter(request, response);
} else {
response.sendRedirect(request.getContextPath() + "/login");
}
}
}This puts access control in one place. Your controllers stop caring about who you are and can focus on what they do.
Case 2. Request id and logging that actually helps
When production gets noisy you want a way to tie a log line to a request. Create one id per request in a filter and stash it in MDC if you use SLF4J or Logback. Pair it with a request listener for timing.
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import java.util.UUID;
@WebFilter("/*")
public class CorrelationFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws java.io.IOException, ServletException {
String rid = UUID.randomUUID().toString();
MDC.put("rid", rid);
req.setAttribute("rid", rid);
try {
chain.doFilter(req, res);
} finally {
MDC.remove("rid");
}
}
}import javax.servlet.*;
import javax.servlet.annotation.WebListener;
@WebListener
public class TimingListener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
sre.getServletRequest().setAttribute("t0", System.nanoTime());
}
public void requestDestroyed(ServletRequestEvent sre) {
Object t0 = sre.getServletRequest().getAttribute("t0");
if (t0 != null) {
long ms = (System.nanoTime() - (Long) t0) / 1_000_000;
System.out.println("[perf] rid=" + sre.getServletRequest().getAttribute("rid") + " took " + ms + " ms");
}
}
}Now every log line can include rid and you can see how long things take without touching your controllers.
Case 3. Startup bootstrapping and cleanup that never leaks
Creating a data source, warming caches, and closing stuff on shutdown all belong in a ServletContextListener. No more static singletons created on the first hit.
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
@WebListener
public class AppBoot implements ServletContextListener {
private BasicDataSource ds;
public void contextInitialized(ServletContextEvent sce) {
ds = new BasicDataSource();
ds.setUrl("jdbc:mysql://localhost/app");
ds.setUsername("app");
ds.setPassword("secret");
sce.getServletContext().setAttribute("ds", ds);
System.out.println("App started. DataSource ready.");
}
public void contextDestroyed(ServletContextEvent sce) {
try { ds.close(); } catch (Exception ignore) {}
System.out.println("App stopped. DataSource closed.");
}
}Any servlet can now grab the data source from context and you know it will be closed when the container stops.
Objections and Replies
We already have this check in our controllers. Great, you can remove it from there and centralize. Future fixes happen once. Less risk. Fewer bugs.
We use Spring so we can just add an interceptor. Interceptors are fine inside Spring. Filters and listeners sit at the container door. They also cover static content, JSP, or any other framework you plug in later. You can mix both. Use the filter for the broad rule and the interceptor for framework level hooks.
Web xml is painful. With Servlet 3.0 you can use @WebFilter and @WebListener. If your container is still on older servlet spec, keep web xml but it is a one time edit and done. Either way your app code gets simpler.
Async or Comet will break this. Filters still wrap the request. For async, make sure you clean up MDC or ThreadLocal data in a finally block and use the async listeners if you need deeper hooks. The pattern holds.
Security frameworks already exist. True, and they are good. A small auth filter is still useful for simple apps or for rules that sit before a bigger framework, like blocking by IP or short circuiting bots. Filters are your quick move.
Do This Next
Pick one cross cutting concern and move it out of your controllers today. You will feel the difference right away.
- Add a CorrelationFilter with a request id and put it in your log pattern. Search by that id during the next bug hunt.
- Create an AuthFilter for your private routes. Remove the repeated checks from two controllers as a start.
- Add an AppBoot context listener to set up shared resources and to close them when the server stops.
- Turn one slow spot into a measured one with the TimingListener. Keep the data and decide what to speed up later.
- Write a quick readme so the team knows where to add new cross cutting code. One page is enough.
We spend a lot of time building features. The boring glue decides if tomorrow is calm or on fire. Filters and listeners are not flashy but they pay back every week. If you are on Tomcat 7 or GlassFish 3 you already have everything you need. Keep your controllers clean. Let the container help you. That is the right way to handle cross cutting in Java web apps.