ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SpringMVC拦截器与过滤器:核心区别、执行顺序与实战选型指南

SpringMVC拦截器与过滤器:核心区别、执行顺序与实战选型指南

1. 项目概述:拦截器与过滤器的核心辨析

在构建基于SpringMVC的Web应用时,我们经常需要处理一些横切关注点,比如权限校验、日志记录、请求参数预处理、响应内容加工等。这时候,HandlerInterceptor(拦截器)和Filter(过滤器)这两个概念就会高频出现。很多开发者,尤其是刚接触Spring生态的朋友,常常会混淆它们,觉得功能上似乎差不多,都是“拦截请求干点事儿”。但事实上,它们在技术栈中的定位、实现机制、执行时机乃至应用场景上,都有着泾渭分明的区别。理解这些区别,不仅是为了应付面试,更是为了在实际项目中做出最合理、最优雅的技术选型,避免因为用错工具而导致功能缺陷或性能瓶颈。今天,我们就来彻底拆解SpringMVC拦截器和Servlet过滤器,从源码设计、执行流程到实战场景,把它们的异同和执行顺序讲透。

简单来说,过滤器是Servlet规范定义的标准组件,作用于Web容器层面,范围更广、更底层;而拦截器是Spring MVC框架定义的组件,作用于Spring的DispatcherServlet处理流程之内,与Spring上下文深度集成,功能更精细、更强大。一个请求从客户端发出到响应返回,会依次经过过滤器链和拦截器链,这个顺序是固定的,但内部的细节却大有乾坤。搞不清这个顺序,就可能出现“日志记录了但权限没拦住”,或者“参数解密了但Spring没接收到”这类让人头疼的Bug。

2. 核心概念与设计哲学深度解析

要理解区别,必须先回到它们的“出身”和设计目标。这就像理解螺丝刀和扳手,虽然都能拧东西,但一个针对螺丝,一个针对螺母,设计初衷不同,用法自然不同。

2.1 Servlet过滤器:Web容器的守门人

过滤器是Java EE(现Jakarta EE)Servlet规范的一部分,定义在javax.servlet(现jakarta.servlet)包中。它的核心接口是Filter。过滤器的设计哲学是提供一个通用的、声明式的机制,用于对客户端请求和服务器响应进行预处理和后处理

核心特性与定位:

  1. 规范标准:它是Java Web应用的标准,任何实现了Servlet规范的Web容器(如Tomcat, Jetty, Undertow)都必须支持。这意味着你的过滤器代码,可以几乎无修改地运行在不同的Web服务器上,可移植性极强。
  2. 作用范围广:过滤器作用于Web容器级别。它过滤的是ServletRequestServletResponse对象。这意味着,所有进入容器的请求和离开容器的响应都会经过过滤器,无论这个请求是请求一个JSP、一个静态资源(如.jpg, .css),还是一个由Spring MVC的DispatcherServlet处理的动态请求。这是过滤器最强大的地方,也是它与拦截器最根本的区别之一。
  3. 功能纯粹:过滤器主要对请求和响应的“流”进行操作。例如,你可以通过ServletRequestWrapperServletResponseWrapper来修改请求参数、请求体,或者修改响应头、响应体。常见的用例包括:字符编码设置、敏感词过滤、压缩响应内容、记录全局访问日志等。
  4. 与Spring上下文无关:标准的Servlet过滤器在初始化时,无法直接通过@Autowired等方式注入Spring管理的Bean。因为它是由Web容器创建和管理的,生命周期独立于Spring的ApplicationContext。虽然可以通过一些技巧(如DelegatingFilterProxy)让过滤器委托给Spring Bean执行,但这本身也说明了它们的隔离性。

2.2 Spring MVC拦截器:MVC框架的流程钩子

拦截器是Spring MVC框架的一部分,定义在org.springframework.web.servlet包中。它的核心接口是HandlerInterceptor。拦截器的设计哲学是在Spring MVC处理请求的特定生命周期节点插入自定义逻辑,实现与业务处理流程紧密相关的横切功能

