ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring MVC中Filter、拦截器、Aspect执行顺序详解

2026/9/19 4:26:27 拓冰建站 浏览量
Spring MVC中Filter、拦截器、Aspect执行顺序详解 我之前在维护一个老项目时遇到过这样一件事新来的同事给某个接口同时加了权限拦截器、日志切面又在外部补了一个Filter做请求签名校验。结果上线后,日志顺序完全对不上,签名校验也被绕了半套。当时几个人对着调用链猜了很久,最后扒代码才发现,大家其实对Controller调用前后的执行顺序理解不统一——有人以为Filter最先,有人以为是Aspect先跑,还有人以为拦截器挡在切面外面。这个问题的根源在于Spring MVC和Servlet容器是两个不同层次的东西。搞清楚Controller的Aspect、Filter和拦截器的执行顺序不只能让你日志好看,更影响权限、事务、链路追踪这些核心功能的正确性。这篇博客我会用一次完整可复现的测试,把Filter、拦截器、Aspect的顺序彻底讲透,并把踩过的坑一并整理出来。适合正在用Spring Boot做Web开发、被调用链搞晕的读者。1. 先理清三者的层级关系1.1 它们根本不在同一个容器里很多人把Filter、拦截器、Aspect混在一起,以为都是请求进来后要执行的一段代码,然后纠结谁先谁后。实际上,它们分别由不同的容器管理,目标也不一样。Filter是Servlet规范里的东西,运行在Servlet容器内部,由Servlet容器(比如Tomcat、Jetty)管理。它几乎能在HTTP请求进入任何Servlet之前执行,天然可以作用于静态资源、Servlet、以及Spring MVC的DispatcherServlet。拦截器(HandlerInterceptor)是Spring MVC框架提供的能力,它只作用于Spring MVC处理流程中,也就是DispatcherServlet分配请求、调用HandlerAdapter处理Controller方法的前后。它依赖Spring容器,可以方便地注入各种Bean。Aspect(AOP切面)则是Spring AOP基于代理机制实现的横切能力。它不关心你是不是HTTP请求,它关心的是某个方法调用——比如Service方法、Controller方法、甚至是任意被Spring管理的Bean方法。它和请求链路本身没有直接关系,只是可以切入Controller方法。理解这个层级,顺序就清晰了一半。从代码执行路径来看,一个大致的嵌套关系是Servlet容器 - Filter - DispatcherServlet - 拦截器 - AOP代理 - Controller方法。1.2 一次请求的完整穿行过程当外部请求进来时,Tomcat先接收到HTTP请求,把它包装成HttpServletRequest交给Filter链。每个Filter调用doFilter方法,如果通过就调用chain.doFilter放行,请求进入DispatcherServlet。DispatcherServlet找到匹配的HandlerMapping,定位到某个Controller方法,然后通过适配器调用。但注意,这个调用Controller方法可不是直接调用你写的方法。如果目标Bean被AOP代理了,那么调用会先走到代理对象,代理的执行链中包含Aspect的通知逻辑,然后才真正进入Controller方法。而在进入Controller之前,拦截器的preHandle会在HandlerAdapter里执行,postHandle在Controller方法返回、视图渲染之前执行,afterCompletion在整个请求处理完成后执行。所以默认情况下,执行顺序可以简化成Filter - Interceptor.preHandle - Aspect.Around前 - Controller方法 - Aspect.Around后 - Interceptor.postHandle - Interceptor.afterCompletion - Filter后续这个顺序不是谁规定的,而是由Tomcat、Spring MVC、Spring AOP三层嵌套自然推导出来的。后面我会用实际日志验证。2. 执行顺序的核心规则与逻辑2.1 默认顺序的逐步解析先看最典型的正向流程。我写了一个示例项目,把Filter、拦截器、Aspect、Controller都配置好,并在每一步打印日志。为了直观,我用符号标注执行时间点。首先是FilterComponent public class DemoFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println([Filter] 进入 doFilter 前); chain.doFilter(request, response); System.out.println([Filter] 离开 doFilter 后); } }然后是拦截器,通过WebMvcConfigurer注册public class DemoInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { System.out.println([Interceptor] preHandle); return true; } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { System.out.println([Interceptor] postHandle); } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println([Interceptor] afterCompletion); } }注册方式Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new DemoInterceptor()).addPathPatterns(/**); } }注意我这里把拦截器注册到/**路径,让它能覆盖全部请求。然后是Aspect,切入Controller层Aspect Component public class DemoAspect { Around(execution(* com.example.controller..*.*(..))) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println([Aspect] Around 前); Object result joinPoint.proceed(); System.out.println([Aspect] Around 后); return result; } }Controller很简单RestController RequestMapping(/demo) public class DemoController { GetMapping(/hello) public String hello() { System.out.println([Controller] 方法执行中); return hello; } }启动项目后,请求/demo/hello。控制台输出顺序如下[Filter] 进入 doFilter 前 [Interceptor] preHandle [Aspect] Around 前 [Controller] 方法执行中 [Aspect] Around 后 [Interceptor] postHandle [Interceptor] afterCompletion [Filter] 离开 doFilter 后这验证了默认顺序。这个顺序非常关键一旦你在Filter里做了校验,校验通过再放行,拦截器和Aspect才有机会执行。而Aspect的Around包裹了Controller方法本身,所以它的前在Controller方法之前,它的后在Controller方法之后,但理论上在拦截器的postHandle之前。这里有一个细节值得注意——Around的后逻辑其实是在Controller方法返回之后、HandlerAdapter继续处理之前执行的,而拦截器的postHandle也是在Controller返回后执行。我的实际测试表明,Aspect的Around后的日志会先于postHandle打印,原因在于AOP代理是包裹方法调用的最内层,postHandle需要在DispatcherServlet的调用栈中等待代理方法完全返回后才执行。2.2 前置条件变化时顺序会怎么变理解了默认顺序还不够,因为一旦某个环节阻断或异常,整个链条会发生变化。Filter放行与否Filter中如果调用chain.doFilter,后续才能执行;如果Filter直接返回响应而没有放行,那么后续所有拦截器、Aspect、Controller都不会执行。Interceptor.preHandle返回false如果preHandle返回false,Spring MVC会停止请求处理。拦截器链中尚未执行的拦截器会被跳过,Controller和Aspect也不会执行。但注意已经执行的拦截器的afterCompletion会按照逆序触发吗答案是只有已经被调用过preHandle且返回true的拦截器,在后续异常或完成时,其afterCompletion才会被调用。所以如果第一个拦截器返回false,它自己的afterCompletion也不会被调用。这个细节经常被忽略。异常发生如果Controller方法抛出异常,那么Around中proceed()后面的代码不会执行除非你在aspect中捕获异常,而是向上传播。postHandle不会执行,但afterCompletion会执行,而且Aspect可以捕获异常后改变返回值。需要注意的是,如果Aspect没有捕获异常,异常会一路传到DispatcherServlet,再传到Filter,如果Filter没有catch,最终Tomcat会返回错误页。日志顺序中Around 后不打印,但afterCompletion仍会打印。2.3 为什么Aspect会比拦截器更接近方法这里要提一个很多人问的点既然拦截器位于DispatcherServlet和Controller之间,而AOP代理也在Controller之前,为什么不是拦截器先执行?实际上在链路中,拦截器的preHandle确实先于Aspect执行。但为什么Aspect的后会先于拦截器的postHandle这就要从Spring MVC处理流程看起。HandlerAdapter调用目标Handler时,如果Handler本身被CGLIB或JDK动态代理包装,那么调用的是代理方法。代理方法内部,切面逻辑包裹了真实Controller方法。也就是说,proceed()之后的代码其实还在代理方法内部,只有整个代理方法返回后,控制权才回到HandlerAdapter,然后才执行postHandle。因此Aspect的后一定在拦截器postHandle之前。如果调换顺序,那就违背了AOP代理的基本原理。人话版你可以把请求处理想象成洋葱,Filter是最外层,拦截器稍内,Aspect再内,Controller是芯。剥洋葱的时候从外到里,剥完里层往外退的时候,Aspect先出来,再到拦截器,最后到Filter。3. 实操验证的完整配置与注意点3.1 从零搭建可复现的最小工程为了确保我们讨论的不是纸上谈兵,我建议你直接搭一个最小的Spring Boot项目来实验。不需要复杂的业务逻辑,只需引入spring-boot-starter-web和spring-boot-starter-aop。pom.xml关键依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency主类不需要特殊配置,用Spring Boot默认的SpringBootApplication即可。但要注意启用AspectJ支持。在Spring Boot中,只要classpath存在spring-boot-starter-aop,AOP自动配置就会生效,不需要额外EnableAspectJAutoProxy。不过你如果自己维持非Spring Boot环境,就需要手动添加这个注解。3.2 Filter、拦截器和切面的配置细节Filter的注册方式。我在前面用了Component注解,这种方式很直接,但缺点是Filter对所有URL生效,且执行顺序不可控。更推荐用FilterRegistrationBean显式注册,可以指定urlPatterns和order。Spring Boot中,Component注册的Filter会被自动适配,doFilter里不要忘记调用chain.doFilter。多个Filter会按Order注解或FilterRegistrationBean.setOrder决定顺序。拦截器的执行顺序则和注册顺序相关registry.addInterceptor先添加的先执行preHandle,但afterCompletion逆序执行。所以不要只依赖直觉,最好通过日志验证。Aspect的切入点。我上面用execution(* com.example.controller..*.*(..))来匹配Controller包下所有方法。在Spring AOP中,Controller方法也可以被切入,因为DispatcherServlet调用的是一个Spring Bean,而Spring Bean默认被AOP代理。但要注意,如果Controller的方法不是public,或者类没被Spring管理,切面就不生效。还有,如果用Aspect在Controller类上,可能会产生代理问题,但一般不推荐这么写,因为横切关注点应独立。拦截器不生效的典型原因是注册了但没addPathPatterns,或者路径匹配不符合预期。Spring Boot 2后,自定义拦截器必须通过WebMvcConfigurer.addInterceptors注册,只用Component给拦截器加注解是没有用的,它不会自动进入请求处理链。3.3 日志中每个阶段的前后时机运行测试时,还可以通过增加线程睡眠来观察时序。你可以在Around前打印当前时间戳,在postHandle打印时间戳,会发现两者几乎是紧邻的,如果Controller方法执行很快,时间差极小。但如果你在Around的proceed前做大量计算,拦截器的preHandle已执行完成,所以Controller的响应时间包含AOP前的耗时。这也是为什么在一些监控系统里,如果你把耗时统计放在Aspect中,会漏掉拦截器和Filter的耗时——它们不是同一层,别混在一起统计。我实际项目里就把接口耗时统计放在Filter中,这样能覆盖到整个HTTP请求;而Aspect只统计业务方法自身耗时。两者各有侧重。3.4 异步请求对顺序的影响现代项目使用Callable或DeferredResult处理异步请求时,执行顺序会发生变化。在异步场景下,preHandle和postHandle可能出现异常:Spring MVC的拦截器可以被configureAsyncSupport配置,异步请求开始后,afterCompletion会在异步线程完成时被调用,而不是请求线程中。这种情况下,Aspect如果切入Controller方法,可能在一个线程中执行,而Filter后续逻辑在另一个线程才继续,日志顺序可能不再是简单的线性,甚至会让人怀疑过滤器是否被绕过。但这不是顺序规则失效,而是异步切换线程导致的。处理异步时,最好把上下文传递(比如TraceId)放在Filter中,并使用request.getAttribute方式传递到不同线程。3.5 测试环境的Provider与坑有些环境里Spring Boot会对Feign、RestTemplate等调用也进行AOP代理。如果你写的切面表达式是execution(* com.example.controller..*.*(..)),那只会切入Controller包,不会有影响。但若切面表达式写成execution(* com.example..*.*(..))这种,你可能会发现拦截器只执行一次,但Aspect却不只执行一次,因为可能内部Bean自调用也会触发切面。不过Controller类之间的自调用通常不会,因为它们是本类调用。但Service中this.method()自调用是绕过代理的,这是一个经典坑。如果你在Controller中调用了别的Controller方法(通过注入),该Bean的代理依然生效,切面会再次切入。4. 执行顺序中的常见问题与排查技巧4.1 为什么拦截器preHandle里注入的Service是null这其实不是执行顺序问题,而是初始化时机问题。Spring MVC拦截器默认被Spring容器管理,如果你在拦截器里用Autowired注入Service,通常是可以生效的。但如果拦截器是在WebMvcConfigurer中用new创建,而不是通过Spring管理,那么Autowired必然为null。我自己就踩过这个坑在注册拦截器时写了registry.addInterceptor(new AuthInterceptor()),而在AuthInterceptor里用Autowired注入了RedisService,结果运行时空指针。解决方案是,让拦截器本身也是一个Spring BeanComponent public class AuthInterceptor implements HandlerInterceptor { Autowired private RedisService redisService; }注册时直接注入Configuration public class WebConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor); } }这样Spring会帮你完成依赖注入。4.2 Filter和拦截器都做了鉴权,为什么Aspect还能先执行有一种常见场景是你想在Filter做鉴权,失败则不再执行后续逻辑。但如果在Filter中鉴权通过后直接放行,而拦截器里又定义了权限校验,结果Aspect反而先于拦截器的权限校验执行了长耗时逻辑,这显然不是我们想要的。这时候要检查拦截器是否真的配置了路径,preHandle返回是否为true。如果preHandle返回true并且Aspect先执行,那说明拦截器其实没匹配到该路径,或路径规则写错了。我建议在这种情况下,把通用的、代价高的校验尽量提前到Filter层,而把业务相关、较弱敏感的校验放拦截器。过滤器可以利用HttpServletRequest直接判断,但缺点是无法获取Controller方法上的注解信息。如果你希望根据方法上某个自定义注解决定是否放行,那就必须在拦截器里做,因为拦截器的handler参数可以解析出Method,而Filter做不到。4.3 如何精确控制执行顺序如果你真的需要调整顺序,需要理解不同层的控制方式。多个Filter之间使用FilterRegistrationBean或Order控制先后,order数值越小越先执行。在Filter链中,先后会影响其他Filter的执行顺序。多个拦截器之间按addInterceptor调用的顺序执行preHandle;postHandle和afterCompletion则逆序执行,这一点和Filter链的“先进后出”略有不同。拦截器的顺序可以借助Order在拦截器Bean上声明,但实际仍以注册顺序为准。多个Aspect之间使用Order注解或实现Ordered接口控制,数值越小越先执行。当多个切面包裹同一个方法时,先执行的Aspect的Around前会先打印,proceed内部继续执行下一个Aspect,最后一个Aspect再调用真实方法;返回时,顺序完全反转。下面这张表可以帮你快速记忆环节注册方式控制顺序的机制作用范围FilterFilterRegistrationBean / ComponentOrder / setOrderServlet请求,含所有资源拦截器WebMvcConfigurer.addInterceptors注册顺序Spring MVC处理器AspectComponent/AspectOrder / Ordered接口Spring Bean方法4.4 对返回值的修改会带来什么连锁反应Filter、拦截器、Aspect中都可以对响应做调整。比如Aspect可以修改Controller的返回值如果你的Controller返回一个对象,Aspect可以把它改写成另一个对象。但这种修改对拦截器的postHandle来说是透明的,因为它拿到的ModelAndView已经是经过视图解析的形式,不一定能看到修改后的对象。而Filter拿到的是HttpServletResponse,更不利于修改响应体。所以如果你要做全局响应包装,一般选Filter/Aspect都有自己的做法,但要注意顺序带来的重复包装问题。我在一个项目中想在Filter里统一给返回体加签名,结果发现Filter设置响应头后,拦截器里又改了一遍,导致签名不一致。后来规范了各层职责Filter只做协议层处理,拦截器处理会话,切面处理业务增强。这样顺序问题才不会变成逻辑Bug。4.5 排查执行顺序的三板斧光靠理论容易翻车,实际排查时我习惯用三板斧。第一板斧加日志,在每个环节打印同一请求的标识(比如UUID)。可以在Filter中往request设置traceId,拦截器、Aspect、Controller中都能通过RequestContextHolder取同一个值。日志输出后,一眼就能看出顺序。如果发现日志缺失,就知道哪一层没执行,或执行顺序被异步干扰了。第二板斧断点调试。在Filter的doFilter、拦截器的preHandle、Aspect的Around、Controller方法中各打断点。用Debug模式跑一次,观察方法调用栈的变化。这个方法最直观,能看清代理是如何嵌套的。第三板斧使用Spring Actuator的Beans端点查看代理情况。如果某个Controller被代理成了DemoController$$EnhancerBySpringCGLIB,说明AOP已经生效;如果显示是原始类,说明AOP没生效。排查时很有用。5. 我能给出的几条实战建议先给结论,再讲原因。在大多数常规的Spring Boot Web项目中,我建议按这样分配职责Filter只干三件事字符编码、跨域处理、全局请求日志/打点。除非你非常清楚后果,否则不要在Filter里写太重的业务逻辑。因为Filter层拿不到Spring MVC的Handler,很多业务对象还没有注入,容易写出隐性Bug。拦截器干权限校验、登录态校验、请求频次控制、公共参数校验。这些操作往往需要读取注解或方法信息,拦截器是最合适的位置。用preHandle做校验,不通过就返回false并直接写响应。注意preHandle要尽早、尽量快,避免阻塞后续处理。Aspect干事务、缓存、日志埋点、数据权限、审计。AOP的方法级粒度最适合这类横切逻辑。但如果切面范围过大,注意避免自调用导致代理失效。执行顺序本身不是一句“Filter最先”就能涵盖的。Aspect切入的是方法调用,而拦截器切入的是处理器执行,这两者在微秒级的时间差上也有区别。理解底层模型比死记顺序重要得多。在你实际设计链路时,可以把这份顺序当成默认配置,但在以下几种情况要特别留意设置了拦截器,但方法经过AOP代理后,异常抛出时AfterThrowing会先捕获,还是afterCompletion先捕获实测中,如果Aspect内部没有catch,异常先经过Aspect的异常通知逻辑(如果有),再传回HandlerInterceptor的afterCompletion。所以如果你想记录异常,两者都能捕获,但捕获顺序不同,最终日志会出现重复记录。建议只选一层做异常监控,避免重复。使用Spring Security时,它内部使用的是Filter链,这个过滤器链位于我们自定义Filter之后还是之前Spring Security的过滤器链默认在Servlet容器中靠后位置,所以如果自定义Filter在最外层顺序靠前,可能先于Security过滤器执行。这个顺序会影响匿名身份获取。我通常把自定义Filter的order设置在SecurityProperties.DEFAULT_FILTER_ORDER之后,或者干脆不在Filter里依赖用户身份,统一在拦截器中取Spring Security的上下文。若你在Around中开启了一个事务,然后调用proceed()返回后立即提交事务,而拦截器afterCompletion里要读取刚更新的数据,可能读不到,因为事务已经提交,但存在数据库主从延迟。这种跨层的一致性问题,比执行顺序本身更加致命。最后分享一个小经验当你发现请求处理顺序不符合预期时,不要先急着改代码,先借助RequestContextHolder和日志把完整链路打印出来。确认每一次调用的线程名、耗时、TraceId,再判断是哪一层被跳过、哪一层被重复调用。顺序问题多数不是“规则不对”,而是某个配置没生效。快速定位的方法有两个,一是看启动日志里是否确认注册了拦截器(AOP切面没有明确的启动日志),二是用调试模式看HandlerMapping里的interceptor列表。只要把这三层关系理清,后续加功能就不慌。而且当你需要引入微服务、网关时,你会发现那个执行顺序同样适用网关相当于Filter层,服务间调用时没有拦截器,但AOP依然有效。理解一次,受用很久。