拦截器与过滤器:一次请求穿过 Spring 时,会先经过谁呢 关于作者你好这里是一条泥憨鱼| 专注于后端开发、AI 前沿技术与计算机基础。个人主页一条泥憨鱼通过技术博客记录技术成长分享学习心得与经验。期待与您共同交流精选技术专栏❄️后端开发❄️Redis| 黑马点评❄️从0开始学习计算机网络❄️数据结构与算法❄️苍穹外卖❄️探秘AI与agent校验写在 Filter 还是 Interceptor几乎每个 Spring 项目都会遇到一次。一条请求的完整链路把一次 HTTP 请求想象成穿过几层管道的流体先看它到底走了哪些节点浏览器 → TomcatServlet 容器 → FilterChain → DispatcherServlet → HandlerMapping找到 Controller 方法 → HandlerExecutionChainInterceptor 链 → Controller 方法 → 返回视图/JSON这条链上有一条关键的分界线Filter 属于 Servlet 容器规范Interceptor 属于 Spring MVC。Tomcat 只认识 Servlet 规范里的东西Filter 接口就定义在这个规范里所以它由 Tomcat 调用跟 Spring 没有任何关系。而 DispatcherServlet 是 Spring MVC 的入口 ServletInterceptor 是在它内部、由 HandlerMapping 组装出来的属于 Spring 自己的机制。这个归属差异直接决定了后面所有行为Filter 执行时 Spring 的 ApplicationContext 还没给它做任何注入Interceptor 执行时整个 Spring MVC 的上下文已经就绪能拿到 HandlerMethod 这种描述“哪个类的哪个方法在处理这次请求”的对象。过滤器Servlet 规范里的第一道闸门Filter 接口只有三个方法init、doFilter、destroy。init 在容器启动时调用一次destroy 在容器关闭时调用真正干活的是 doFilterpublic class EncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // chain.doFilter 之前请求进入业务逻辑前 request.setCharacterEncoding(UTF-8); response.setContentType(application/json;charsetUTF-8); chain.doFilter(request, response); // 放行交给链上的下一个节点 // chain.doFilter 之后响应即将返回浏览器前 // 这里能拿到完整的响应适合做耗时统计、响应头补写 } }FilterChain 是一个链式结构chain.doFilter() 的含义是“把请求交给下一个 Filter如果后面没有 Filter 了就交给 Servlet”。这个设计带来一个很有用的性质doFilter 前后都能写逻辑形成一个环绕结构。请求耗时统计、MDC 埋点、响应包装都靠这个环绕能力实现。一个典型的场景是读取请求体做签名校验或日志。HttpServletRequest 的输入流默认只能读一次读完 Controller 就拿不到了这时需要用包装类缓存// 把请求体缓存进内存让后续 Controller 仍能正常读取 ContentCachingRequestWrapper wrapped new ContentCachingRequestWrapper(request); chain.doFilter(wrapped, response); // 请求处理完从缓存里拿到完整请求体 byte[] body wrapped.getContentAsByteArray();Filter 的能力边界也很明确。它拿到的是 ServletRequest这是 Servlet 规范里的通用接口里面根本没有“当前是哪个 Controller 方法”这种概念。想在 Filter 里判断某个方法上有没有 RequireLogin 注解是做不到的。另一个容易踩的点是依赖注入。Filter 由 Servlet 容器创建默认不在 Spring 的 Bean 管理范围内。如果 Filter 里要用 RedisTemplate 或某个 Mapper直接用 Autowired 注入会得到 null。解决办法通常有两个一是把 Filter 注册成 Spring Bean通过 FilterRegistrationBean让 Spring 管理生命周期二是在 Filter 里通过 WebApplicationContextUtils 手动取。前者更干净。拦截器Spring MVC 的 HandlerExecutionChain 机制请求进到 DispatcherServlet 之后HandlerMapping 会做两件事找到处理这次请求的 Handler通常是一个 HandlerMethod封装了 Controller 实例和方法对象再把当前容器里所有匹配路径的 Interceptor 取出来和 Handler 一起组装成 HandlerExecutionChain。这个链路对象就是 Interceptor 机制的核心载体。HandlerInterceptor 有三个回调方法触发时机完全不同public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { // Controller 方法执行之前 // 返回 false 直接中断后续拦截器和 Controller 都不执行 return true; } Override public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView mv) throws Exception { // Controller 方法正常执行完、视图渲染之前 // 注意Controller 抛异常时这个方法不会执行 } Override public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception { // 整个请求处理完成之后无论成功还是异常都会执行 // 适合做资源清理 } }三个方法的执行条件要分清楚- preHandle 返回 false整条链在这里断掉postHandle 和 afterCompletion 都不会执行。- preHandle 返回 trueController 正常执行完postHandle 执行然后 afterCompletion 执行。- Controller 执行中抛异常postHandle 被跳过但 afterCompletion 仍然会执行异常对象通过参数传进来。这个差异很实用。比如在 preHandle 里往 ThreadLocal 写了用户信息清理动作必须放 afterCompletion——放 postHandle 的话一旦业务抛异常ThreadLocal 就永远留在当前线程里线程池复用时会串数据。关键的一点是 preHandle 的第三个参数 handler。对于普通的 Controller 请求这个对象就是 HandlerMethod可以从中拿到方法对象和类对象if (handler instanceof HandlerMethod handlerMethod) { // 读取方法上的注解 RequireLogin annotation handlerMethod .getMethodAnnotation(RequireLogin.class); if (annotation null) { return true; // 没标注解放行 } }这正是 Filter 做不到的事情。基于注解的权限控制、接口级别的限流、操作日志记录都依赖这个能力。登录拦截器怎么写先给出实现。拦截器本身public class LoginInterceptor implements HandlerInterceptor { private final TokenService tokenService; // 通过构造器注入因为拦截器由 Spring 管理 public LoginInterceptor(TokenService tokenService) { this.tokenService tokenService; } Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { // 预检请求直接放行否则浏览器跨域会失败 if (HttpMethod.OPTIONS.matches(req.getMethod())) { return true; } String token req.getHeader(Authorization); if (token null || !tokenService.isValid(token)) { // 返回 JSON不要重定向 resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } return true; } }注册需要在 WebMvcConfigurer 里完成注意拦截器必须由 Spring 管理否则拿不到注入的 TokenServiceConfiguration public class WebConfig implements WebMvcConfigurer { Bean public LoginInterceptor loginInterceptor(TokenService tokenService) { return new LoginInterceptor(tokenService); } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor(null)) .addPathPatterns(/**) // 默认全拦 .excludePathPatterns( /login, // 登录接口 /error, // 错误页 /static/**, /css/**, // 静态资源 /js/**, /images/**, /favicon.ico ); } }这段代码里有三个坑每一个都很常见。第一个坑静态资源和登录页被拦形成死循环。用 addPathPatterns(/**) 全拦之后如果登录页的 HTML、CSS、JS 也被拦浏览器拿不到登录页用户就无法登录登录接口本身被拦也会导致永远登不进去。所以 excludePathPatterns 必须把登录接口和静态资源路径都排除掉。第二个坑OPTIONS 预检被拦。跨域场景下浏览器在发真实请求前会先发一个 OPTIONS 预检请求这个请求不带 Authorization 头。如果拦截器把它拦下来返回 401浏览器会判定跨域失败真实请求根本发不出去。所以 preHandle 开头要先判断 OPTIONS 直接放行。第三个坑返回重定向而不是 JSON。前后端分离项目里前端拿到的应该是 401 状态码和 JSON 错误体由前端决定跳转登录页。如果拦截器直接 resp.sendRedirect(/login)前端会收到一个 HTML 登录页AJAX 回调里解析 JSON 直接报错问题还不好排查。另外如果项目里同时用了 Filter 做 CORS 处理要确保 CORS 相关的响应头在拦截器返回 401 时也带上否则浏览器仍然会报跨域错误。通常把 CORS 处理放在优先级最高的 Filter 里统一做。怎么选与常见误区把两者的差异放在统一维度上对比| 维度 | Filter | Interceptor || 规范归属 | Servlet 容器 | Spring MVC || 能否注入 Bean | 默认不行需注册为 Bean | 可以由 Spring 管理 || 能否拿到方法注解 | 不能 | 能通过 HandlerMethod || 执行次数 | 每个请求一次 | 每个请求一次含 forward 时需注意 || 能否修改请求/响应 | 能可包装 | 不能只能读 || 异常处理位置 | 在 Filter 内自行处理 | 可配合 HandlerExceptionResolver |选择建议很直接- 全局、粗粒度、与业务无关的逻辑放 Filter字符编码、跨域、请求日志、全局限流、请求体包装。- 与业务、权限、注解相关的逻辑放 Interceptor登录校验、基于注解的权限判断、接口级操作日志、接口级限流。一个常见误区是把登录校验全放在 Filter 里然后在 Filter 里用反射去解析 Controller 方法注解。这样做不是不行但等于绕开 Spring MVC 已经准备好的 HandlerMethod 自己重造一遍代码复杂且容易出错属于典型的用错层级。另一个误区是把 Interceptor 和 AOP 混为一谈。两者都能在方法前后织入逻辑但关注点不同Interceptor 绑定的是 HTTP 请求能拿到 HttpServletRequest、HttpServletResponse适合处理请求维度的逻辑AOP 绑定的是方法调用能拿到方法参数和返回值适合处理业务维度的逻辑比如事务、缓存、方法级日志。需要请求上下文时用 Interceptor只需要方法上下文时用 AOP。判断一段逻辑该放哪先问一句它需不需要知道是哪个 Controller 方法在处理这次请求。需要就放 Interceptor因为只有它能拿到 HandlerMethod不需要就放 Filter让它在更靠前的位置、以更低的耦合度完成。这个判断成立的前提是理解了两者的容器归属——Filter 在 Spring 之外Interceptor 在 Spring MVC 之内所有行为差异都从这里长出来。