核心特性与定位:

  1. 框架特定:它是Spring MVC框架的专属特性。如果你的Web应用没有使用Spring MVC(比如只用Spring Boot的WebFlux做响应式编程,或者用其他MVC框架),那么拦截器就不存在。
  2. 作用范围精准:拦截器作用于Spring的DispatcherServlet内部。只有当请求被DispatcherServlet接收,并且已经成功映射到一个具体的处理器(Handler,通常是我们写的@Controller中的方法)之后,拦截器链才会开始执行。这意味着,对于静态资源的请求、直接访问的JSP页面,或者未被DispatcherServlet处理的请求,拦截器是不会触发的
  3. 生命周期钩子丰富HandlerInterceptor接口定义了三个方法,对应了处理器执行的关键节点:
    • preHandle:在处理器方法执行之前被调用。可以进行权限校验、参数预处理等。返回true则继续执行拦截器链和处理器;返回false则中断流程,后续拦截器和处理器都不会执行。
    • postHandle:在处理器方法执行之后,但在视图渲染之前被调用。此时可以修改ModelAndView对象,向模型添加公共数据等。
    • afterCompletion:在整个请求处理完毕(即视图渲染完毕)之后被调用。通常用于资源清理、性能监控统计(计算整个请求耗时)等。注意:即使preHandle返回false,已经执行了preHandle且返回true的拦截器的afterCompletion方法依然会被调用,这为资源清理提供了保障。
  4. 深度集成Spring:拦截器本身是由Spring IoC容器创建和管理的Bean。因此,你可以在拦截器中轻松地使用@Autowired注入其他Spring Bean,如Service、Repository、配置属性等,实现非常复杂的业务逻辑。这是拦截器相对于过滤器的一大优势。

一个关键类比:想象一个公司的前台(Web容器)。过滤器就像是公司大门的安检(Filter),每个进出的人(请求/响应)都必须经过安检,检查包裹、登记信息。而拦截器就像是某个特定部门(如研发部,对应DispatcherServlet)内部的会议室管理规则。只有已经进入研发部、并且预定了一间具体会议室(映射到Handler)的访客,才会触发这些规则(Interceptor),比如“进入会议室前必须签到(preHandle)”、“会议结束后要填写反馈表(postHandle)”、“所有人离开后要关灯锁门(afterCompletion)”。那些只是来送快递(访问静态资源)或者去其他部门的人,不会触发研发部的会议室规则。

3. 执行流程与顺序的逐帧拆解

理解了各自的身份,我们来看它们是如何协作的。一个HTTP请求的处理流水线是理解顺序的关键。下图清晰地展示了从请求到响应的完整路径:

请求生命周期:Filter->DispatcherServlet->Interceptor->Controller->Interceptor->View->Filter-> 响应

我们来分解这个流程中的每一个关键步骤:

3.1 第一阶段:过滤器链的预处理

  1. 请求到达容器:HTTP请求到达Tomcat等Servlet容器。
  2. 创建请求/响应对象:容器创建HttpServletRequestHttpServletResponse对象。
  3. 执行过滤器链:容器根据web.xml@WebFilter注解(Spring Boot中常用FilterRegistrationBean)定义的顺序,依次调用每个过滤器的doFilter方法。
    • doFilter内部,开发者可以:
      • ServletRequest进行包装或修改(如HttpServletRequestWrapper)。
      • ServletResponse进行包装或修改(如HttpServletResponseWrapper)。
      • 执行核心逻辑(如权限判断)。
      • 调用FilterChain.doFilter(request, response)将请求传递给链中的下一个过滤器,或者如果这是最后一个过滤器,则传递给目标Servlet(对于Spring MVC应用,就是DispatcherServlet)。
    • 如果某个过滤器在doFilter中没有调用FilterChain.doFilter,那么请求就在这里被拦截,直接返回响应,后续所有过滤器、DispatcherServlet、拦截器、控制器都不会执行。这是过滤器拦截请求的唯一方式。

