☰
SpringMVC请求链路全解析:Filter、Interceptor、AOP与@RequestBody执行顺序
2026/10/10 14:15:37 网站建设 项目流程

前几天同事问了个看起来很简单的问题:在一个SpringMVC项目里,想给所有接口加统一耗时日志,应该用Filter还是Interceptor还是AOP?他之前一直以为这三个东西最终干的事差不多,顶多写法不同。听到这个问题我就知道,他其实没搞明白SpringMVC请求的执行顺序——Filter、Interceptor、@RequestBody解析、AOP切面,乃至Controller方法本身,根本不是在同一层、同一时刻介入请求的。弄清楚这条真实链路,不只是应付面试时背那几句“DispatcherServlet接收请求、调用HandlerMapping、返回ModelAndView”,更是日常排查问题、设计扩展点的基础。这篇文章我打算从实际执行链路出发,把几个组件各自介入的时机、边界和背后的实现原理,一层层讲透。

1. 为什么这个链路值得逐层拆解:一次请求要经过几道关卡

1.1 先给结论:一条请求经历的完整顺序

为了不晕,先用大白话把链路捋一遍。请求从客户端发过来,先进入Tomcat/Jetty这类Servlet容器。容器里有一套Filter链,SpringMVC的DispatcherServlet本质上只是这条链末端的那个Servlet。Filter链里可以做编码处理、跨域、登录态粗校验这类事情,它是请求进SpringMVC之前的“外部关卡”。

请求从Filter链走出来,进入DispatcherServlet后,核心的doDispatch()方法会做几件事:找Handler,也就是对应哪个Controller的哪个方法;找HandlerAdapter,也就是用哪个适配器去执行这个Handler;再执行拦截器链的preHandle;然后调用HandlerAdapter反射执行Controller方法。

这里有个很容易忽视的细节:如果Controller方法签名里有@RequestBody,那么参数的解析就发生在这个阶段,比真正“进入”Controller方法还要早。而且,Controller在Spring容器里往往是一个AOP代理对象,所以“执行Controller方法”这句描述,实际还要穿过一层切面代理。

方法返回后,还有拦截器的postHandle,finally阶段的afterCompletion。最后响应往回走,再顺着Filter链一层层倒回去。完整的时序是这样的:

  1. Filter.doFilter(Servlet容器层)
  2. DispatcherServlet.doDispatch(SpringMVC入口)
  3. HandlerMapping.getHandler 找到HandlerExecutionChain
  4. Interceptor.preHandle
  5. HandlerAdapter.handle
  6. HandlerAdapter内部解析方法参数(@RequestBody在这里读消息体)
  7. 调用AOP代理对象的Controller方法
  8. 切面通知依次执行
  9. Controller方法本体真的执行
  10. 返回值处理器处理结果(比如@ResponseBody序列化)
  11. Interceptor.postHandle
  12. Interceptor.afterCompletion
  13. Filter链逐层返回

把这个顺序列出来之后,很多疑问其实已经解决一半了。比如同事问的“耗时日志放哪”,答案就藏在顺序里:如果你只想统计到Controller方法层面,Interceptor就够;如果你想连“框架解析参数、拦截器本身、序列化”的时间都算进去,就必须用Filter。因为Filter在最外层,它包住了后面所有环节。

1.2 顺序背后挂着的几个核心组件

SpringMVC能把这个顺序跑起来,靠的是几个关键抽象,我先把它们点名,后面章节再逐个展开。

  • DispatcherServlet:整个SpringMVC的前端控制器,所有请求的第一个Spring入口。
  • HandlerMapping:根据URL找Handler,返回HandlerExecutionChain,这个Chain里带着拦截器列表。
  • HandlerAdapter:负责真正调用Handler。@RequestBody参数解析、@ResponseBody返回值处理,都是Adapter内部的工作。
  • HandlerMethodArgumentResolver:参数解析器的总称,@RequestBody只是它要处理的一个分支。
  • HandlerMethodReturnValueHandler:返回值处理器的总称,Controller方法返回后靠它写响应。
  • ProxyFactory:AOP代理的创建入口,Controller被代理后,真正调用时才会穿过切面。

很多人背熟了“SpringMVC工作流程”,无非就是DispatcherServlet收到请求、调用HandlerMapping、再调用HandlerAdapter、返回ModelAndView、交给ViewResolver。但这条流程背出来之后,并不知道每一层里面到底发生了什么。Filter和Interceptor到底谁先执行?@RequestBody到底在哪一步被解析?AOP切面包在Controller外面,那它跟Interceptor的执行顺序怎么排?这些问题不把源码翻一遍、不写个日志Demo验证一遍,靠猜是猜不出来的。

