1. 为什么需要 OncePerRequestFilter?
在Web开发中,过滤器(Filter)是处理HTTP请求和响应的常用组件。但标准Servlet过滤器存在一个潜在问题:当请求在服务器内部发生转发(forward)或包含(include)时,同一个过滤器可能会被多次执行。
想象这样一个场景:你的应用需要对每个请求进行耗时统计。如果使用普通Filter,当请求从/a转发到/b时,过滤器会被执行两次——这显然会导致统计结果失真。更糟的是,如果过滤器涉及资源初始化或权限检查等操作,重复执行可能引发各种问题。
Spring的OncePerRequestFilter正是为解决这个问题而生。它通过请求属性标记机制,确保无论请求在服务器内部如何流转,过滤器的核心逻辑只会执行一次。这种特性在以下场景特别有价值:
- 请求耗时统计
- 跨域处理(CORS)
- 认证/授权检查
- 请求/响应日志记录
- 线程本地(ThreadLocal)资源管理
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OncePerRequestFilter 实现原理深度解析
2.1 核心工作机制
OncePerRequestFilter的实现相当精妙。其核心逻辑在doFilter方法中:
java复制public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
throw new ServletException("OncePerRequestFilter just supports HTTP requests");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if (request.getAttribute(alreadyFilteredAttributeName) != null) {
// 已处理过则跳过
chain.doFilter(request, response);
}
else {
// 标记为已处理
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 执行实际过滤逻辑
doFilterInternal(httpRequest, httpResponse, chain);
}
finally {
// 清除标记
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
关键点在于:
- 每个过滤器实例有唯一的
alreadyFilteredAttributeName(默认是类名+".FILTERED") - 通过
request.getAttribute()检查是否已处理 - 使用
request.setAttribute()设置处理标记 - 在
finally块中清除标记
2.2 与普通Filter的对比
| 特性 | 普通Filter | OncePerRequestFilter |
|---|---|---|
| 执行次数 | 可能多次 | 确保一次 |
| 转发(forward)处理 | 会重复执行 | 跳过已处理请求 |
| 包含(include)处理 | 会重复执行 | 跳过已处理请求 |
| 异步请求支持 | 需要特殊处理 | 内置支持 |
| 线程安全 | 需自行保证 | 基于请求属性,天然线程安全 |
注意:虽然OncePerRequestFilter能防止同一过滤器重复执行,但不同过滤器实例之间不会互相影响。如果有多个过滤器都需要"once per request"特性,需要各自继承OncePerRequestFilter。
3. 实战:自定义OncePerRequestFilter
3.1 基础实现模板
下面是一个完整的自定义过滤器实现示例,包含最佳实践:
java复制import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomTrackingFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomTrackingFilter.class);
// 可覆盖此方法改变默认的"已过滤"属性名
@Override
protected String getAlreadyFilteredAttributeName() {
return getClass().getName() + ".FILTERED";
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 前置处理逻辑
long startTime = System.currentTimeMillis();
String requestId = generateRequestId(request);
try {
// 必须调用doFilter继续链式调用
filterChain.doFilter(request, response);
}
finally {
// 后置处理逻辑
long duration = System.currentTimeMillis() - startTime;
logger.info("Request {} completed in {} ms", requestId, duration);
}
}
private String generateRequestId(HttpServletRequest request) {
return String.format("%s@%d",
request.getRequestURI(),
System.currentTimeMillis());
}
// 可选:控制哪些请求需要过滤
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getRequestURI().startsWith("/static/");
}
}
关键实践要点:
- 总是使用
try-finally确保后置逻辑执行 - 在
finally中执行清理/统计等操作 - 重写
shouldNotFilter可以排除特定请求 - 使用SLF4J等日志框架而非System.out
3.2 高级配置选项
在Spring Boot中注册过滤器时,可以配置更多选项:
java复制@Configuration
public class FilterConfiguration {
@Bean
public FilterRegistrationBean<CustomTrackingFilter> trackingFilter() {
FilterRegistrationBean<CustomTrackingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CustomTrackingFilter());
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 100); // 控制执行顺序
registration.addUrlPatterns("/*");
registration.setName("customTrackingFilter");
registration.setAsyncSupported(true); // 支持异步请求
return registration;
}
}
过滤器顺序(order)非常重要:
- 安全相关过滤器通常设置最高优先级(最低order值)
- 日志/统计类过滤器可以设置较低优先级
- 使用
Ordered.HIGHEST_PRECEDENCE和Ordered.LOWEST_PRECEDENCE作为基准
4. 生产环境中的常见问题与解决方案
4.1 异步请求处理
当处理异步请求(@Async或DeferredResult)时,需要注意:
java复制@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
// 必须确保在异步上下文中也能执行后置逻辑
AsyncContext asyncContext = request.startAsync();
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) {
// 异步完成时的处理
}
// 其他事件方法...
});
chain.doFilter(request, response);
}
最佳实践:
- 注册过滤器时调用
setAsyncSupported(true) - 为AsyncContext添加监听器处理异步完成事件
- 避免在异步环境中使用ThreadLocal
4.2 性能监控过滤器实现
下面是一个更完善的性能监控过滤器示例:
java复制public class PerformanceMonitoringFilter extends OncePerRequestFilter {
private static final String ATTRIBUTE_START_TIME = "startTime";
private final MetricsCollector metricsCollector;
public PerformanceMonitoringFilter(MetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
long startTime = System.nanoTime();
request.setAttribute(ATTRIBUTE_START_TIME, startTime);
try {
chain.doFilter(request, response);
} finally {
Long start = (Long) request.getAttribute(ATTRIBUTE_START_TIME);
if (start != null) {
long duration = System.nanoTime() - start;
metricsCollector.record(
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration
);
}
}
}
}
这个实现:
- 使用纳秒级精度计时
- 将开始时间存储在请求属性中
- 通过MetricsCollector接口上报数据(可对接Prometheus等监控系统)
- 记录HTTP方法、路径和状态码
4.3 常见陷阱与解决方案
问题1:过滤器被跳过
- 可能原因:
shouldNotFilter返回了true,或请求已被标记为已过滤 - 解决方案:检查URL模式匹配和
shouldNotFilter逻辑
问题2:后置逻辑未执行
- 可能原因:未使用try-finally,或filterChain.doFilter抛出异常
- 解决方案:确保后置逻辑在finally块中
问题3:性能瓶颈
- 可能原因:过滤器逻辑过于复杂
- 解决方案:将耗时操作异步化,或使用缓存
问题4:内存泄漏
- 可能原因:在请求属性中存储了大对象
- 解决方案:及时清理属性,或使用弱引用
5. 进阶应用场景
5.1 分布式追踪集成
在现代微服务架构中,可以将OncePerRequestFilter与分布式追踪系统(如Zipkin、SkyWalking)集成:
java复制public class TracingFilter extends OncePerRequestFilter {
private final Tracer tracer;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
Span span = tracer.nextSpan()
.name(request.getRequestURI())
.kind(Span.Kind.SERVER)
.start();
try (Scope scope = tracer.withSpan(span)) {
chain.doFilter(request, response);
} catch (Exception ex) {
span.error(ex);
throw ex;
} finally {
span.finish();
}
}
}
5.2 请求/响应日志记录
一个安全的请求日志记录过滤器实现:
java复制public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
private final int maxPayloadLength = 1000;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
try {
chain.doFilter(wrappedRequest, wrappedResponse);
} finally {
logRequest(request, wrappedRequest.getContentAsByteArray());
logResponse(response, wrappedResponse.getContentAsByteArray());
wrappedResponse.copyBodyToResponse(); // 必须调用以返回响应体
}
}
private void logRequest(HttpServletRequest request, byte[] body) {
String payload = body.length > 0 ?
new String(body, StandardCharsets.UTF_8).substring(0, Math.min(body.length, maxPayloadLength)) : "";
logger.info("Request: {} {} - Headers: {} - Body: {}",
request.getMethod(),
request.getRequestURI(),
getHeaders(request),
payload);
}
// 类似的response日志方法...
}
关键点:
- 使用
ContentCachingRequestWrapper和ContentCachingResponseWrapper缓存请求/响应体 - 限制日志输出的数据量防止日志爆炸
- 必须调用
copyBodyToResponse()将缓存的响应写回客户端
5.3 安全头部过滤器
增强应用安全性的HTTP头部过滤器:
java复制public class SecurityHeadersFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("Strict-Transport-Security", "max-age=31536000 ; includeSubDomains");
response.setHeader("Content-Security-Policy", "default-src 'self'");
chain.doFilter(request, response);
}
}
这个过滤器添加了:
- 防MIME类型嗅探
- 防点击劫持
- XSS保护
- HTTPS强制
- 内容安全策略
6. 性能优化与最佳实践
6.1 过滤器执行顺序优化
合理的过滤器顺序可以显著提升性能:
- 安全相关过滤器(如CORS、CSRF)最先执行
- 然后是认证/授权过滤器
- 接着是上下文设置过滤器(如Locale、Theme)
- 最后是日志/监控类过滤器
示例配置:
java复制@Configuration
public class FilterOrderConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public FilterRegistrationBean<CorsFilter> corsFilter() {
// CORS配置...
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public FilterRegistrationBean<AuthenticationFilter> authFilter() {
// 认证配置...
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE - 100)
public FilterRegistrationBean<LoggingFilter> loggingFilter() {
// 日志配置...
}
}
6.2 轻量级实现技巧
对于高性能场景,可以优化过滤器实现:
- 避免在过滤器中做复杂计算
- 使用快速失败(fail-fast)策略
- 缓存频繁访问的数据
- 使用高效的字符串操作
示例优化:
java复制public class OptimizedFilter extends OncePerRequestFilter {
private static final String[] EXCLUDE_PATHS = {"/health", "/metrics"};
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
for (String exclude : EXCLUDE_PATHS) {
if (path.startsWith(exclude)) {
return true;
}
}
return false;
}
// 简化的doFilterInternal实现...
}
6.3 测试策略
确保过滤器正确性的测试方法:
- 单元测试:直接测试
doFilterInternal方法 - 集成测试:使用MockMvc测试过滤器链
- 性能测试:验证过滤器对吞吐量的影响
示例测试:
java复制@SpringBootTest
@AutoConfigureMockMvc
class CustomFilterTest {
@Autowired
private MockMvc mockMvc;
@Test
void testFilterExecution() throws Exception {
mockMvc.perform(get("/api/test"))
.andExpect(status().isOk())
.andExpect(header().exists("X-Custom-Header"));
}
@Test
void testFilterSkip() throws Exception {
mockMvc.perform(get("/static/resource.css"))
.andExpect(header().doesNotExist("X-Custom-Header"));
}
}
7. 与其他Spring组件的协作
7.1 与Spring Security集成
当同时使用Spring Security时,过滤器执行顺序特别重要:
code复制SecurityFilterChain (Spring Security的过滤器链)
↓
自定义OncePerRequestFilter
↓
其他应用过滤器
配置示例:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)
// 其他安全配置...
}
}
7.2 与Spring MVC的交互
过滤器在Spring MVC处理之前执行,因此:
- 过滤器无法直接访问Controller的@ModelAttribute
- 但可以通过请求属性传递数据
- 拦截器(Interceptor)更适合处理与MVC紧密相关的逻辑
7.3 异常处理策略
过滤器中的异常处理最佳实践:
java复制public class ErrorHandlingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (BusinessException ex) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().write("{\"error\":\"" + ex.getMessage() + "\"}");
} catch (Exception ex) {
logger.error("Filter error", ex);
response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
}
关键点:
- 捕获特定业务异常并返回友好错误
- 记录未预期异常
- 设置适当的HTTP状态码
8. 实际案例:构建API网关过滤器
下面是一个模拟API网关功能的完整过滤器实现:
java复制public class ApiGatewayFilter extends OncePerRequestFilter {
private final RateLimiter rateLimiter;
private final ApiKeyValidator apiKeyValidator;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 1. 认证
String apiKey = request.getHeader("X-API-KEY");
if (!apiKeyValidator.validate(apiKey)) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid API key");
return;
}
// 2. 限流
if (!rateLimiter.tryAcquire(apiKey)) {
response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "Rate limit exceeded");
return;
}
// 3. 请求改写
String modifiedPath = modifyPath(request.getRequestURI());
RequestDispatcher dispatcher = request.getRequestDispatcher(modifiedPath);
// 4. 添加跟踪头
response.setHeader("X-Request-ID", UUID.randomUUID().toString());
// 继续处理
dispatcher.forward(request, response);
}
private String modifyPath(String originalPath) {
// 路径重写逻辑...
}
}
这个网关过滤器提供了:
- API密钥验证
- 速率限制
- 请求路由/改写
- 请求跟踪
9. 调试与问题诊断
9.1 日志配置建议
为过滤器配置详细日志:
java复制public class DebuggableFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(DebuggableFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Processing request: {} {}", request.getMethod(), request.getRequestURI());
Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()) {
String name = headers.nextElement();
logger.debug("Header {}: {}", name, request.getHeader(name));
}
}
chain.doFilter(request, response);
}
}
9.2 常见问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 过滤器未执行 | URL模式不匹配 | 检查addUrlPatterns配置 |
| 过滤器执行多次 | 未继承OncePerRequestFilter | 确保继承正确基类 |
| 后置逻辑未执行 | 未使用try-finally | 用try-finally包裹doFilter调用 |
| 异步请求处理不完整 | 未设置asyncSupported=true | 注册过滤器时启用异步支持 |
| 内存泄漏 | 在请求属性中存储大对象 | 及时清理属性或使用弱引用 |
9.3 性能分析技巧
使用Spring Boot Actuator监控过滤器性能:
- 添加依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 配置application.properties:
properties复制management.endpoints.web.exposure.include=metrics,health
management.metrics.web.server.request.metric-name=filter.timer
- 通过
/actuator/metrics/filter.timer端点获取性能数据
10. 未来演进与替代方案
10.1 Servlet 3.0+的异步支持
对于现代应用,可以考虑异步过滤器实现:
java复制public class AsyncFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(5000); // 5秒超时
CompletableFuture.runAsync(() -> {
try {
chain.doFilter(request, response);
} catch (Exception e) {
// 异常处理
} finally {
asyncContext.complete();
}
});
}
}
10.2 响应式编程中的替代方案
在Spring WebFlux中,可以使用WebFilter:
java复制@Component
public class ReactiveWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
long startTime = System.currentTimeMillis();
return chain.filter(exchange)
.doFinally(signal -> {
long duration = System.currentTimeMillis() - startTime;
log.info("Request processed in {} ms", duration);
});
}
}
10.3 云原生时代的演进
在服务网格(Service Mesh)架构中,部分过滤器功能可以下沉到Sidecar代理(如Envoy):
- 认证/授权:通过Istio RBAC实现
- 限流:通过Envoy速率限制服务
- 监控:通过服务网格的可观测性功能
但在应用层,OncePerRequestFilter仍然适用于:
- 业务特定的请求处理
- 应用级的安全检查
- 与业务紧密耦合的监控逻辑
