Spring Boot中OncePerRequestFilter原理与Filter单次调用控制全解析

在Spring Boot/Web应用中,OncePerRequestFilter是解决「Filter被多次调用」问题的核心组件。要理解其原理,需先搞清楚Filter为何会被多次执行 ,再拆解OncePerRequestFilter的"防重复"设计,最后掌握控制Filter单次调用的通用方法。

一、先搞懂:普通Filter为什么会被多次调用?

Filter是Servlet规范的核心组件,其调用次数并非由Spring决定,而是由Servlet容器(Tomcat/Jetty)的请求分发机制 决定。普通Filter(直接实现javax.servlet.Filter)被多次调用的核心场景有3类:

1. 核心触发场景(Servlet规范定义)

场景 触发原因 示例
请求转发(forward) request.getRequestDispatcher().forward(req, resp) 会重新执行Filter链 控制器转发到JSP页面、内部接口转发
请求包含(include) request.getRequestDispatcher().include(req, resp) 会嵌套执行Filter链 页面包含公共组件(如header.jsp)
异步请求(async) 异步处理时(如request.startAsync()),异步分发阶段会再次触发Filter链 Spring MVC的异步控制器(Callable
错误页面(error) 异常触发错误页面(如web.xml配置<error-page>),会重新执行Filter链 404/500错误跳转自定义页面

2. 实战示例:普通Filter被多次调用

java 复制代码
// 普通Filter:实现javax.servlet.Filter,无防重复逻辑
@Component
public class NormalFilter implements Filter {
    private static final Logger log = LoggerFactory.getLogger(NormalFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        log.info("NormalFilter执行,请求路径:{},分发类型:{}", 
                 req.getRequestURI(), 
                 req.getDispatcherType()); // 打印分发类型
        chain.doFilter(request, response);
    }
}

// 控制器:触发forward转发
@Controller
public class TestController {
    @GetMapping("/test")
    public String test() {
        // 转发到/inner,会重新触发Filter链
        return "forward:/inner"; 
    }

    @GetMapping("/inner")
    @ResponseBody
    public String inner() {
        return "inner";
    }
}

执行结果

bash 复制代码
NormalFilter执行,请求路径:/test,分发类型:REQUEST
NormalFilter执行,请求路径:/inner,分发类型:FORWARD

可见,一次HTTP请求触发了Filter两次执行(REQUEST+FORWARD),这是普通Filter的"天然问题"。

二、OncePerRequestFilter的核心原理:保证单次执行

OncePerRequestFilter是Spring提供的Filter抽象类,核心目标是让Filter在「一次完整的HTTP请求周期」内仅执行一次,无论经历多少次forward/include/异步分发。

1. 核心设计思路

通过「请求属性标记」+「分发类型过滤」双重机制,确保Filter逻辑仅执行一次:

  1. 标记机制 :执行Filter前,检查请求(HttpServletRequest)中是否存在"已执行"的标记属性;若存在则跳过,不存在则执行并设置标记;
  2. 分发类型控制:可配置仅在指定分发类型(如REQUEST)下执行,忽略FORWARD/INCLUDE/ERROR等场景。

2. 源码级拆解(Spring 6.x/Spring Boot 3.x适配版)

OncePerRequestFilter的核心逻辑在doFilter方法中,简化后关键代码如下:

java 复制代码
public abstract class OncePerRequestFilter implements Filter {
    // 核心doFilter方法:控制单次执行
    @Override
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 校验是否为HttpServletRequest(非HTTP请求直接放行)
        if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
            filterChain.doFilter(request, response);
            return;
        }

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 2. 生成唯一标记名:避免不同Filter的标记冲突
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        // 3. 检查是否已执行过:存在标记则跳过
        if (request.getAttribute(alreadyFilteredAttributeName) != null) {
            filterChain.doFilter(request, response);
            return;
        }

        // 4. 检查是否需要跳过当前分发类型(如FORWARD/ERROR)
        if (shouldNotFilter(httpRequest)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 5. 设置"已执行"标记:防止重复执行
        request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
        try {
            // 6. 执行子类重写的核心过滤逻辑(仅执行一次)
            doFilterInternal(httpRequest, httpResponse, filterChain);
        } finally {
            // 7. 移除标记(异步场景下可能需要保留,Spring做了特殊处理)
            request.removeAttribute(alreadyFilteredAttributeName);
        }
    }

    // 子类必须实现的核心方法:真正的过滤逻辑
    protected abstract void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException;

    // 生成唯一标记名:默认是Filter类名 + ".FILTERED"
    protected String getAlreadyFilteredAttributeName() {
        return getClass().getName() + ".FILTERED";
    }

    // 是否跳过过滤:默认返回false,子类可重写
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return false;
    }

    // 异步分发时是否跳过(默认true:异步分发不执行)
    protected boolean shouldNotFilterAsyncDispatch() {
        return true;
    }

    // 错误分发时是否跳过(默认true:错误页面跳转不执行)
    protected boolean shouldNotFilterErrorDispatch() {
        return true;
    }
}

