Filter与Interceptor到底怎么选?Java Web请求链路一次讲透

📅 发布时间:2026/10/18 7:04:18
Filter与Interceptor到底怎么选?Java Web请求链路一次讲透
前几天一个同事跑来问我说用户登录状态明明传过来了Controller 却一直进不去权限拦截根本没有触发。他查了半天最后发现是项目入口一个过期的 Filter 在 chain.doFilter 之前直接 return 了。这个场景特别典型——很多人分得清“拦截器叫 Interceptor、过滤器叫 Filter”可真到出问题的时候还是会在这两者之间反复纠结。拦截器和过滤器是 Java Web 后端里出镜率最高的两个词。但绝大多数讨论都停在表面Filter 是 Servlet 规范的Interceptor 是 Spring MVC 的Filter 先执行Interceptor 后执行。这话没毛病但也确实没什么用。真正值得搞清楚的是它们各自活在哪个世界由谁创建、由谁调用、能拿到什么信息、遇到异常时表现有什么区别。这篇文章用自己的排查经验把这两者从头到尾拆一遍不堆概念重点放在“为什么会有这些差异”以及“生产环境选型时怎么用”。适合已经写过几个接口、想在边界问题上不再含糊的同学。1. 先分清两者各在哪一层工作1.1 Filter 是 Servlet 容器给的“大门保安”Filter 定义在javax.servlet包里是 Servlet 规范的一部分。只要你的项目跑在 Tomcat、Jetty 这类 Servlet 容器里Filter 就能用哪怕完全不用 Spring 框架它照常工作。一个 Filter 有三个生命周期方法init、doFilter、destroy。容器在启动时创建并初始化 Filter每次请求进来都会经过doFilter请求结束后由容器负责销毁。最核心的放行逻辑是这句public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // 请求进来时先做点事 chain.doFilter(request, response); // 响应返回时还能做点事 }不调用chain.doFilter请求就被卡在当前 Filter后面的 Filter、Servlet、Controller 全都接触不到。这是 Filter 和 Interceptor 最直观的区别之一——Filter 是“链式”传递每个节点必须主动把请求往后传。Filter 的作用范围是整个 Servlet 容器理论上所有能被容器处理的请求都会经过它包括静态资源、Servlet、JSP以及最终交给 DispatcherServlet 的请求。它看的是 Web 容器的“大门口”并不关心请求进来之后会被哪个 Spring Controller 处理。1.2 Interceptor 是 Spring MVC 的“楼层前台”Interceptor 完全活在 Spring 的世界里。它依赖 Spring MVC 的HandlerInterceptor接口由 DispatcherServlet 在分发请求时触发脱离了 Spring MVCInterceptor 就无从谈起。Spring MVC 处理一个请求时先通过 HandlerMapping 找到对应的 Controller 方法然后把HandlerExecutionChain返回给 DispatcherServlet。执行链里除了最终的 Handler还有一串 Interceptor。DispatcherServlet 在调用 Controller 前后会依次执行这些 Interceptor 的preHandle、postHandle和afterCompletion。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Controller 执行前 return true; } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // Controller 执行后视图渲染前 } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 整个请求完成后 } }preHandle返回false时请求被短路后续不会进入 Controller。这里要注意的是postHandle只有在 Controller 执行成功返回后才会触发而afterCompletion无论是否抛异常只要preHandle放行了就一定会执行——这也是做监控埋点时最容易踩的坑之一。1.3 一张图理解小区保安和楼层前台把一次请求比作去一栋办公楼办事。Filter 是小区大门口的门卫。所有车辆、所有访客不分去哪栋楼、哪层楼进小区前都要过他这一关。他管的是“整个园区的进出”。Interceptor 是楼里的前台。只有你的目标是这栋楼里的某个公司到了楼下才会遇到前台。前台能查你预约的是哪个部门、哪个人权限不足直接把你拦在外面。两者之间还有一段小区内部道路这段路相当于 DispatcherServlet 做的事情确定你要找哪家公司Router、怎么走HandlerMapping、需要哪些前置确认拦截器链。这个类比能解释一个高频问题为什么 Filter 里注入 Service 有时候是null而 Interceptor 里注入完全没有问题。因为 Filter 虽然是对象但默认由容器创建管理和 Spring 容器不是一家人Interceptor 则由 Spring 容器管理天然能使用 Spring 的所有依赖注入能力。2. 核心差异对照别再只背 401 和 4032.1 一张表看全八项差异对比项FilterInterceptor所属规范Servlet 规范Spring MVC 框架生命周期管理Servlet 容器Spring 容器作用范围所有进入容器的请求经过 DispatcherServlet 的请求触发时机Servlet 进入前后Controller 方法执行前后放行方式手动调用chain.doFilterpreHandle返回true路径匹配Servlet 的/*、/path/*规则Spring 的 Ant 规则支持/**、/*依赖注入默认拿不到 Spring Bean直接注入异常处理无 Controller 上下文可通过afterCompletion拿到异常这些差异是“为什么选 A 不选 B”的答案来源而不是面试背诵题。2.2 实操中容易踩坑的几个差异点路径匹配规则不一样。Filter 的 urlPatterns 写的是 Servlet 路径规则/*会匹配所有路径包括静态资源、错误页跳转等。Interceptor 用 Spring 的路径匹配/**匹配多层路径/*只匹配一层。曾经有人把拦截器路径配成/api/*结果/api/user/detail这种多级路径根本没进拦截器换成/**才正常。这个细节在排查“拦截器没生效”时优先级非常高。Filter 默认不属于 Spring 容器。如果只用WebFilter配置Filter 实例由容器创建你在 Filter 里Autowired一个 Service运行时会发现它是null。解决办法是用FilterRegistrationBean注册或者让 Filter 本身被 Spring 管理。这个坑属于“平时没事一上线就炸”的典型。静态资源的处理心态不一样。Spring Boot 项目里Filter 配了/*静态资源也会被过滤需要自己在代码里判断 URI 放行。Interceptor 虽然默认会拦截静态资源但由于静态资源通常交给ResourceHttpRequestHandler处理不走 Controller更常见的操作是在注册时用excludePathPatterns(/static/**)把静态资源放掉。3. 调用链路的真实顺序一次请求到底经过谁3.1 先在纸上画出完整链路很多人知道 Filter 先于 Interceptor但只有把这个顺序放到实际请求链路里看才能真正理解为什么会有先后的差异。一次典型的请求处理流程是这样的客户端请求 - Servlet 容器接收 - FilterChain 按顺序执行 Filter 前半段 - DispatcherServlet 接收请求 - HandlerMapping 找到 Controller 方法 - Interceptor.preHandle - Controller 方法执行 - Interceptor.postHandle - 视图解析 / 响应处理 - Interceptor.afterCompletion - DispatcherServlet 返回 - FilterChain 执行 Filter 后半段 - 容器写响应给客户端Filter 包在最外层它的一半逻辑在 DispatcherServlet 之前执行另一半在 DispatcherServlet 返回之后执行。Interceptor 则完全嵌在 DispatcherServlet 内部围绕 Controller 执行。3.2 用日志把这个链路打印出来纸上谈兵不如实际跑一次。建一个最简单的 Demo在 Filter 里打两行日志在 Interceptor 三个方法里各打一行日志然后请求一个接口。Filter 的日志Component Order(1) public class TraceFilter implements Filter { private static final Logger log LoggerFactory.getLogger(TraceFilter.class); Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { log.info([Filter] before chain); chain.doFilter(request, response); log.info([Filter] after chain); } }Interceptor 的日志public class TraceInterceptor implements HandlerInterceptor { private static final Logger log LoggerFactory.getLogger(TraceInterceptor.class); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { log.info([Interceptor] preHandle); return true; } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.info([Interceptor] postHandle); } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { log.info([Interceptor] afterCompletion); } }打印出来的结果顺序是[Filter] before chain [Interceptor] preHandle Controller 执行中 [Interceptor] postHandle [Interceptor] afterCompletion [Filter] after chain这串日志比任何概念解释都有用。它告诉我们拦截器三个方法并不是都在 Controller 前后postHandle在 Controller 返回后、视图渲染前执行afterCompletion是整个请求最后回调阶段但 Filter 的后半段还在它后面。3.3 异常一来顺序就变了如果 Controller 抛了异常链路就不再是完整的一条线。关键规则有两条postHandle不会执行。因为 Controller 抛异常后Spring MVC 会直接把异常交给异常处理机制不会走正常返回后的postHandle。afterCompletion仍然会执行。它专门用于请求结束后的清理和记录无论成功失败都会回调这是做监控埋点的重要位置。如果异常发生在preHandle之前或者preHandle直接返回false那么postHandle和afterCompletion都不执行——因为请求根本没进入 Controller。Filter 一旦抛异常整个请求直接断裂后面所有拦截器和 Controller 都不会执行。这也解释了一个常见现象在过滤器和拦截器里都加了日志Controller 抛错后只看到了preHandle看不到postHandle和afterCompletion。这时候不要怀疑拦截器配置错了先确认异常是不是发生在 Controller 层之前。4. 实操搞一个能跑通的 Filter 和 Interceptor 最小工程4.1 先写一个稳妥的 Filter很多老项目习惯用WebFilter加ServletComponentScan来注册 Filter这种方式能用但有两个问题一是 Filter 实例不在 Spring 容器里依赖注入容易失效二是它和 Spring Boot 的自动配置是两条线排查顺序问题比较绕。我现在更倾向于用FilterRegistrationBean代码只多几行但 Filter 由 Spring 管理依赖注入正常排序一目了然。Configuration public class FilterConfig { Bean public FilterRegistrationBeanTraceFilter traceFilter() { FilterRegistrationBeanTraceFilter registration new FilterRegistrationBean(); registration.setFilter(new TraceFilter()); registration.addUrlPatterns(/*); registration.setOrder(1); return registration; } }setOrder(1)控制 Filter 在 FilterChain 里的顺序数字越小越先执行。多个 Filter 混用时这个顺序直接影响执行效果。比如日志 Filter 要放在最前面才能记录到完整请求信息。4.2 再写一个能用的 Interceptor先实现HandlerInterceptor再通过WebMvcConfigurer注册。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/login, /api/refresh); } }addPathPatterns表示拦截哪些路径excludePathPatterns表示放行哪些路径。这里用 Ant 风格的通配符/api/**包含/api/user/list、/api/order/detail这种多级路径/api/*则只匹配一级。Interceptor 的注册顺序就是执行顺序。Spring 容器里配置多个 Interceptor 时先注册的先执行preHandle后注册的先执行afterCompletion——这点和 Filter 的Order表现并不完全一致实际排障时很容易混。4.3 两个组件同时使用时执行顺序有讲究Filter 的Order只控制 Filter 之间的顺序拦截器的注册顺序只控制拦截器之间的顺序。跨类型时Filter 永远跑在 Interceptor 前面无论 Filter 的 order 是 100 还是 10000。因为 Filter 属于容器层而容器执行完整个 FilterChain 后才会进入 DispatcherServlet 的拦截器机制。这导致一个安全设计上的常识真正严格的入口校验应该放在 Filter 里做因为 Filter 更靠外更早拦截能避免无效请求进入 Spring 上下文业务级别的权限校验才适合放在 Interceptor因为 Interceptor 能拿到HandlerMethod可以做方法级细粒度控制。4.4 现场演示组合链路跑出来的日志打个实际请求完整链路应该是[Filter-before] 打印请求路径 [IAuthInterceptor-preHandle] 校验 token [ILogInterceptor-preHandle] 记录开始时间 Controller 查询数据 [ILogInterceptor-postHandle] 记录结束时间 [IAuthInterceptor-afterCompletion] 清理 ThreadLocal [Filter-after] 打印响应耗时这两个拦截器的执行顺序很有意思preHandle按注册顺序执行但postHandle和afterCompletion会反过来后注册的先执行。这是 Spring MVC 的经典设计类似于一个洋葱模型一层层进去再一层层出来。5. 生产环境怎么选型它们不是对立关系5.1 Filter 更适合做这些“入口脏活”Filter 因为站位靠前天然适合处理“和业务无关但必须最先搞定”的事情。字符编码处理。很多项目最外层 Filter 就是设置请求响应编码的Spring Boot 自带CharacterEncodingFilter也是这个道理必须在业务代码接触参数之前把编码定下来。跨域配置。CORS 响应头加到 Filter 里所有接口统一生效。虽然 Spring 也有CorsFilter但原理都一样——靠 Filter 提前给响应打上Access-Control-Allow-Origin等头。请求日志统一记录。想在所有接口入口记录完整请求信息Filter 是最合适的位置因为它在整个 Spring 处理之前就能拿到原始请求体。不过请求体只能读一次用 Filter 记日志时要考虑是否使用包装 Request 的方式缓存请求体否则后续 Controller 就读不到参数了。统一包装响应。如果业务需要把所有响应都包成统一的 JSON 结构Filter 能做但有一点麻烦——直接用原始HttpServletResponse写入后后续链路过完之后就没法再改了。想要在 Filter 里改响应体通常需要自定义HttpServletResponseWrapper重写getOutputStream和getWriter把输出截获到内存里在chain.doFilter结束后统一写入。这块要对缓冲和 Content-Length 处理有把握否则会出现中文乱码或响应截断。5.2 Interceptor 更适合这些“业务主路检查”Interceptor 能拿到 Handler 和 Spring 容器里的 Bean适合做和具体业务强相关的逻辑。登录状态校验。这是最经典的使用场景。通过HandlerMethod判断方法上有没有自定义注解比如RequireLogin没有就直接放行有则校验 token、判断登录态。Filter 拿不到方法级别的元数据做不了这么细。权限角色校验。和登录类似基于用户角色、操作权限做控制Interceptor 可以结合 Spring Security 或自定义权限 Service 做二次校验且能直接注入权限服务。性能监控和埋点。在preHandle里记录请求进入时间在afterCompletion里计算总耗时这种逻辑只依赖请求上下文和 Handler非常适合放 Interceptor。用户上下文注入。登录用户在preHandle确认后放入ThreadLocalController 里直接可以取。注意在afterCompletion里清理ThreadLocal否则线程池复用时会串数据。5.3 统一登录失效响应为什么我用 Interceptor 而不是 Filter需求是这样的用户登录过期所有接口返回{code: 401, msg: 登录过期}。Filter 也能实现但因为 Filter 在链的最外层chain.doFilter返回时Controller 已经结束Filter 再写响应体就需要包装 Response如果做得不严谨还会出现响应已经提交、写入失败的问题。Interceptor 做这件事就舒服很多。在preHandle里发现未登录直接设置响应状态和 ContentType然后用 Jackson 把 JSON 写出去返回false即可。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { if (!checkLogin(request)) { response.setCharacterEncoding(UTF-8); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\not login\}); return false; } return true; }这个写法简单且不容易出错。Filter 当然能实现同样效果但需要额外处理 Response 包装、缓冲、提交时机等问题对于“返回固定 JSON 给前端”这种需求Interceptor 明显更划算。6. 排查问题速查表看到现象直接对照6.1 高频问题与排查方向现象优先排查方向拦截器完全不进是否实现WebMvcConfigurer并调用了addInterceptors路径是否写成了非/**拦截器进了但 Controller 不执行preHandle返回了false前端拿到的响应是拦截器里写的Filter 里的Autowired为 null用WebFilter注册导致实例不受 Spring 管理改成FilterRegistrationBeanFilter 执行了两次是否WebFilter和FilterRegistrationBean同时注册了同一个 Filter静态资源被拦截Filter 的 urlPatterns 命中所有路径Interceptor 没有把/static/**放进 exclude登录接口出现重定向循环登录拦截器没放行/api/login登录请求自己又被拦截日志里只看到preHandle没有postHandleController 或 Service 层抛了异常postHandle不会执行看afterCompletion的异常参数Filter 里对 Response 操作时报提交后异常doFilter之后又尝试写响应需要包装 Response 或者提前写6.2 几个定位问题的实际技巧拦下请求后可以在 Filter 和 Interceptor 的日志里都带上线程 ID把一条请求的日志串起来看。多线程环境下没有线程 ID 很难分清日志属于哪一次请求。遇到“过滤器不知道在哪个环节把请求挡了”的问题最快的方式是临时在 Filter 的doFilter和 Interceptor 的preHandle里各加一行日志打印请求 URI然后看日志落在哪个位置。如果 Filter 打印了但 Interceptor 没打印问题基本出在 Filter 侧的代码逻辑或路径匹配上。还有一种情况容易被忽略拦截器不生效但项目是 Spring Boot 3.x 且用了 Spring Security。这种情况下 Security 过滤器链可能把请求挡在前面根本没走到自定义 Interceptor。排查时如果连日志都没有先确认最外层过滤器链是否正常放行。6.3 多说一句排除法的适用范围Filter 和 Interceptor 的出问题范围其实是分开的。请求还没进 Spring大概率是 Filter 的问题请求已经进了 Spring 但没进 Controller大概率是 Interceptor 的问题。拿到一个“请求不通”的问题先看入口日志在哪一层断了再决定往哪个方向查能省大量时间。说一个我自己的判断习惯遇到跨域、编码、请求日志这类问题优先考虑在 Filter 层解决遇到和具体业务强绑定的校验逻辑比如登录、权限、操作审计优先考虑 Interceptor遇到“返回统一结构”这类既和协议相关又把控输出内容的需求则要具体分析能用 Interceptor 就直接用 Interceptor别为了技术栈的完整性硬去包一层 Response。写在最后的小建议这篇东西不是从哪本教材里抄来的是这几年排查问题查出来的经验。我自己带人时从来不先问“Filter 和 Interceptor 有什么区别”而是让新人去搭一个最小工程打印一遍请求链路。只要能说清楚哪行日志在什么时候打印以及如果把preHandle改成false会发生什么边界自然就清楚了。你下一次真分不清的时候也别急着背概念去翻一下项目里现有的 Filter 和 Interceptor找一条最典型的请求路径照着日志走一遍。跑通的瞬间很多疑惑会自己解开。