2. Filter 与 DispatcherServlet 的分工边界:请求的第一层预处理发生在哪

2.1 Filter由Servlet容器管理,Spring Boot里照样能注册

Filter的底层归Servlet容器管,它不在SpringMVC的代码路径里。只要容器收到请求,会先构建一个FilterChain,然后按照注册顺序依次执行每个Filter的doFilter方法,直到链路末端是Servlet。在SpringMVC项目里,这个Servlet就是DispatcherServlet。

Spring Boot项目里注册Filter有两个常见写法:一个是写一个类实现javax.servlet.Filter或者jakarta.servlet.Filter,加上@WebFilter注解,然后在启动类上配@ServletComponentScan;另一个是更灵活的FilterRegistrationBean,可以直接new出来并setOrder来控制顺序。我个人更推荐第二种,因为它能拿到Spring容器里的依赖,调试起来也更方便。

@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<OncePerRequestFilter> logFilter() { FilterRegistrationBean<OncePerRequestFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new OncePerRequestFilter() { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { long start = System.currentTimeMillis(); System.out.println("[Filter] request uri = " + request.getRequestURI()); try { chain.doFilter(request, response); } finally { System.out.println("[Filter] cost = " + (System.currentTimeMillis() - start) + " ms"); } } }); registration.addUrlPatterns("/*"); registration.setOrder(1); return registration; } }

这里有个小坑:虽然FilterOrdered接口有常量,但不同容器对Order的理解不太一样,最好用FilterRegistrationBean.setOrder显式指定。如果你注册多个Filter又没排顺序,日志会变得很难看,排查问题时根本不知道谁先谁后。

2.2 Filter能拿到什么,不能拿到什么

Filter拿到的是原始的HttpServletRequest和HttpServletResponse。它可以读请求头、读请求体、改响应状态、写数据,甚至可以包一层Wrapper来修改请求对象。

但它拿不到SpringMVC层面的任何东西:拿不到Controller方法对象、拿不到方法参数注解、拿不到它命中哪个处理器。这也不奇怪,因为Filter执行时DispatcherServlet还没被调用,HandlerMapping还没开始工作,SpringMVC自己都不知道这个URL会交给哪个方法处理,Filter自然也不可能知道。

这个边界很有意思。你可以在Filter里做全局的请求日志,因为你需要的是原始URL、Header、耗时;你可以在Filter里做跨域,因为CORS本身就是Servlet容器层的东西;你可以在Filter里做白名单校验,因为登录态通常和Controller参数无关。但如果你需要根据某个接口的注解来决定要不要放行,Filter就做不到了,因为注解信息还没解析出来。

2.3 什么场景真的该用Filter

实际项目里,Filter最合适的场景有三个:

  • 编码过滤器:CharacterEncodingFilter,这几乎人人都会配。
  • 粗粒度登录校验:比如统一判断请求有没有带token,没带直接401,不必关心接口细节。
  • 全链路日志:想统计“包含SpringMVC框架处理在内”的完整耗时,Filter是唯一能把整个链路包住的点。

反过来,如果需求已经牵扯到“某个注解方法才需要做X”“某个Controller方法才需要做Y”,就要考虑Interceptor甚至AOP了。你操作的信息粒度越靠近方法级,就越应该往链路深处走。

记住这个原则,Filter和Interceptor怎么选就有依据了:Filter面向的是“请求/响应”本身,Interceptor面向的是“哪个Handler被调用了”,AOP面向的是“哪个方法被代理了”。

3. HandlerExecutionChain 的装配:Interceptor 在三个钩子里的不同语义

3.1 getHandler 如何把拦截器挂到请求上

DispatcherServlet的doDispatch方法里,第一步核心逻辑是:

mappedHandler = getHandler(processedRequest);

getHandler会遍历容器里所有的HandlerMapping,逐个调用getHandler方法。SpringMVC里最常见的是RequestMappingHandlerMapping,它维护了一个MappingRegistry,里面存着URL路径与HandlerMethod的映射关系,以及当前项目里注册的全部拦截器。

RequestMappingHandlerMapping最终调用的是AbstractHandlerMapping.getHandler,这个方法做两件事:找Handler、查当前请求路径匹配哪些Interceptor。然后把它们组装成一个HandlerExecutionChain。如果你看源码,会发现HandlerExecutionChain内部有个List ,还带一个Handler对象。