3. 关键机制解析

核心方法/属性 作用
getAlreadyFilteredAttributeName 生成唯一标记名(如com.xxx.MyFilter.FILTERED),避免多Filter标记冲突
shouldNotFilter 全局控制是否跳过过滤(子类可重写,如根据URL跳过)
shouldNotFilterAsyncDispatch 异步分发时是否跳过(默认true,避免异步场景重复执行)
shouldNotFilterErrorDispatch 错误页面分发时是否跳过(默认true,避免错误跳转重复执行)
doFilterInternal 子类重写的核心逻辑,仅在首次执行时调用

4. 实战验证:OncePerRequestFilter仅执行一次

java 复制代码
// 继承OncePerRequestFilter,保证单次执行
@Component
public class OncePerRequestDemoFilter extends OncePerRequestFilter {
    private static final Logger log = LoggerFactory.getLogger(OncePerRequestDemoFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("OncePerRequestFilter执行,请求路径:{},分发类型:{}", 
                 request.getRequestURI(), 
                 request.getDispatcherType());
        filterChain.doFilter(request, response);
    }
}

执行结果

bash 复制代码
OncePerRequestFilter执行,请求路径:/test,分发类型:REQUEST

即使触发forward转发,Filter仅在REQUEST分发类型下执行一次,完美解决重复调用问题。

三、如何控制Filter只被调用一次?(两种核心方案)

方案1:推荐------继承OncePerRequestFilter(Spring官方方案)

这是最简单、最稳定的方式,Spring已封装所有细节,只需重写doFilterInternal

java 复制代码
// 步骤1:继承OncePerRequestFilter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 控制Filter执行顺序
public class MySingleFilter extends OncePerRequestFilter {

    // 步骤2:重写doFilterInternal,实现核心逻辑
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 你的过滤逻辑:如token校验、日志记录、参数解析等
        String token = request.getHeader("token");
        if (token == null) {
            response.setStatus(401);
            response.getWriter().write("token为空");
            return;
        }

        // 放行请求
        filterChain.doFilter(request, response);
    }

    // 可选:重写shouldNotFilter,指定跳过的URL
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String path = request.getRequestURI();
        // 跳过静态资源和登录接口
        return path.startsWith("/static/") || path.equals("/login");
    }
}

方案2:手动实现------普通Filter+请求标记(无Spring依赖)

若不想依赖Spring(如纯Servlet项目),可手动实现"标记机制",核心逻辑与OncePerRequestFilter一致:

java 复制代码
@Component
public class ManualSingleFilter implements Filter {
    // 步骤1:定义唯一标记名
    private static final String FILTERED_MARK = "ManualSingleFilter.FILTERED";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        // 步骤2:检查标记,存在则跳过
        if (req.getAttribute(FILTERED_MARK) != null) {
            chain.doFilter(request, response);
            return;
        }

        // 步骤3:过滤分发类型(仅处理REQUEST)
        if (!req.getDispatcherType().equals(DispatcherType.REQUEST)) {
            chain.doFilter(request, response);
            return;
        }

        try {
            // 步骤4:设置标记
            req.setAttribute(FILTERED_MARK, Boolean.TRUE);
            // 步骤5:核心过滤逻辑
            String token = req.getHeader("token");
            if (token == null) {
                resp.setStatus(401);
                resp.getWriter().write("token为空");
                return;
            }
            // 放行
            chain.doFilter(request, response);
        } finally {
            // 步骤6:移除标记(可选,请求结束后会自动销毁)
            req.removeAttribute(FILTERED_MARK);
        }
    }
}

