Sessions, Cookies & Filters

Introduction to Filters

18 min Lesson 6 of 13

Introduction to Filters

A servlet filter is a reusable component that intercepts every HTTP request before it reaches its target servlet (or JSP) and every HTTP response before it leaves the server. Filters form a pipeline — the filter chain — and each filter decides independently whether to pass the request along, modify it, or stop it entirely. This architecture cleanly separates cross-cutting concerns (logging, authentication checks, compression, CORS headers) from your business servlets.

The Filter Contract

Every filter implements the jakarta.servlet.Filter interface, which declares three methods:

  • init(FilterConfig config) — called once when the container instantiates the filter. Use it to read init-params or acquire resources.
  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain) — called on every matching request. This is where your logic lives.
  • destroy() — called once before the container removes the filter. Release any resources acquired in init.
Default methods since Servlet 6 (Jakarta EE 10): init() and destroy() both have empty default implementations in the interface, so you only override them when you have something meaningful to do. doFilter remains abstract and must always be implemented.

The Filter Chain

The container builds an ordered chain of all filters whose URL pattern matches the current request. When you call chain.doFilter(request, response) inside your filter, control passes to the next filter in the chain (or to the target servlet if no more filters remain). You can run logic before that call (pre-processing) and logic after it returns (post-processing). If you never call chain.doFilter you short-circuit the chain — the target servlet is never invoked. This is the mechanism authentication filters use to block unauthenticated requests.

package com.example.filters; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/*") // matches every URL in the app public class TimingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long start = System.currentTimeMillis(); // --- PRE-PROCESSING --- chain.doFilter(request, response); // hand off to next filter / servlet long elapsed = System.currentTimeMillis() - start; // --- POST-PROCESSING --- System.out.printf("[TimingFilter] %s ms%n", elapsed); } }

Notice the structure: everything before chain.doFilter runs on the way in, everything after runs on the way out. The target servlet executes inside that gap.

Registering a Filter with @WebFilter

The @WebFilter annotation (available since Servlet 3.0) is the modern, zero-XML way to declare a filter. Key attributes:

  • value / urlPatterns — URL patterns this filter applies to. Supports wildcards: "/api/*", "*.json".
  • servletNames — target specific servlet names instead of URL patterns.
  • filterName — override the default name (class name).
  • initParams — pass @WebInitParam key-value pairs readable in init(FilterConfig).
  • dispatcherTypes — control whether the filter fires on REQUEST, FORWARD, INCLUDE, ERROR, or ASYNC dispatches. Defaults to REQUEST only.
@WebFilter( urlPatterns = { "/admin/*", "/api/*" }, filterName = "AuthFilter", initParams = { @WebInitParam(name = "loginPage", value = "/login") } ) public class AuthFilter implements Filter { private String loginPage; @Override public void init(FilterConfig cfg) { loginPage = cfg.getInitParameter("loginPage"); // "/login" } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { var httpReq = (jakarta.servlet.http.HttpServletRequest) req; var httpRes = (jakarta.servlet.http.HttpServletResponse) res; if (httpReq.getSession(false) != null && httpReq.getSession().getAttribute("user") != null) { chain.doFilter(req, res); // authenticated — continue } else { httpRes.sendRedirect(httpReq.getContextPath() + loginPage); // block + redirect } } }
Cast to HttpServletRequest / HttpServletResponse when you need HTTP-specific APIs. The doFilter signature uses the base ServletRequest/ServletResponse types (which also cover non-HTTP protocols), but in a web application the objects are always HTTP variants. Cast early and assign to a local variable rather than casting repeatedly.

Filter Ordering

When multiple filters match the same URL, the container must pick an order. The rule is:

  • Annotation-registered filters (@WebFilter): the Servlet specification does not guarantee any order among them. The order is implementation-dependent.
  • web.xml-registered filters: execute in the order their <filter-mapping> elements appear in the file.
  • Mixed: XML-declared filters run first, then annotation-declared filters in undefined order.

If ordering matters — and for security filters it almost always does — declare your filters in web.xml or use a ServletContextListener / ProgrammaticRegistration approach via ServletContext.addFilter().

<!-- web.xml — guaranteed ordering --> <filter> <filter-name>TimingFilter</filter-name> <filter-class>com.example.filters.TimingFilter</filter-class> </filter> <filter-mapping> <filter-name>TimingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.example.filters.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping>

In this configuration TimingFilter wraps AuthFilter: the request enters TimingFilter, then (if the URL matches) AuthFilter, then the target servlet. The response unwinds in reverse — servlet, AuthFilter post, TimingFilter post.

Wrapping the Request or Response

Filters can not only inspect but also replace the request or response objects by wrapping them. The API provides HttpServletRequestWrapper and HttpServletResponseWrapper for this purpose. A common use-case is capturing the response body (for logging or compression) by substituting a wrapper that buffers output before forwarding it downstream.

// A trivial wrapper that forces a specific character encoding on every request public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); // original objects passed through unchanged } }
Never forget to call chain.doFilter(). If your filter conditionally skips it (e.g., only on auth failure), make sure the else-branch still writes a complete response (redirect, error status, or body). Returning from doFilter without calling the chain and without writing a response leaves the client hanging with an empty 200.

When to Use Filters vs. Interceptors

Filters are a Servlet API feature and operate at the HTTP layer — they see raw requests and responses. Spring MVC provides its own HandlerInterceptor that operates at the dispatcher layer, after Spring has resolved the handler method. Choose filters for concerns that are truly transport-level (security headers, encoding, logging) and interceptors for concerns tied to Spring's request lifecycle (authentication with Spring Security, audit logging tied to controller names, locale resolution). Both mechanisms are complementary and commonly used together in production.

Summary

The Filter interface and the filter chain are the Servlet API's answer to cross-cutting concerns. Implement doFilter, call chain.doFilter to pass control forward, and place logic before or after that call for pre/post processing. Use @WebFilter for simple cases and web.xml (or programmatic registration) when ordering is critical. The next lesson builds on this foundation with practical, production-grade filter implementations.