也就是说,Interceptor并不是在Controller方法执行中动态插入的,而是在HandlerMapping阶段就已经全部确定好了。这也是为什么Interceptor也被称为“Handler级别的处理逻辑”。

3.2 preHandle、postHandle、afterCompletion 三个钩子的时机差异

HandlerExecutionChain对拦截器提供了三个钩子:

  • preHandle:在HandlerAdapter执行之前调用。如果preHandle返回false,请求直接终止,后续的Controller方法不会执行,还会计入afterCompletion。
  • postHandle:在HandlerAdapter执行完之后调用,这时Controller已经返回了,但响应还没写出去。注意,如果Controller抛异常,postHandle不会执行。
  • afterCompletion:整个请求处理完成后调用,通常在finally里触发,不管正常与否都会走。

这三个钩子最大的区别在于它们和“方法执行”的状态绑定。preHandle时你还能决定“要不要继续”,postHandle时只能“看看结果不能再干预”,afterCompletion则是“无论成败都必须收尾”。所以清理ThreadLocal、记录最终错误状态这类事,一定要放在afterCompletion里。放在postHandle容易漏,一旦Controller抛异常,这个钩子根本不跑。

我在项目里就用这段逻辑踩过坑:有个全局ThreadLocal存当前用户信息,我随手放在postHandle里清理,结果接口一抛异常,用户信息就残留到下一个请求,后来排查半天才发现是钩子选错了。

3.3 Interceptor 和 Filter 的边界对比

Interceptor比Filter背后的信息量多太多了,因为它已经是在SpringMVC的上下文里。它能看到HandlerMethod,能看到Controller类上和方法上的注解,能在preHandle里做非常精细的权限判断。

举个例子:某个接口标注了@PermissionCheck,你可以在Interceptor里这样:

if (handler instanceof HandlerMethod method) { PermissionCheck annotation = method.getMethodAnnotation(PermissionCheck.class); if (annotation != null && !hasPermission(annotation.value())) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); return false; } }

这种写法对Filter来说是不可能的,因为Filter阶段你根本不知道未来要执行的方法是谁。反过来,Interceptor也做不了Filter的事,比如你想把请求体整个打一份日志,然后原样传下去,这需要包一层HttpServletRequestWrapper,还是在Filter里做更地道。

4. @RequestBody 在 HandlerAdapter 内部的完整解析路径

4.1 HandlerAdapter 到底适配了什么

doDispatch里拿到HandlerExecutionChain之后,后面紧跟着:

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

getHandlerAdapter会遍历容器里的HandlerAdapter,找到一个支持当前Handler的适配器。对@RequestMapping注解的方法来说,这个适配器就是RequestMappingHandlerAdapter。

RequestMappingHandlerAdapter继承自AbstractHandlerMethodAdapter,它的核心方法handleInternal里调用invokeHandlerMethod,这一步是SpringMVC方法调用的中枢。源码里大致流程是这样的:

  1. 创建ServletInvocableHandlerMethod,它里面封装了Controller方法、参数解析器、返回值处理器。
  2. 如果有@ResponseStatus、@SessionAttributes之类的需求,先处理。
  3. 调用invokeAndHandle。
  4. invokeAndHandle内部先调用invokeForRequest,这里完成参数解析和反射调用。
  5. 再用返回值处理器处理返回结果。

所以“@RequestBody到底是什么时候解析的”这个问题,答案很明确:在invokeForRequest里,先解析参数,再调用方法。也就是说,@RequestBody的解析发生在HandlerAdapter内部,比AOP切面通知触发的时机还要早一丢丢。

4.2 参数解析器和返回值处理器的工作方式

invokeForRequest里有个关键方法叫getMethodArgumentValues,它遍历Controller方法的每个参数,找到一个合适的HandlerMethodArgumentResolver来执行resolveArgument。

SpringMVC默认注册了一堆参数解析器,常见映射如下表:

参数写法负责解析的组件
@RequestParamRequestParamMethodArgumentResolver
@PathVariablePathVariableMethodArgumentResolver
@RequestBodyRequestResponseBodyMethodProcessor
@RequestHeaderRequestHeaderMethodArgumentResolver
Model / ModelMapModelMethodProcessor
HttpServletRequestServletRequestMethodArgumentResolver

这里值得注意:@RequestBody对应的组件名字叫RequestResponseBodyMethodProcessor,它同时实现了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler两个接口。也就是说,Controller方法参数上的@RequestBody和返回值上的@ResponseBody,走的是同一套HttpMessageConverter体系,一个负责读,一个负责写。