方案3:进阶------配置Filter的DispatcherType(Servlet 3.0+)

通过@WebFilter或配置类指定Filter仅在REQUEST分发类型下执行,忽略FORWARD/INCLUDE/ERROR/ASYNC:

java 复制代码
// 方式1:注解配置
@WebFilter(urlPatterns = "/*", dispatcherTypes = DispatcherType.REQUEST)
public class DispatcherFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 仅在REQUEST分发类型下执行,避免forward/include重复调用
        chain.doFilter(request, response);
    }
}

// 方式2:Spring Boot配置类(推荐,更灵活)
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<DispatcherFilter> dispatcherFilter() {
        FilterRegistrationBean<DispatcherFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new DispatcherFilter());
        registration.addUrlPatterns("/*");
        // 指定仅处理REQUEST类型
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return registration;
    }
}

四、关键注意事项(避坑指南)

1. 异步请求的特殊处理

若Filter需要处理异步请求(如Spring MVC的@Async控制器),需重写shouldNotFilterAsyncDispatch返回false,并确保标记在异步上下文保留:

java 复制代码
@Override
protected boolean shouldNotFilterAsyncDispatch() {
    // 异步分发时也执行(默认true是跳过)
    return false;
}

2. Filter执行顺序控制

多个Filter时,通过@OrderFilterRegistrationBean.setOrder()指定顺序,OncePerRequestFilter不影响执行顺序,仅控制单次执行。

3. 避免标记名冲突

手动实现标记时,标记名必须唯一(建议用"类名+FILTERED"),否则多个Filter会互相干扰。

4. 跨域Filter的特殊处理

跨域(CORS)Filter必须是第一个执行的Filter,且需继承OncePerRequestFilter,否则OPTIONS预检请求可能被重复处理:

java 复制代码
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 其他跨域头配置...
        filterChain.doFilter(request, response);
    }
}

五、总结

1. 核心原理

  • 普通Filter多次调用的根源:Servlet容器的forward/include/异步/错误分发会重新触发Filter链;
  • OncePerRequestFilter的核心:通过「请求属性标记」+「分发类型过滤」,保证一次HTTP请求周期内仅执行一次;
  • 关键方法:doFilterInternal(核心逻辑)、shouldNotFilter(跳过规则)、getAlreadyFilteredAttributeName(唯一标记)。

2. 最佳实践

场景 推荐方案
Spring Boot项目 继承OncePerRequestFilter(最简单)
纯Servlet项目 手动实现"标记机制"+ 分发类型过滤
仅需处理直接请求 配置DispatcherType.REQUEST(最轻量化)

3. 避坑核心

  • 异步请求需重写shouldNotFilterAsyncDispatch
  • 标记名必须唯一;
  • 跨域Filter需优先执行且继承OncePerRequestFilter

通过以上方式,可彻底解决Filter重复调用的问题,保证过滤逻辑的准确性和性能。

相关推荐
小璐猪头11 分钟前
专为 Spring Boot 设计的 Elasticsearch 日志收集 Starter
java
韩师傅13 分钟前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
ps酷教程31 分钟前
HttpPostRequestDecoder源码浅析
java·http·netty
闲人编程31 分钟前
消息通知系统实现:构建高可用、可扩展的企业级通知服务
java·服务器·网络·python·消息队列·异步处理·分发器
栈与堆1 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
superman超哥1 小时前
双端迭代器(DoubleEndedIterator):Rust双向遍历的优雅实现
开发语言·后端·rust·双端迭代器·rust双向遍历
1二山似1 小时前
crmeb多商户启动swoole时报‘加密文件丢失’
后端·swoole
马卡巴卡1 小时前
Java CompletableFuture 接口与原理详解
后端
OC溥哥9991 小时前
Paper MinecraftV3.0重大更新(下界更新)我的世界C++2D版本隆重推出,拷贝即玩!
java·c++·算法
星火开发设计1 小时前
C++ map 全面解析与实战指南
java·数据结构·c++·学习·算法·map·知识