3.2 第二阶段:DispatcherServlet与拦截器链

  1. 进入DispatcherServlet:经过所有过滤器后,请求到达Spring MVC的核心——DispatcherServlet
  2. 映射处理器DispatcherServlet根据请求URL,通过HandlerMapping找到对应的处理器(Handler)和拦截器链(HandlerExecutionChain)。这个链里包含了匹配到的所有HandlerInterceptor
  3. 执行拦截器preHandle:按顺序执行拦截器链中每个拦截器的preHandle方法。
    • 如果某个preHandle返回false,则:
      • 该拦截器之后的拦截器的preHandle不会执行。
      • 对应的处理器(Controller方法)不会执行。
      • 但是,之前已经执行过且preHandle返回true的拦截器,其afterCompletion方法仍会被调用(注意,不是postHandle)。然后流程直接跳到第10步(执行过滤器的后处理)。
  4. 执行控制器方法:所有拦截器的preHandle都返回true后,DispatcherServlet通过HandlerAdapter调用实际的处理器方法(如@RequestMapping标注的方法),执行业务逻辑,并返回一个ModelAndView(或@ResponseBody直接写回响应)。
  5. 执行拦截器postHandle:处理器执行完毕后,倒序执行拦截器链中每个拦截器的postHandle方法。注意这里是倒序!这给了拦截器一个“后进先出”的加工机会。
  6. 渲染视图:如果返回的是视图名,DispatcherServlet会调用ViewResolver解析视图,然后由View进行渲染,将模型数据填入模板,生成最终的响应内容。对于@ResponseBody,内容已在第7步写入响应流。
  7. 执行拦截器afterCompletion:视图渲染完成后(或异常发生后),倒序执行拦截器链中每个拦截器的afterCompletion方法。同样也是倒序。这是进行最终清理和统计的最终位置。

3.3 第三阶段:过滤器链的后处理

  1. 返回过滤器链DispatcherServlet处理完毕,控制权返回到过滤器链。
  2. 执行过滤器后处理逻辑:每个过滤器的doFilter方法中,在调用FilterChain.doFilter()之后的代码,此时开始执行。因为FilterChain.doFilter()是一个“分水岭”,其调用前的代码是请求预处理,调用后的代码是响应后处理。
    • 在这里,你可以对已经由Spring MVC处理过的ServletResponse进行最终修改,比如添加统一的响应头、对响应体进行加密或压缩。
  3. 响应返回客户端:响应经过所有过滤器的后处理,最终由Web容器发送回客户端。

3.4 顺序总结与记忆口诀

总体顺序:Filter(前置) ->Interceptor.preHandle->Controller->Interceptor.postHandle(倒序) ->View Render->Interceptor.afterCompletion(倒序) ->Filter(后置)

记忆口诀“先滤后拦,先正后倒”

  • 先滤后拦:过滤器在拦截器之前开始执行(请求进入时),也在拦截器之后结束执行(响应返回时)。过滤器包裹着整个Spring MVC处理流程。
  • 先正后倒:拦截器的preHandle正序执行,而postHandleafterCompletion倒序执行。想象成一个栈(Stack)的压入和弹出过程。

4. 实战场景与选型指南

理论清晰了,到底该用哪个?下面通过几个典型场景来分析。

4.1 必须使用过滤器的场景

  1. 全局字符编码设置:你需要处理所有请求的编码,包括静态文件、JSP、API。这必须在请求到达任何具体处理逻辑之前完成。

    // 示例:在Filter中设置UTF-8编码 @Component public class CharacterEncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); // 必须调用,否则请求中断 } } // 在Spring Boot中通过配置类注册 @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<CharacterEncodingFilter> registrationBean() { FilterRegistrationBean<CharacterEncodingFilter> bean = new FilterRegistrationBean<>(); bean.setFilter(new CharacterEncodingFilter()); bean.addUrlPatterns("/*"); // 匹配所有路径 bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 设置最高优先级,最先执行 return bean; } }
  2. 静态资源访问控制/日志:你想记录所有对/images/,/css/等目录的访问日志。这些请求根本不会进入DispatcherServlet,拦截器无效,必须用过滤器。

  3. 响应内容压缩(GZIP):你想对所有文本响应(JSON, HTML)进行GZIP压缩以节省带宽。这需要在响应最终发出前,对输出流进行包装和压缩,是典型的过滤器后处理逻辑。

  4. 跨域资源共享(CORS):虽然Spring MVC提供了@CrossOrigin注解和WebMvcConfigurer配置,但在某些复杂场景(如需要动态配置、支持预检请求OPTIONS)下,一个配置完善的CORS过滤器是更通用和可靠的选择,因为它能处理所有类型的请求。