4.3 RequestResponseBodyMethodProcessor 与 HttpMessageConverter

@RequestBody真正干活的核心是RequestResponseBodyMethodProcessor.readWithMessageConverters。逻辑大体上是:

  1. 拿到HttpServletRequest的InputStream。
  2. 遍历当前配置的HttpMessageConverter列表,找一个canRead方法返回true的。
  3. 调用converter.read(...),把输入流转成Java对象。
  4. 如果转换过程中类的泛型信息有关联,还需要区分GenericHttpMessageConverter和普通HttpMessageConverter。

SpringMVC默认配置了多个Converter,处理JSON靠的是MappingJackson2HttpMessageConverter。它内部持有的就是一个Jackson的ObjectMapper,配置了哪些Jackson模块、哪些注解,最终解析就按那套规则来。

如果你自定义了ObjectMapper,比如定制了LocalDateTime的序列化格式,务必确认它真的被MappingJackson2HttpMessageConverter用上了。Spring Boot里一般直接配置一个ObjectMapper Bean就能生效,但如果你手动new ObjectMapper存在别的地方,SpringMVC压根不知道,会出现“本地测试没问题、接口里格式却不对”的诡异现象。

这个环节还有个很实用的排查技巧:如果遇到@RequestBody解析时报错,优先去看正在使用的Converter是谁,日志里通常会有清楚提示。很多时候根本不是代码写错了,而是数据类型和Converter不匹配。

4.4 流的“一次性读取”限制引发的连锁问题

这个问题做过接口对接的人应该都遇到过:某个Filter或者AOP里先把请求体读了一遍,想打印出来,结果到@RequestBody解析的时候,抛了空指针或者参数全是null。

原因不复杂:InputStream是单向流,读到头就没了。@RequestBody要从请求体里拿数据,如果前面有人把流消费完了,后面自然读不到任何东西。

要解决,一般用ContentCachingRequestWrapper包一层请求对象,它会把请求体缓存到内存里。注意,缓存以后,后面的代码读到的仍是原来的流,不会被提前消费掉。这个场景在Filter里处理最顺手,因为Filter处在请求对象还没进SpringMVC的最前端。

另外还有一个容易忽略的点:如果接口支持文件上传,走的是MultipartResolver,这时请求流已经被包装过了,再叠加自己的Filter时要注意顺序,否则容易把上传内容弄丢。我实际处理过几次这种问题,最后都把自定义Filter的优先级严格放到Multipart处理的后面,或者干脆用OncePerRequestFilter配合ContentCachingRequestWrapper,稳定很多。

5. AOP 代理与 Controller 的真正交汇点:切面到底包在哪一层

5.1 Controller 的代理从何而来

AOP切面能够包住Controller方法,靠的是Controller在Spring容器里被创建时,经过了一个BeanPostProcessor的加工。这个BPP的名字是AbstractAutoProxyCreator,它的子类AnnotationAwareAspectJAutoProxyCreator会扫描项目里的@Aspect切面,判断当前Bean是否命中了切点表达式。

如果命中了,它不会返回原始Controller对象,而是返回一个代理对象。之后的依赖注入、HandlerMapping里登记的bean,拿到的都是这个代理。所以在“AOP生效”的前提下,HandlerExecutionChain里的handler其实是个代理对象,而不是最原始的Controller实例。

Spring Boot项目里,只要引入了spring-boot-starter-aop,不需要额外配置@EnableAspectJAutoProxy,AOP默认就是开启的。这点很多新手不知道,总以为自己漏配了什么东西。

5.2 切面通知与参数解析的先后关系

前面说了,@RequestBody的参数解析发生在HandlerAdapter内部、参数解析完后才reflect调用方法。而AOP是包在方法反射调用外面的。所以这两者在时间轴上的关系是:

  • 先进入Interceptor.preHandle。
  • 再进入HandlerAdapter,解析方法参数,@RequestBody在这个节点读流并反序列化。
  • 然后真正去调用Controller方法,因为这时的Controller是代理对象,JVM会把这次调用引导到AOP链路。
  • 切面的@Around先执行到proceed()之前的部分。
  • @Before执行。
  • 目标Controller方法本体执行。
  • 返回后执行@AfterReturning、@After,最后回到@Around的proceed()之后。

这里有个面试经常考的细节:@RequestBody解析完之后,参数已经是以Java对象的形式传递到代理方法调用链里。所以你在@Around里通过joinPoint.getArgs()拿到的,已经是解析好的对象,而不是原始JSON字符串。

