Filters
In Java Web development, filters are core components of the Servlet specification designed to intercept and preprocess both incoming requests and outgoing responses before they reach or leave a target servlet. By leveraging filters, developers can cleanly decouple cross-cutting concerns—such as authentication, character encoding, and audit logging—from core business logic, achieving a centralized and modular approach to request processing.
Filters operate on an intercepting chain model. When a client initiates a request, it does not hit the target servlet immediately; instead, it flows through a registered sequence of filters managed by the container:
- The container evaluates the request URL against filter mapping rules to gather all matching candidates.
- These qualifying filters are organized into a filter chain based on their configuration order.
- The request flows sequentially through each filter, advancing only when a filter explicitly invokes the pass-through method.
- Once the servlet processes the request, the response travels backward through the filter chain in reverse order before finally returning to the client.
It is worth noting that filters can either pass requests along or terminate the flow entirely by returning a response directly—such as issuing a redirect to a login page when an unauthenticated session is detected.
Creating a custom filter requires implementing the javax.servlet.Filter interface (or jakarta.servlet.Filter in Jakarta EE 9 and later). Consider the following example of an encoding filter that ensures uniform character sets across the application:
@WebFilter("/*")
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Invoked once during container startup; ideal for reading init parameters.
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
chain.doFilter(request, response); // Passes control to the next filter or servlet.
}
@Override
public void destroy() {
// Invoked once during container shutdown to release resources.
}
}
The core logic resides inside the doFilter method, where chain.doFilter() delegates control to the subsequent component. Omitting this call intercepts the request entirely, halting any downstream filters and servlets from executing.
In addition to the @WebFilter annotation, filters can be declared in web.xml for centralized management of routing rules:
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>com.example.filter.EncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
The Filter Lifecycle
A filter's lifecycle is managed entirely by the servlet container and consists of three distinct phases:
- Initialization: Upon application startup, the container instantiates the filter and invokes its
init()method exactly once. - Interception: Whenever a matching request arrives, the container invokes the
doFilter()method, which may occur repeatedly. - Destruction: When the container shuts down or the application unloads, the
destroy()method is called once to clean up.
Since filter instances are typically treated as singletons within an application runtime, developers must avoid storing request-specific state in member variables to prevent concurrency issues.
Filter Chains and Execution Order
When multiple filters match a given request, they form a pipeline. Requests traverse the chain in the order of their configuration, while responses travel back in reverse order. In web.xml, execution sequence is determined by the physical placement order of <filter-mapping> tags; annotation-based filters generally sort alphabetically by class name. Therefore, filters requiring strict ordering—such as setting character encoding before performing authentication—should be explicitly configured via XML.
A Classic Use Case: Authentication Checks
Authentication checking is one of the most prevalent applications of filters. The following example intercepts restricted paths to verify user sessions:
@WebFilter(urlPatterns = {"/user/*", "/order/*"})
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
Object user = request.getSession().getAttribute("loginUser");
if (user != null) {
chain.doFilter(req, resp); // Authenticated, proceed normally.
} else {
response.sendRedirect(request.getContextPath() + "/login.jsp");
}
}
}
By specifying precise patterns via urlPatterns, sensitive resources are rigorously protected while public endpoints remain accessible without accidental blocks.
Best Practices
When incorporating filters into production projects, keep the following recommendations in mind:
- Keep interception scopes precise; avoid overusing broad rules like
/*to prevent unnecessary performance overhead. - Reserve filters strictly for cross-cutting, infrastructure-level logic, leaving fine-grained workflow control to framework-level constructs like Spring MVC interceptors (
HandlerInterceptor). - Remember that code placed after
chain.doFilter()executes during the return trip of the response, making it useful for measuring execution time or wrapping response streams. - Always design filters to be stateless and thread-safe.
Mastering the principles and practical usage of filters empowers developers to handle cross-cutting concerns with elegance, making it an indispensable skill in Java Web engineering.