1. Spring过滤器基础与OncePerRequestFilter定位
在Web应用开发中,过滤器(Filter)是Java Servlet规范提供的核心组件之一,它能够在请求到达Servlet之前和响应返回客户端之前对数据进行预处理和后处理。Spring框架在此基础上进行了深度封装和功能增强,其中OncePerRequestFilter作为过滤器体系中的特殊实现,解决了传统过滤器在特定场景下的重复执行问题。
传统Servlet过滤器在请求转发(forward)或包含(include)时会被重复调用,这可能导致性能损耗和逻辑错误。比如当使用Spring的视图解析器进行JSP渲染时,如果过滤器中有耗时的安全检查或日志记录代码,同一请求的多次过滤会显著影响系统性能。OncePerRequestFilter通过内部标记机制确保每个HTTP请求只被过滤一次,无论请求经过多少次转发。
关键区别:普通Filter的doFilter()可能在一次请求中被多次调用,而OncePerRequestFilter的doFilterInternal()保证单次请求只执行一次
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OncePerRequestFilter核心机制解析
2.1 执行流程控制原理
OncePerRequestFilter的核心逻辑体现在其doFilter方法中。通过查看源码可以发现,它使用请求属性(attribute)作为标记位来实现单次执行控制:
java复制public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if (request.getAttribute(alreadyFilteredAttributeName) != null) {
chain.doFilter(request, response); // 已过滤则直接放行
}
else {
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, chain); // 实际过滤逻辑
}
finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
这种实现方式具有以下技术特点:
- 线程安全:基于请求级别的属性存储,不同请求互不影响
- 轻量级:属性操作开销极小,几乎不影响性能
- 可扩展:子类可通过覆盖getAlreadyFilteredAttributeName()自定义属性名
2.2 与普通Filter的对比选择
| 特性 | 普通Filter | OncePerRequestFilter |
|---|---|---|
| 执行次数 | 可能多次 | 保证一次 |
| 适用场景 | 简单处理 | 需幂等操作 |
| 性能影响 | 转发时可能有重复计算 | 无重复计算 |
| 实现复杂度 | 简单 | 需考虑单次执行逻辑 |
| Spring集成度 | 需手动配置 | 无缝集成Spring安全体系 |
3. 典型应用场景与实现
3.1 认证与授权控制
在Spring Security体系中,各种安全过滤器大多继承自OncePerRequestFilter。例如实现一个简单的JWT校验过滤器:
java复制public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = resolveToken(request);
if (StringUtils.hasText(token) && validateToken(token)) {
Authentication auth = createAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
注意事项:认证过滤器必须继承OncePerRequestFilter,避免在请求转发时重复校验Token导致性能问题或逻辑错误
3.2 请求日志记录
实现一个记录请求耗时和基本信息的过滤器:
java复制public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
long startTime = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
logger.info("{} {} - {}ms (UA: {})",
request.getMethod(),
request.getRequestURI(),
duration,
request.getHeader("User-Agent"));
}
}
}
日志过滤器需要特别注意:
- 耗时计算要放在finally块中确保始终执行
- 不要记录敏感信息如密码、token等
- 生产环境应考虑异步记录避免阻塞请求线程
4. 高级配置与性能优化
4.1 过滤器顺序控制
在Spring Boot中配置过滤器顺序的两种方式:
- 通过@Order注解(仅对FilterRegistrationBean有效)
java复制@Bean
public FilterRegistrationBean<MyFilter> myFilter() {
FilterRegistrationBean<MyFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new MyFilter());
bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return bean;
}
- 实现Ordered接口
java复制public class OrderedFilter extends OncePerRequestFilter implements Ordered {
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1;
}
}
经验法则:安全相关过滤器应设置较高优先级,日志记录类可设置较低优先级
4.2 异步请求处理
对于异步请求(如DeferredResult或Callable),需要特殊处理:
java复制@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false; // 默认true表示不处理ASYNC分发
}
@Override
protected boolean shouldNotFilterErrorDispatch() {
return false; // 默认true表示不处理ERROR分发
}
异步场景下的最佳实践:
- 明确区分初始请求和异步分发请求
- 避免在异步阶段修改响应头(可能已提交)
- 考虑使用RequestContextHolder保存请求上下文
5. 常见问题排查与调试
5.1 过滤器不生效排查步骤
- 检查过滤器是否被Spring管理(有@Component或@Bean声明)
- 确认没有错误的@Conditional条件限制
- 检查过滤器路径配置(urlPatterns或shouldFilter实现)
- 查看Spring Boot启动日志中的Registered filters列表
5.2 性能问题诊断
当发现请求处理变慢时,可通过以下方式检查过滤器性能:
- 添加Trace级别日志记录进入和退出时间
- 使用Spring Actuator的metrics端点查看过滤器耗时
properties复制management.endpoints.web.exposure.include=metrics
management.metrics.web.server.request.metric-name=filter.time
- 考虑使用@Profile限制过滤器仅在特定环境生效
5.3 与Spring Security的协作问题
当自定义过滤器和Spring Security共存时,典型问题包括:
- 安全上下文丢失:确保安全过滤器先执行
java复制http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
- CSRF保护冲突:对于API请求可能需要禁用CSRF
java复制http.csrf().disable();
- 静态资源被拦截:配置忽略路径
java复制web.ignoring().antMatchers("/static/**");
6. 测试策略与最佳实践
6.1 单元测试方案
使用MockMvc测试过滤器的基本模式:
java复制@SpringBootTest
@AutoConfigureMockMvc
class MyFilterTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldAddCustomHeader() throws Exception {
mockMvc.perform(get("/test"))
.andExpect(header().exists("X-Custom-Header"));
}
}
测试要点:
- 验证过滤器的正向逻辑
- 测试异常路径(如无效输入)
- 模拟并发请求验证线程安全性
6.2 生产环境部署建议
- 为关键过滤器添加Circuit Breaker模式
java复制@Override
protected void doFilterInternal(...) {
try (CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("filterName")) {
cb.run(() -> {
// 实际过滤逻辑
chain.doFilter(request, response);
});
}
}
- 配置合理的超时机制
- 重要过滤器添加健康检查指标
- 考虑过滤器链路追踪(结合Sleuth/Zipkin)
7. 扩展与定制开发
7.1 自定义过滤条件
覆盖shouldNotFilter方法实现条件过滤:
java复制@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/v2/");
}
7.2 响应包装处理
实现响应内容修改的示例:
java复制public class ResponseWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
ContentCachingResponseWrapper wrappedResponse =
new ContentCachingResponseWrapper(response);
try {
chain.doFilter(request, wrappedResponse);
byte[] content = wrappedResponse.getContentAsByteArray();
String modifiedContent = modifyContent(new String(content));
wrappedResponse.resetBuffer();
wrappedResponse.getWriter().write(modifiedContent);
} finally {
wrappedResponse.copyBodyToResponse();
}
}
}
关键点:使用ContentCachingResponseWrapper避免直接操作输出流导致的异常
在实际项目中,OncePerRequestFilter的正确使用可以避免许多微妙的bug。我曾在一个电商项目中遇到过滤器重复执行导致用户积分被错误累加的问题,改用OncePerRequestFilter后不仅解决了问题,还减少了约15%的请求处理时间。对于需要处理敏感操作(如支付、库存扣减)的过滤器,必须确保其继承自OncePerRequestFilter以保证操作的幂等性。