实际项目中,有人想在切面里改Controller入参,比如把某个字段统一填充,其实有两个思路:一个是在切面里对可变对象做属性修改,这样能传入Controller;另一个是干脆在参数解析之前去做,但那样就得走Filter和Wrapper的路子。大部分业务用前者更省事。

5.3 JDK动态代理与CGLIB的区别

Spring AOP的代理方式有两种:

  • JDK动态代理:基于接口,代理对象实现了目标接口,调用时通过InvocationHandler转发。
  • CGLIB代理:基于继承,代理对象是目标类的一个子类,通过生成字节码增强方法。

Spring Boot 2.x之后默认spring.aop.proxy-target-class=true,也就是即使Controller有接口,默认也走CGLIB。这在绝大多数情况下不用管,但有一个非常经典的问题要注意:CGLIB代理是基于继承的,被代理的Controller方法如果有final修饰,方法无法被覆盖,切面也就“看起来不生效”。还有一个类似的情况,是切点表达式写错,比如把@within写成了@target,结果只有接口方法命中,实现类方法全都不生效。

排查AOP是否生效,可以打一行日志:

System.out.println(userController.getClass());

看到class后面带了$$EnhancerBySpringCGLIB$$这样的字符串,说明代理确实创建了。如果打印出来是原始类,那就要去查切点表达式和代理配置了。

5.4 最容易翻车的自调用失效

AOP失效的经典案例:Controller里一个方法调用同一个Bean的另一个方法,也就是说用this.methodB()去调,这时候methodB上的@Around不会触发。因为这个调用发生在目标对象内部,根本没有经过代理对象。

这个坑非常隐蔽,因为代码看起来完全正常。我之前在一个项目里通过AOP做了数据权限过滤,方法A调用方法B,B的切面始终不执行,查了半天才发现是自调用。解决方式很简单:注入代理对象自己,用代理去调用;或者把B挪到另一个Bean里,再注入进来。

记住,Spring AOP是基于代理的,代理只对“外部进入Bean的调用”起作用。凡是this.xxx()这种,都绕开了代理,这是判断AOP是否生效的第一直觉。

6. 把整个链路跑出来:一个带日志的完整 Demo 和输出解读

6.1 工程准备与代码示例

光讲原理总归抽象,最好自己亲手打日志验证。我先给一个最小可运行示例,你复制改改就能跑。

Filter部分:

@Component public class ChainLogFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { System.out.println("1. Filter doFilter start"); filterChain.doFilter(request, response); System.out.println("8. Filter doFilter end"); } }

Interceptor部分:

@Component public class ChainLogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { System.out.println("3. Interceptor preHandle"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { System.out.println("6. Interceptor postHandle"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println("7. Interceptor afterCompletion"); } }

注册拦截器:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ChainLogInterceptor()).addPathPatterns("/**"); } }

AOP切面部分:

@Aspect @Component public class ChainLogAspect { @Around("@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("4. AOP Around before"); Object result = pjp.proceed(); System.out.println("5. AOP Around after"); return result; } }

Controller部分:

@RestController public class DemoController { @PostMapping("/demo") public String demo(@RequestBody Map<String, Object> body) { System.out.println("4-2. Controller method execute, body keys = " + body.keySet()); return "ok"; } }

注意,为了能看清@RequestBody解析和Controller执行之间的顺序,我在AOP日志和Controller日志之间用的编号4-2。实际跑起来后,日志输出顺序就是最好的说明。

6.2 日志输出解读

用curl或者Postman发一个POST请求到/demo,把body设为{"name":"springmvc"},控制台输出的顺序大致是:

1. Filter doFilter start 3. Interceptor preHandle 4. AOP Around before 4-2. Controller method execute, body keys = [name] 5. AOP Around after 6. Interceptor postHandle 7. Interceptor afterCompletion 8. Filter doFilter end

这个日志里面有两个地方特别值得注意:

  • @RequestBody解析发生在第4步之前。你仔细看,Controller方法直接拿到了Map对象,body里已经有name这个key了。这个Map不是控制器里解析出来的,而是在进入代理方法之前,也就是HandlerAdapter内部调用代理对象前的参数解析阶段就完成了。
  • AOP的Around包住了Controller方法本体,但包不住Controller之外的整条链。它跟Interceptor之间是嵌套关系:Interceptor在外面,AOP在里面。

如果把请求处理当成一个洋葱,Filter是洋葱最外一层,Interceptor是下一层,AOP紧贴Controller方法本身。每层只负责自己的那一段职责,谁先谁后由容器、DispatcherServlet和代理机制共同决定。

6.3 记忆链路的口诀兼谈调试技巧

我这里给一个自己平时常用的记忆方式:

Filter最先,收尾最后;Interceptor看Handler;@RequestBody在Adapter里解析;AOP贴在方法上,Around夹住本体。

遇到顺序相关的问题,第一反应应该是去加日志,而不是对着文档猜。SpringMVC这套组件是分层嵌套的,只要理解“哪个组件包住哪个组件”,执行顺序就不会记混。加日志的时候建议统一加前缀,比如[Filter]、[Interceptor]、[AOP]、[Controller],全部打出来之后,链路图一目了然。

还有一个调试技巧:如果怀疑AOP没生效,先打印Controller的Class对象;如果怀疑拦截器没生效,直接在preHandle里打日志,看看到底进没进来。把问题拆开定位,比在整条链里瞎找快得多。

7. 面试高频问题与实战踩坑记录

7.1 面试官到底在考什么

“SpringMVC的执行流程”“AOP原理”“IOC和AOP的原理面试”这些关键词在热搜词里排得很靠前,确实也是面试的高频点。但面试官如果只让你背流程,那没意思。真正有区分度的问题往往是:

  • Filter和Interceptor的区别是什么?
  • @RequestBody的执行顺序和底层原理是什么?
  • 一个请求在Filter、Interceptor、AOP、Controller之间的顺序是什么?
  • AOP代理失效的场景有哪些?

第一个问题的核心就是“层”的差异:Filter在Servlet容器、Interceptor在SpringMVC内部、AOP在方法代理层。第二个问题的核心是HttpMessageConverter:请求体通过InputStream读入,经过Converter反序列化,发生在HandlerAdapter阶段。第三个问题也就是本文的主题。第四个问题,最常见的就是自调用和final方法。

所以准备面试时,不要只背结论,要把“为什么这个组件在这个位置”讲清楚。能讲出“@RequestBody是在HandlerAdapter的参数解析器里经过HttpMessageConverter读取InputStream得到的”,就已经比只会说“用来接收JSON”的人高一个档次。

7.2 几个必须纠正的误解

误解一:拦截器在Filter之前执行。不对。Filter永远在Servlet容器层先执行。只是很多项目Filter没有输出日志,让你误以为拦截器先跑。

误解二:@RequestBody是在进入Controller方法时解析的。严格说,它是在调用代理方法之前解析好的。你可以在参数解析前后打日志验证,解析动作比方法本体早。

误解三:AOP的Around可以包住整个请求。不对。Around只包住目标方法本身。Filter和Interceptor的执行时间都不在Around的内部。

误解四:所有Filter都会对每个请求执行。实际上有OncePerRequestFilter这种约束,同时注册顺序、URL匹配规则都会影响执行。不是所有Filter都一定被触发。

7.3 我在实际项目中踩过的坑

第一个坑是Filter里读请求体导致@RequestBody失效。之前的接口联调时,对方说请求参数为空,排查半天发现是我们自己有个日志Filter把InputStream读光了。后来改成ContentCachingRequestWrapper才正常。

第二个坑是Interceptor里preHandle抛异常。一开始我直接在preHandle里写业务校验,校验逻辑抛了运行时异常,结果响应直接就是500。后来统一改成返回false加response写入错误信息,或者用@ControllerAdvice处理这个异常,代码干净很多。这个选择本身就是链路位置决定的。

第三个坑是多个Aspect的生效顺序。项目里既有接口耗时切面,又有权限校验切面,但没配@Order,导致权限校验在耗时统计里发生,接口被拒的请求也贡献了耗时日志。后面给两个切面都配了@Order,权限切面优先,日志才合理。

第四个坑是Controller自调用导致AOP失效。一个方法内部调另一个带切面的方法,切面不执行,这个之前已经说过,算是AOP问题里最容易被忽视的,没有之一。

这些坑的共同特点都是:原理理解不到位,光看表面代码发现不了问题。而一旦把请求执行顺序这条链路搞清楚,很多问题的答案其实都是可以直接推出来的。说白了,SpringMVC这套执行机制并不玄乎,它就是一套分层的责任划分:容器层、框架层、方法代理层各司其职。你只要知道每一层手里握着什么信息、在什么时候介入,遇到问题自然就能定位到层,再往下排查具体组件就快多了。

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

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

立即咨询