Spring Boot Built-in Filters Explained: Usage, Customization, and Performance Tips
This article provides a comprehensive guide to Spring Boot's built-in servlet filters, shows how to implement custom filters using the Filter interface, OncePerRequestFilter, and FilterRegistrationBean, explains filter ordering and configuration, and presents practical scenarios with performance‑optimisation best practices.
Introduction
Filters are the core mechanism for handling cross‑cutting concerns such as logging, security, CORS, compression, and request tracing in Java web applications. Spring Boot automatically registers a set of built‑in filters and also supports flexible custom filter implementations.
Servlet Filter Basics
A servlet filter follows a simple lifecycle: init() (executed once), doFilter() (executed for each request), and destroy() (executed on container shutdown).
public class SimpleFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String param = filterConfig.getInitParameter("configParam");
System.out.println("Filter init, param: " + param);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
System.out.println("[" + System.currentTimeMillis() + "] " +
httpRequest.getMethod() + " " + httpRequest.getRequestURI());
chain.doFilter(request, response);
System.out.println("Response completed: " + httpRequest.getRequestURI());
}
@Override
public void destroy() {
System.out.println("Filter destroyed");
}
}Spring Boot Built-in Filters
Spring Boot auto‑configures the following filters (ordered by execution priority):
CharacterEncodingFilter – sets request/response character encoding.
FormContentFilter – parses form data for PUT, PATCH, DELETE.
HiddenHttpMethodFilter – converts a hidden _method field to the actual HTTP method.
CORS Filter – handles cross‑origin requests.
Spring Security filter chain – provides authentication and authorization.
WebMvcMetricsFilter – collects request metrics via Micrometer.
Example of the CharacterEncodingFilter configuration:
@ConfigurationProperties(prefix = "spring.http.encoding")
public class HttpEncodingAutoConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequest(this.properties.isForceRequest());
filter.setForceResponse(this.properties.isForceResponse());
return filter;
}
}Corresponding application.yml snippet:
spring:
http:
encoding:
enabled: true
charset: UTF-8
force: true
force-request: true
force-response: trueCustom Filter Implementations
1. Implementing the Filter interface
@Component
@WebFilter(urlPatterns = "/*", filterName = "loggingFilter")
public class RequestLoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestId = UUID.randomUUID().toString().replace("-", "");
long start = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - start;
logger.info("Request ID: {} | Method: {} | URI: {} | Status: {} | Duration: {}ms",
requestId, httpRequest.getMethod(), httpRequest.getRequestURI(),
httpResponse.getStatus(), duration);
}
}
}2. Extending OncePerRequestFilter
@Component
@Order(1)
public class ApiAuditFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
private final AuditLogService auditLogService;
private static final Set<String> AUDIT_PATHS = Set.of("/api/orders/","/api/payments/","/api/users/","/api/admin/");
private static final Set<String> SENSITIVE_FIELDS = Set.of("password","token","secret","creditCard","ssn");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String requestId = UUID.randomUUID().toString().replace("-", "");
long start = System.currentTimeMillis();
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
wrappedResponse.setHeader("X-Trace-ID", requestId);
try {
chain.doFilter(wrappedRequest, wrappedResponse);
if (shouldAudit(request.getRequestURI())) {
logAudit(requestId, wrappedRequest, wrappedResponse, System.currentTimeMillis() - start);
}
} finally {
wrappedResponse.copyBodyToResponse();
}
}
private boolean shouldAudit(String uri) {
return AUDIT_PATHS.stream().anyMatch(uri::startsWith);
}
// logAudit method (omitted for brevity) sanitises request/response bodies and saves them asynchronously.
}3. Using FilterRegistrationBean
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestLoggingFilter());
registration.addUrlPatterns("/*");
registration.setName("loggingFilter");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 10);
registration.addInitParameter("includeRequestBody", "true");
registration.addInitParameter("includeResponseBody", "true");
registration.addInitParameter("maxPayloadLength", "10000");
return registration;
}
}Filter Chain Configuration and Ordering
Filters are executed in ascending order of their order value (smaller value = higher priority). The diagram below illustrates the request flow:
Request → Filter1 (Order1) → Filter2 (Order10) → Filter3 (Order20) → Controller
↓ ↓ ↓
Response ← Filter3 ← Filter2 ← Filter1Three common ways to set order:
Annotate the filter bean with @Order.
Implement Ordered and return a custom order value.
Configure the order via FilterRegistrationBean.setOrder().
Practical Scenarios
Scenario 1 – API Gateway Request Logging & Auditing
The ApiAuditFilter records detailed request/response information, sanitises sensitive fields, and stores the audit log asynchronously.
Scenario 2 – Rate‑Limiting Filter
@Component
@Order(2)
public class RateLimitFilter extends OncePerRequestFilter {
private final RateLimitProperties properties;
private final LoadingCache<String, RateLimiter> ipRateLimiter = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES)
.build(new CacheLoader<String, RateLimiter>() {
@Override public RateLimiter load(String key) { return RateLimiter.create(properties.getIpLimitPerSecond()); }
});
private final LoadingCache<String, RateLimiter> userRateLimiter = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES)
.build(new CacheLoader<String, RateLimiter>() {
@Override public RateLimiter load(String key) { return RateLimiter.create(properties.getUserLimitPerSecond()); }
});
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String uri = request.getRequestURI();
if (isExcluded(uri)) { chain.doFilter(request, response); return; }
String clientIp = getClientIp(request);
if (!tryAcquire(ipRateLimiter, clientIp)) { sendRateLimitResponse(response, "IP request rate too high", clientIp); return; }
String userId = getCurrentUserId(request);
if (userId != null && !tryAcquire(userRateLimiter, userId)) {
sendRateLimitResponse(response, "User request rate too high", userId); return; }
chain.doFilter(request, response);
if (userId != null) {
RateLimiter limiter = userRateLimiter.getUnchecked(userId);
response.setHeader("X-RateLimit-Remaining", String.valueOf((int) limiter.getAvailablePermits()));
}
}
// Helper methods (isExcluded, tryAcquire, getClientIp, getCurrentUserId, sendRateLimitResponse) omitted for brevity.
}Scenario 3 – Response Compression Filter
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 10)
public class CompressionFilter extends OncePerRequestFilter {
private static final int COMPRESSION_THRESHOLD = 1024; // bytes
private static final List<String> COMPRESSIBLE_MIME_TYPES = List.of(
"application/json","application/xml","text/html","text/plain","text/css","application/javascript");
private static final List<String> EXCLUDED_MIME_TYPES = List.of(
"image/png","image/jpeg","image/gif","application/pdf","application/zip");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String acceptEncoding = request.getHeader("Accept-Encoding");
if (acceptEncoding == null || !acceptEncoding.contains("gzip")) { chain.doFilter(request, response); return; }
CompressionResponseWrapper wrapped = new CompressionResponseWrapper(response);
try { chain.doFilter(request, wrapped); }
finally { wrapped.compressIfNeeded(); }
}
// Inner class CompressionResponseWrapper implements the actual GZIP logic (omitted for brevity).
}Scenario 4 – Multi‑Tenant Context Filter
@Component
@Order(3)
public class TenantContextFilter extends OncePerRequestFilter {
private static final String TENANT_HEADER = "X-Tenant-ID";
private static final String TENANT_SUBDOMAIN_PATTERN = "^([a-zA-Z0-9-]+)\\.(?:example\\.com|localhost)$";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
String tenantId = request.getHeader(TENANT_HEADER);
if (tenantId == null || tenantId.isEmpty()) tenantId = extractFromSubdomain(request);
if (tenantId == null || tenantId.isEmpty()) tenantId = extractFromToken(request);
if (tenantId != null && !tenantId.isEmpty()) {
TenantContext.setTenantId(tenantId);
MDC.put("tenantId", tenantId);
}
chain.doFilter(request, response);
} finally {
TenantContext.clear();
MDC.clear();
}
}
// extractFromSubdomain, extractFromToken (JWT parsing) omitted for brevity.
}Performance Optimisation & Best Practices
1. Avoid Time‑Consuming Operations Inside Filters
Never perform blocking database queries or synchronous HTTP calls directly in doFilterInternal. Use caches or asynchronous processing instead.
2. Use Request/Response Wrappers Judiciously
Wrap only when the request size is reasonable and the endpoint requires body inspection. Large file uploads should bypass caching to avoid memory pressure.
3. Asynchronous Filters
For logging or tracing that does not need to block the request thread, extend AsyncFilterSupport and perform work after the request has been dispatched.
4. Filter Performance Monitoring
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
public class FilterPerformanceMonitor extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
public FilterPerformanceMonitor(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; }
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
long start = System.nanoTime();
try { chain.doFilter(request, response); }
finally {
long duration = System.nanoTime() - start;
Timer timer = meterRegistry.timer("filter.execution.time", "filter", getClass().getSimpleName(), "uri", request.getRequestURI());
timer.record(duration, java.util.concurrent.TimeUnit.NANOSECONDS);
if (duration > 100_000_000) { // >100ms
logger.warn("Filter {} took {}ms for URI {}", getClass().getSimpleName(), duration/1_000_000.0, request.getRequestURI());
}
}
}
}5. Configuration Best Practices
Use @ConditionalOnProperty to enable/disable filters via configuration.
Separate production and development filter beans with @Profile annotations.
Prefer constants for order values (e.g., FilterConstants.CHARACTER_ENCODING_FILTER_ORDER) to avoid magic numbers.
Common Issues & Troubleshooting
Filter Not Effective
Possible causes:
Filter bean not registered (missing @Component or FilterRegistrationBean).
Servlet component scanning not enabled for @WebFilter (add @ServletComponentScan to the application class).
Incorrect URL pattern or order.
Solution: Verify bean registration, URL patterns, and order using a debugger bean that prints all registered filters and their order.
Response Body Empty
If ContentCachingResponseWrapper is used, always call copyBodyToResponse() in a finally block; otherwise the client receives an empty payload.
Filter Order Conflicts with Spring Security
Custom filters that need authentication data must be placed after the Spring Security filter chain, e.g., using
http.addFilterAfter(customFilter, UsernamePasswordAuthenticationFilter.class)in the security configuration.
Conclusion
Spring Boot filters provide a powerful, extensible way to address cross‑cutting concerns. Understanding the built‑in filter chain, correctly implementing custom filters, managing ordering, and following performance best practices enable developers to build robust, observable, and secure web services.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