4.2 优先考虑拦截器的场景

  1. 基于会话(Session)或Token的权限校验:你需要判断用户是否有权限访问某个Controller的某个方法。这需要用到Spring管理的用户服务、Token解析服务等Bean。拦截器可以方便地注入这些Bean,并且在preHandle中,你已经能获取到具体的处理器(Handler)信息,可以做更细粒度的权限判断(比如基于注解)。

    @Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private UserService userService; // 方便注入Spring Bean @Autowired private JwtTokenUtil jwtTokenUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 判断handler类型,避免静态资源等 if (!(handler instanceof HandlerMethod)) { return true; } // 2. 获取方法上的注解,进行精细控制 HandlerMethod handlerMethod = (HandlerMethod) handler; RequiresPermission anno = handlerMethod.getMethodAnnotation(RequiresPermission.class); if (anno == null) { return true; } // 3. 执行复杂的、依赖Spring Bean的权限校验逻辑 String token = request.getHeader("Authorization"); User user = jwtTokenUtil.parseToken(token); if (user == null || !userService.hasPermission(user, anno.value())) { response.sendError(HttpStatus.FORBIDDEN.value(), "权限不足"); return false; // 中断流程 } // 将用户信息放入请求属性,供Controller使用 request.setAttribute("currentUser", user); return true; } } // 通过WebMvcConfigurer注册 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") // 只拦截API路径 .excludePathPatterns("/api/auth/login"); // 排除登录接口 } }
  2. 接口耗时监控:你想记录每个Controller方法的执行时间。在preHandle中记录开始时间,存入request属性,在afterCompletion中计算耗时并打印日志。这需要用到处理器执行完毕的钩子,过滤器难以在Controller粒度上实现。

  3. 统一处理Controller的返回结果:在postHandle中,你可以修改ModelAndView,为所有视图添加一些公共模型数据(如当前年份、用户菜单)。虽然现在更推荐使用@ControllerAdvice配合@ModelAttribute,但拦截器在某些场景下仍是一种选择。

  4. 防重复提交:在preHandle中,根据请求参数和用户身份生成一个唯一令牌,并检查该令牌是否已被使用过。这需要与业务状态(如Redis中的令牌记录)交互,拦截器能方便地注入RedisTemplate等Bean。

4.3 混合使用与协作案例

一个成熟的Web应用通常会同时使用两者,各司其职。

案例:一个安全的API网关层

  1. Filter 1 (LoggingFilter):最先执行,记录所有进出的原始请求和响应日志(包括静态资源)。它只关心流量,不关心业务。
  2. Filter 2 (CorsFilter):处理跨域请求,添加必要的响应头。
  3. Filter 3 (CharacterEncodingFilter):设置请求/响应的编码。
  4. DispatcherServlet
    • Interceptor 1 (AuthInterceptor):进行JWT Token解析和基础身份认证,将用户信息放入请求属性。
    • Interceptor 2 (PermissionInterceptor):进行细粒度的接口权限校验。
    • Controller:执行业务逻辑。
    • Interceptor 2 (postHandle):可能无需操作。
    • Interceptor 1 (postHandle):可能无需操作。
    • Interceptor 1 & 2 (afterCompletion):清理线程局部变量,记录最终访问日志(包含业务状态)。
  5. Filter 4 (ResponseWrapperFilter):对API返回的JSON格式进行统一包装(如添加code,msg,data字段),或进行响应加密。
  6. Filter 1 (后处理):记录最终的响应大小和状态码。

在这个协作中,过滤器负责协议层、传输层的通用处理,而拦截器负责业务层、应用层的特定逻辑。

5. 常见陷阱、疑难排查与最佳实践

即使理解了原理,在实际编码和运维中还是会遇到各种坑。下面分享一些实战中积累的经验。

5.1 典型问题排查清单

