A Jakarta Servlet filter can inspect or wrap a request and response before the target servlet runs and after it returns. Filters are appropriate for transport-wide concerns such as correlation IDs, authentication integration, compression, and response headers. They should not contain the application’s business workflow.
This lesson assumes the servlet request lifecycle. After it, you can map a filter to the intended requests, call FilterChain exactly when processing should continue, preserve response behavior with wrappers, and avoid leaking request-specific state between threads.
A Servlet Filter is a Java class that intercepts HTTP requests and responses before they reach a Servlet (or after the Servlet processes them). Filters implement the jakarta.servlet.Filter interface and are configured to intercept specific URL patterns.
Filters are ideal for cross-cutting concerns that apply to multiple Servlets:
package com.example.filters;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.*;
import java.io.*;
// Apply to all URLs
@WebFilter("/*")
public class LoggingFilter implements Filter {
private FilterConfig filterConfig;
// Called once when filter is initialized
@Override
public void init(FilterConfig config) throws ServletException {
this.filterConfig = config;
System.out.println("LoggingFilter initialized");
}
// Called for every request matching the URL pattern
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
long startTime = System.currentTimeMillis();
String uri = req.getRequestURI();
String method = req.getMethod();
String ip = req.getRemoteAddr();
System.out.println("[REQUEST] " + method + " " + uri + " from " + ip);
// Pass request to next filter or servlet
chain.doFilter(request, response);
// Post-processing (after servlet response)
long duration = System.currentTimeMillis() - startTime;
int status = resp.getStatus();
System.out.println("[RESPONSE] " + method + " " + uri
+ " -> " + status + " (" + duration + "ms)");
}
// Called once when filter is destroyed
@Override
public void destroy() {
System.out.println("LoggingFilter destroyed");
}
}
// Protect /admin/* URLs
@WebFilter("/admin/*")
public class AuthFilter implements Filter {
private static final String[] PUBLIC_PATHS = {"/login", "/register", "/public"};
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String path = req.getRequestURI().substring(req.getContextPath().length());
// Check if path is public
for (String publicPath : PUBLIC_PATHS) {
if (path.startsWith(publicPath)) {
chain.doFilter(request, response); // Allow through
return;
}
}
// Check if user is logged in
HttpSession session = req.getSession(false);
boolean loggedIn = (session != null && session.getAttribute("username") != null);
if (loggedIn) {
chain.doFilter(request, response); // Allow through
} else {
// Redirect to login
resp.sendRedirect(req.getContextPath() + "/login?redirect=" + path);
}
}
@Override public void init(FilterConfig config) {}
@Override public void destroy() {}
}
<!-- Filter ordering is defined by order in web.xml -->
<!-- Filters execute in the order they are declared -->
<filter>
<filter-name>LoggingFilter</filter-name>
<filter-class>com.example.filters.LoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LoggingFilter</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>
The container constructs the configured chain and calls each filter’s doFilter method. Calling chain.doFilter passes control to the next filter or target resource; code after that call runs while the response unwinds. A filter may stop the chain for a valid reason, such as rejecting invalid authentication, but it must then complete the response with an appropriate status and body.
Ordering changes behavior. A correlation ID should exist before logging uses it, authentication should run before authorization, and compression belongs after code has produced response content. Keep each filter focused so ordering remains visible instead of embedding several unrelated policies in one class.
Map filters narrowly by URL pattern, servlet name, and dispatcher types. REQUEST handles normal client dispatches; FORWARD, INCLUDE, ERROR, and ASYNC represent different container flows. Enabling every dispatcher without a reason can run the same filter more than once for one logical request.
Filter instances are shared across concurrent requests. Do not store a request, user, mutable buffer, or per-request timer in instance fields. Use local variables or request attributes, and release ThreadLocal values in a finally block if an integration requires them.
A wrapper can observe or transform request and response behavior, but it must preserve the original contract. Capturing a response body requires handling character encoding, writer versus output stream rules, status, headers, and committed responses. Prefer container features for standard compression and caching when they already solve the need.
Test both allowed and rejected paths. Verify the target servlet runs once on the allowed path, does not run on rejection, and that exceptions still trigger cleanup. These assertions catch missing chain calls and accidental double invocation.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.