☰
Filter与Interceptor到底怎么选?Java Web请求链路一次讲透
2026/10/18 7:04:01 网站建设 项目流程

前几天一个同事跑来问我,说用户登录状态明明传过来了,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 MVC,Interceptor 就无从谈起。

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 和 403

2.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 FilterRegistrationBean<TraceFilter> traceFilter() { FilterRegistrationBean<TraceFilter> 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确认后,放入ThreadLocal,Controller 里直接可以取。注意在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;charset=UTF-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 管理;改成FilterRegistrationBean
Filter 执行了两次是否@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,找一条最典型的请求路径,照着日志走一遍。跑通的瞬间,很多疑惑会自己解开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询