SpringBoot - 深入解析OncePerRequestFilter的底层机制与最佳实践

张开发
2026/4/4 12:17:33 15 分钟阅读
SpringBoot - 深入解析OncePerRequestFilter的底层机制与最佳实践
1. 为什么需要OncePerRequestFilter在Web开发中过滤器(Filter)是最常用的组件之一。想象一下你家的净水器它会对所有进入家庭的自来水进行过滤确保水质安全。Web过滤器的工作原理也类似它会对所有进入应用的请求进行预处理。但这里有个问题有些净水器会在水循环过程中反复过滤同一批水这不仅浪费资源还可能改变水的性质。同样地在Web应用中某些情况下过滤器会被重复执行。我曾经在一个电商项目中遇到过这样的问题用户登录校验过滤器在处理内部转发(forward)请求时被重复调用导致用户会话被意外重置。这就是典型的过滤器重复执行问题。传统过滤器(Filter接口实现类)在某些Web容器中对于服务器内部的forward/include请求会再次触发过滤链而OncePerRequestFilter就是为了解决这个问题而生的。2. OncePerRequestFilter的核心机制2.1 单次过滤的保证原理OncePerRequestFilter的核心思想很简单但非常有效给每个请求打标记。就像超市的收银员会给已结账的商品贴上已付款标签一样OncePerRequestFilter会给处理过的请求设置一个属性标记。让我们看看它的核心代码实现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); } } }这个设计有几点值得注意使用请求属性(attribute)而非类变量存储状态保证线程安全采用try-finally确保标记一定会被清除标记名通过getAlreadyFilteredAttributeName()方法获取子类可以重写2.2 与普通过滤器的性能对比在实际压力测试中我发现使用OncePerRequestFilter相比普通过滤器有约5-10%的性能提升。这是因为避免了重复执行过滤逻辑减少了不必要的资源消耗如数据库连接、加密运算等降低了因重复过滤导致的业务异常概率3. 最佳实践与常见陷阱3.1 认证授权的黄金搭档OncePerRequestFilter最常见的应用场景就是认证授权。下面是一个JWT认证过滤器的典型实现Component Order(1) public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtTokenProvider tokenProvider; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null tokenProvider.validateToken(token)) { Authentication auth tokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest req) { String bearerToken req.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }这里有几个关键点使用Component让Spring管理过滤器实例Order控制过滤器执行顺序将认证信息存入SecurityContextHolder3.2 必须避免的五个坑在我多年的实践中总结了使用OncePerRequestFilter时容易踩的坑顺序问题过滤器链的执行顺序很重要比如应该先执行跨域过滤器再执行认证过滤器。可以通过Order注解或FilterRegistrationBean来调整顺序。异常处理在doFilterInternal中抛出的异常需要妥善处理否则可能导致标记未被清除。建议使用try-catch包裹业务逻辑。路径排除不是所有请求都需要过滤可以通过重写shouldNotFilter方法来排除特定路径Override protected boolean shouldNotFilter(HttpServletRequest request) { return new AntPathMatcher().match(/public/**, request.getServletPath()); }异步请求对于异步请求OncePerRequestFilter的行为可能不符合预期需要特别处理。资源释放在finally块中确保释放所有资源如数据库连接、文件句柄等。4. 高级应用场景4.1 请求日志记录OncePerRequestFilter非常适合用于请求日志记录因为它能确保每个请求只记录一次。下面是一个记录请求耗时和基本信息的实现Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { long startTime System.currentTimeMillis(); try { chain.doFilter(request, response); } finally { long duration System.currentTimeMillis() - startTime; log.info({} {} - {}ms (status: {}), request.getMethod(), request.getRequestURI(), duration, response.getStatus()); } }4.2 响应内容修改配合HttpServletResponseWrapper可以实现响应内容的修改。比如统一给所有JSON响应添加版本号Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { ContentCachingResponseWrapper wrappedResponse new ContentCachingResponseWrapper(response); try { chain.doFilter(request, wrappedResponse); byte[] content wrappedResponse.getContentAsByteArray(); if (content.length 0 isJsonResponse(response)) { String json new String(content, StandardCharsets.UTF_8); json addApiVersion(json); // 添加版本号 wrappedResponse.resetBuffer(); wrappedResponse.getOutputStream().write(json.getBytes()); } } finally { wrappedResponse.copyBodyToResponse(); } }5. 性能调优技巧5.1 跳过不需要的请求通过重写shouldNotFilter和skipDispatch方法可以显著提升性能。比如跳过静态资源请求Override protected boolean shouldNotFilter(HttpServletRequest request) { String path request.getRequestURI(); return path.startsWith(/static/) || path.endsWith(.js) || path.endsWith(.css); }5.2 使用缓存提升效率对于频繁访问但数据变化不大的场景可以引入缓存。比如权限信息缓存Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader request.getHeader(Authorization); String cacheKey auth_ authHeader; Authentication auth cache.get(cacheKey, Authentication.class); if (auth null) { auth authenticate(request); cache.put(cacheKey, auth); } SecurityContextHolder.getContext().setAuthentication(auth); chain.doFilter(request, response); }5.3 合理配置FilterRegistrationBean在Spring Boot中通过FilterRegistrationBean可以更灵活地配置过滤器Bean public FilterRegistrationBeanMyFilter myFilterRegistration() { FilterRegistrationBeanMyFilter registration new FilterRegistrationBean(); registration.setFilter(new MyFilter()); registration.addUrlPatterns(/api/*); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); registration.setName(myFilter); return registration; }这种配置方式比Component注解更灵活可以精确控制过滤器的URL匹配模式和顺序。

更多文章