问题现象可能原因排查思路与解决方案
拦截器对静态资源不生效静态资源请求未经过DispatcherServlet。1. 检查拦截器的addPathPatterns,确保没有错误地包含了静态资源路径。
2. 确认Spring MVC的静态资源处理配置(如ResourceHandlerRegistry),默认情况下静态资源由容器或Spring的ResourceHttpRequestHandler处理,不经过拦截器链。这是正常行为,如需拦截,应使用过滤器。
@Autowired在过滤器中为null过滤器由Servlet容器管理,非Spring Bean。1. 让过滤器类实现ServletContextAware等接口来获取Spring上下文,较为复杂。
2.推荐:使用DelegatingFilterProxy(Spring Boot默认)或FilterRegistrationBean包装一个Spring Bean作为过滤器。在Spring Boot中,最简单的方式是直接@Component定义一个Filter,Spring Boot会自动将其注册为DelegatingFilterProxy
拦截器的postHandleafterCompletion未执行1. 对应的preHandle返回了false
2. 控制器方法或更早的流程中抛出了未处理的异常。
1. 检查preHandle逻辑,确保在正常流程下返回true
2. 使用全局异常处理器(@ControllerAdvice+@ExceptionHandler)捕获异常,确保流程能正常走到视图渲染阶段。注意:即使有异常,afterCompletion仍会执行(前提是preHandle返回了true),这是进行资源清理的好地方。
修改了请求参数,但Controller获取不到在过滤器中修改request参数的方式不对。HttpServletRequest的参数Map默认是只读的。必须使用HttpServletRequestWrapper重写getParameter,getParameterMap等方法来实现参数的修改。直接调用request.setAttribute()设置的是请求属性,不是请求参数。
过滤器顺序混乱依赖了未声明的过滤器顺序。在Spring Boot中,使用@Order注解或实现Ordered接口来定义过滤器的执行顺序。数字越小,优先级越高,越先执行。对于FilterRegistrationBean,使用setOrder方法。切记:过滤器的“后处理”代码执行顺序与“前处理”相反,是“先进后出”
响应已被提交,无法修改头信息在拦截器postHandle或过滤器的后处理中,尝试调用response.sendRedirect()或设置头信息,但此时响应流可能已关闭或提交。1. 重定向操作应尽量在preHandle或控制器方法中完成。
2. 修改响应头尽量在过滤器链的最开始或控制器的处理阶段。
3. 对于响应内容的修改(如包装JSON),确保使用HttpServletResponseWrapper包装响应,并小心处理输出流的关闭时机。

5.2 最佳实践与心得

  1. 职责分离,保持纯粹:让过滤器做它擅长的事(编码、压缩、全局日志、CORS),让拦截器做它擅长的事(认证、授权、业务日志、性能监控)。避免在过滤器中写大量业务逻辑,也避免用拦截器去处理静态资源。

  2. 警惕性能瓶颈:过滤器和拦截器在每个请求中都会执行,其中的代码必须是轻量级、无阻塞的。避免在其中进行复杂的数据库查询、远程HTTP调用等IO操作。如果必须做,考虑异步处理或缓存。

  3. 善用Spring Boot的自动化配置:对于字符编码、CORS等通用需求,Spring Boot提供了spring.http.encoding.charset,spring.mvc.cors.*等配置属性,通常无需自己编写过滤器。先查阅官方文档,看是否有现成的、经过充分测试的配置。

  4. 拦截器的afterCompletion是资源清理的保险栓:无论请求处理成功还是抛出异常,只要对应的preHandle返回了trueafterCompletion就一定会被执行。这是释放线程局部变量(ThreadLocal)、关闭非托管资源(如手动打开的数据库连接)的绝佳位置。

  5. 测试你的拦截链:编写集成测试,模拟发送请求,验证过滤器、拦截器、控制器的执行顺序和结果是否符合预期。特别是当你有多个过滤器和拦截器时,顺序测试至关重要。

  6. 关于@Componentvs@WebFilter:在Spring Boot中,如果你希望过滤器能使用@Autowired注入Bean,推荐使用@Component(或@Configuration中定义@Bean)的方式,让Spring管理过滤器的生命周期。单纯的@WebFilter注解需要配合@ServletComponentScan使用,且该过滤器实例不是Spring Bean,无法直接注入依赖。

理解SpringMVC拦截器与过滤器的区别和执行顺序,是构建健壮、可维护Web应用的基石。它帮助你清晰地规划不同层次的处理逻辑,避免功能冲突和循环依赖。下次当你需要添加一个全局处理逻辑时,不妨先问自己几个问题:这个逻辑需要处理静态资源吗?它需要访问Spring容器中的Bean吗?它需要在Controller方法执行前、后,还是视图渲染后执行?回答完这些问题,该用Filter还是Interceptor,顺序如何,自然就清晰了。

返回列表