Spring 面经系列 · 第 6 篇:Spring MVC 请求全流程、父子容器与零配置原理

Spring 面经系列 · 第 6 篇:Spring MVC 请求全流程、父子容器与零配置原理

💡 一个 URL 敲下去,Spring MVC 内部其实只干了两件大事:先根据请求地址把「该谁处理」查出来,再把处理结果换成能发回浏览器的字节。听起来很简单,但面试的连环问全埋在这条链路的岔口上------返回带 @ResponseBody 的对象时,视图解析器去哪儿了?控制器是单例,那类里的成员变量凭什么敢直接写?GET 参数乱码为什么 CharacterEncodingFilter 救不了?子容器能看见父容器的 Bean,为什么把 Controller 全交给父容器就变成 404?以及最反直觉的一个:只有接口、没有实现类的 MyBatis Mapper,凭什么能被 @Autowired 注进 Service?

这一篇按 Web 层这条线把 10 个分 P 串起来:主流程 → 控制器的状态 → 乱码 → 拦截器与过滤器 → 父子容器 → 零配置 → Mapper 接口注入。素材仍然是语音转写,错字很多(DispatcherServlet 被写成「departure civil」、CharacterEncodingFilter 被写成「chart encoding field」、Servlet 被写成「civil / Sol」),我按术语本来的样子还原了。凡是能对着官方文档和源码核对的,我都明确标出来;核不了的按规范留在「未核实」里。


1. 一条请求在 DispatcherServlet 里走完的路

视频里给的定位很直接:要用 Spring MVC,通常要在 web.xml 里声明一个 Servlet,这个 Servlet 就是 DispatcherServlet;它既叫「前端控制器」,也叫「核心调度器」,因为它会根据你的请求去框架底层调度不同的 API,最终给你响应。整条主干可以压成一句横串:

text 复制代码
浏览器请求 → Filter 链 → DispatcherServlet.doDispatch → HandlerMapping(返回执行链)
→ HandlerInterceptor.preHandle → HandlerAdapter.handle → Controller 方法
→(返回值 ModelAndView)→ ViewResolver 解析视图名 → View.render 渲染 → HTML 响应

拆开看每个环节各自负责什么:

环节 关键类 它负责什么 视频里的说法 源码核实情况
请求入口 DispatcherServlet 接住所有映射到它的请求,是唯一调度点 前端控制器 / 核心调度器 处理逻辑集中在 doDispatch,属实
找处理器 HandlerMapping 按 URL、方法、参数把请求映射到处理方法 从处理器映射器里按地址找 getHandler() 返回 HandlerExecutionChain
前置拦截 HandlerInterceptor 在方法执行前后插入逻辑,可截断请求 执行链里包含拦截器 applyPreHandle 返回 false 时直接 return
调用方法 HandlerAdapter 真正把你写的处理方法调起来 「由 handler adapter 帮我们调用」 ha.handle(...) 返回 ModelAndView
出入参转换 HttpMessageConverter 按媒体类型读写请求体 / 响应体 AJAX 那条分支靠它 官方文档确认 @RequestBody 用它反序列化
结果封装 ModelAndView 请求域数据 + 视图 「不管返回什么都封装成 ModelAndView」 只对走视图的分支成立,见 Q2
视图定位 ViewResolver 视图名加前缀后缀,定位到真实视图 加 /WEB-INF 前缀、.jsp 后缀 在 render 里通过 resolveViewName 完成
渲染输出 View.render 把模型数据渲染进 JSP/FreeMarker/Thymeleaf 模板 「解析视图上的动态数据,响应静态 HTML」 view.render(mv.getModelInternal(), request, response)
Q1:为什么 HandlerMapping 返回的是「执行链」而不是直接返回方法?

因为一个请求要经过的不只是你的方法,还有一串拦截器。HandlerExecutionChain 里装的是「目标 handler + 一组拦截器」,匹配到的拦截器按配置顺序全部塞进这条链,最后才是处理方法。视频里强调的这个设计,在源码里就是 getHandler() 的返回类型------如果只返回方法本身,拦截器就得由调用方自己再查一遍,顺序和短路逻辑(某个 preHandle 返回 false 就整条链路终止)会散得到处都是。

顺着这条链,还有一个细节值得记住:preHandle 是在 Controller 方法之前 跑的,postHandle 是在方法返回之后、视图渲染之前,afterCompletion 则在结果处理阶段触发。视频里只说了「依次进、依次出」,实际这三个时机是有明确差别的。

再看返回值这一句:视频里说「不管你返回 String 还是对象,底层最终都会封装成 ModelAndView」。这句话在走视图 的分支上没问题,但在带 @ResponseBody 的分支上不准确------我对着源码看,ha.handle(...) 完全可能返回 null,而后面做结果处理时是有条件的:只有 mv != null 且视图没被清除,才会进入渲染。也就是说,@ResponseBody 场景下不是「包了一个 ModelAndView 再去解析视图」,而是压根没产生 ModelAndView。

Q2:AJAX / JSON 那条分支真的跳过视图解析了吗?

是的,跳过的不是某一步,而是后半整段。视频作者在讲完主流程后专门补了一句:如果请求的是 JSON 数据,后续的视图解析器、解析视图这些都不会有了,基本直接把数据响应回去。这在框架文档里能对上:@ResponseBody 的返回值是通过 HttpMessageConverter 序列化到响应体的,@RequestBody 则相反,是把请求体读取并反序列化成对象。

text 复制代码
【入】请求体 JSON → HttpInputMessage → HttpMessageConverter.canRead(看 Content-Type)→ read → Java 参数
【出】Java 返回值 → canWrite → HttpMessageConverter.write → HttpOutputMessage → 响应体 JSON(视图链不再进入)

视频里给的配置说法是这样的:Spring MVC 默认支持的 JSON 实现是 Jackson,你只要把 Jackson 的依赖加上,再在 XML 里配一行 annotation-driven,底层就会替你注册一个实现了 HttpMessageConverter 的 Jackson 转换器;如果你换成 Fastjson,就得手动 把对应的转换器配进去,框架不会替你猜。这里有一个坑要提醒:具体转换器类名跟版本绑定,早年是 MappingJackson2HttpMessageConverter(Jackson 2 时代),而 Spring 官方参考文档现在的示例里出现的是 JacksonJsonHttpMessageConverter(Jackson 3 时代)。面试时把「read / write / canRead / canWrite 这四个动作 + 按媒体类型挑转换器」讲清楚,比背一个类名稳得多。


2. 控制器是单例:问题只出在「状态」上

视频里说这道题也可能换个问法:「Spring MVC 的控制器是不是线程安全的?」------两种问法答案同一套,因为它本质上就是一个 Bean,你没设置作用域时默认就是单例。所以这题不需要重新组织语言,只需要把「单例 Bean 的线程安全」那套答案搬到控制器上。

关键概念还是「有状态 / 无状态」:在类里声明了成员变量,就是有状态的单例对象;并发下多个线程同时读写这个字段,就有线安全问题。解决层次视频给了三条,我按代价排一下:

方案 具体做法 代价 适用场景 面试评价
消灭状态 变量只声明在方法里(局部变量、方法参数) 几乎为零 绝大多数控制器 首选,也是控制器的常态
线程隔离 用 ThreadLocal 把值绑到当前线程 需要记得清理 必须跨方法传递请求内状态 正解,但要补一句「用完 remove」
加锁 synchronized / 显式锁保护字段 并行变串行,压吞吐 确实要共享且必须互斥的状态 视频原话:这是「下下策」

一句话理解:控制器本身不负责线程安全,负责线程安全的是你把状态放在哪一层。

这里视频还补了一个很实用的点:在控制器里你可以放心地直接注入 HttpServletRequest、HttpSession 这类 Servlet 作用域对象,不会有问题,因为框架底层早就把「当前请求」放进了 ThreadLocal,注进来的东西本身就是按线程取值的。这一条我在 spring-web 源码里能对上:RequestContextHolder 内部就是两个 ThreadLocal(一个是普通的,一个是可继承的),当前请求属性绑在上面;至于「注入时以可解析依赖的形式暴露」这一步,笔者没有逐行核到注册位置,属于合理推断。


3. 中文乱码:三个位置三把钥匙

这题视频给的开场答案很短:只针对 Spring MVC 来问的话,通常就是没配那个「编码过滤器」(转写里的 chart encoding field 就是 CharacterEncodingFilter),给它设成 UTF-8 就够了。但作者紧接着把话说开了:乱码的成因很多,每一道关卡都要设成 UTF-8 才尽量不出事。这句话我完全同意------乱码不是一个 bug,是一串编码点里某一个对不上。

把「编码发生在哪」想清楚,解法自然就分开了:

乱码位置 谁在做解码/编码 官方默认值 首选解法 兜底手段 最容易踩的坑
GET 的 query 参数 Web 容器解析 URI 时 URIEncoding 默认 UTF-8(Tomcat 9 文档) 前端正确 encode,容器编码统一 useBodyEncodingForURI 以为加了过滤器就行------过滤器跑在它后面
POST 表单体 request.getParameter() 请求没声明字符集时按 ISO-8859-1 CharacterEncodingFilter,UTF-8 方法内显式转码 过滤器排在解析参数之后,设了也没用
响应体 消息转换器 / 视图模板 StringHttpMessageConverter 默认字符集仍是 ISO-8859-1(源码核实) 显式声明 produces 带 charset 开 forceEncoding 连带设响应编码 只设了响应头没设字符集
工程文件本身 编译器 / IDE 跟随 IDE 设置 工具编码统一 UTF-8 另存为时确认目标编码 设了 IDE 但单个文件仍是 GBK
数据库读出来 JDBC / 表字符集 跟随库表与连接串 库、表、连接串三处对齐 --- 只改连接串不改表字符集

这里有两个「视频没说但很要命」的点,是我核对文档后补的。第一,CharacterEncodingFilter 对 GET 的查询串是无效 的:查询串的解码发生在容器解析 URI 阶段,Tomcat 文档里明确说,请求字符集未知时默认按 ISO-8859-1 处理,且 URIEncoding 不影响路径之外的这套默认;换句话说,GET 乱码要往前端 encode 和连接器编码去找,不要指望过滤器。第二,作者提到「设置一下 Tomcat 上的 connector URIEncoding」,这条建议有明显的年代印记------Tomcat 官方文档里 URIEncoding 的默认值已经是 UTF-8,它主要影响的是老版本(早于 Tomcat 8 的年代默认是 ISO-8859-1);面试里可以说这句,但补一句「新版本默认已是 UTF-8」更稳。

至于过滤器本身,源码里的行为是:它是 OncePerRequestFilter 的子类,有 forceRequestEncoding / forceResponseEncoding 两个开关;只设编码时它只在请求没声明字符集的情况下生效,开了 forceEncoding 才会无条件覆盖,并且把响应也纳入默认编码(但这个响应编码通常会被视图里更完整的 Content-Type 覆盖掉)。


4. 拦截器 vs 过滤器:谁属于谁,决定谁先跑

视频作者给这题定调是「非常简单」,只要你搞懂两者的归属,区别和顺序是能自己推出来的。他总结了三点突出差别,我补成一张对照表:

对比维度 HandlerInterceptor(Spring MVC) Servlet Filter(Servlet 规范) 面试怎么答
依赖容器 依赖 Spring MVC,不依赖 Servlet 容器 依赖 Servlet 容器(Tomcat) 归属决定生命周期长短
作用范围 只对 DispatcherServlet 映射到的请求生效(视频里的「action 请求」) 对所有进入容器的请求生效 静态资源、404 页只有过滤器能拦
能否用 DI 它本身是容器里的 Bean,可以注入 原生 Filter 不能访问容器中的 Bean Boot 里用容器注册的 Filter 可以,作者自己补了这句
三个时机 preHandle / postHandle / afterCompletion 只有 doFilter 一个入口,靠链式调用前后夹 拦截器能拿到 ModelAndView,Filter 拿不到
顺序 在 DispatcherServlet 之内 在 DispatcherServlet 之前 谁在外面谁先跑

顺序这条最适合画成流水线,进和出的方向是对称的:

text 复制代码
【进】请求 → Tomcat → Filter#1 → Filter#2 → Servlet(DispatcherServlet)→ Interceptor#1.preHandle → Interceptor#2.preHandle → Controller
【出】Controller → Interceptor#2.postHandle → Interceptor#1.postHandle → 视图渲染 → Interceptor#1.afterCompletion → Interceptor#2.afterCompletion → Filter#2 → Filter#1 → 响应

注意两个容易讲错的地方。第一,拦截器有多个时按配置的顺序执行,但 postHandle / afterCompletion 是反序 退出的,视频只说「依次的出去」,方向要说清。第二,afterCompletion 不只在正常路径上跑------源码里如果分发过程抛异常,会走 triggerAfterCompletion 补触发,所以它是「无论如何都要收尾」的地方(清理 ThreadLocal、埋点计时放这里)。


5. 父子容器:为什么要分,能不能合成一个

这组分 P 在视频里是一组连环问(阿里一面)。作者先把前提钉住了:Spring 整合 Spring MVC 并不是一定 要父子容器,一个容器照样能跑,「你可以去参考 Spring Boot,它就是一个容器就搞定了」。那为什么传统 spring + springmvc 的搭法就要分?他给了三个原因,外加一个他觉得牵强的第四个:

text 复制代码
父容器(root WebApplicationContext,由 ContextLoaderListener 创建)
  ├─ 管:Service / DAO / 事务 / 数据源 / 第三方 Bean
  │     ↑ 子容器可见(查找沿 parent 向上)
  └─ 子容器(DispatcherServlet 的 WebApplicationContext)
        ├─ 管:Controller / 拦截器 / 视图解析器 / 消息转换器
        └─ 对父容器:不可见 ------ 父容器根本不知道子容器里有什么
  • 划分框架边界,有点单一职责的味道:Service、DAO 交给 Spring 管,Controller 和 Web 相关的交给 Spring MVC 管,职责清晰。
  • 规范整体架构:因为子能看到父、父看不到子,而现实中只有「Controller 依赖 Service」,从来没有人「Service 里注入 Controller」。可达性方向和依赖方向刚好一致,等于用容器结构强制了分层只能向下依赖。
  • 方便子容器切换 :这是作者认为最实在的一条。早年确实有 Spring 配 Struts 2 组 SSH 的用法,Web 层不只有 Spring MVC 一个选项;把 Web 层单独抽成子容器后,要把 Spring MVC 换成 Struts 2,只需要换掉子容器的配置,父容器配置一行不用动。而 Spring Boot 从来没跟 Struts 2 搭配过,所以它压根不需要这层结构。
  • 顺带一提的「节省重复 Bean 创建」(多个子容器共用一个 Bean 就放父容器,避免同名冲突):作者自己都评价「这个原因有点牵强」,因为一个父容器挂两个子容器的场景在 Spring + Spring MVC 里很少见。这个坦率的评价我觉得比背四条原因更值得学。
Q3:把 Spring MVC 的 Bean 全交给父容器管理,行不行?

视频给的答案是肯定不行 ,而且给的理由非常具体:所有 Controller 都会被解析成 HandlerMethod 存进处理器映射器,请求进来时按 URI 匹配,匹配不上就是 404;而解析这一步拿的是当前容器的 BeanDefinition 名字 ,不是 getBean------只有 getBean 那条路才会往上找父容器。所以你把 Controller 全放父容器,子容器压根不去那儿捞,注册不到处理方法,请求自然 404。作者现场翻源码时看到的方法是 initHandlerMethods,还特意提醒「具体是不是这个名字我有点忘了」。

我把这段逻辑核实了一遍,方向和视频说的完全一致,而且比视频多一个开关:AbstractHandlerMethodMapping 里有个 getCandidateBeanNames(),实现是判断 detectHandlerMethodsInAncestorContexts 这个布尔位,它为 false(源码里的默认值就是 false)时走 obtainApplicationContext().getBeanNamesForType(Object.class),为 true 时才换成 BeanFactoryUtils.beanNamesForTypeIncludingAncestors(...)。这个位是 RequestMappingHandlerMapping 暴露的公开属性,打开后注释写得很清楚:to detect handler beans in ancestor contexts (typically the Spring root WebApplicationContext)。也就是说:默认不行,但配一个开关就能让子容器识别父容器里的 Controller。 这一层视频没讲到,是可以拿来加分的地方。

Q4:反过来,把 Spring 的 Bean 全交给子容器管理,行不行?

视频的答案是可以:Controller 在子容器里能被解析,请求进得来;Controller 里注入 Service 也在同一个容器,注得上,所以功能上没问题。但一般不这么干,因为它把前面那三条优点(职责边界、架构约束、容器可切换)全丢了。

更要紧的是他点出的一个副作用,正好补进「AOP / 事务失效原因」那份清单里:如果你把事务或 AOP 的配置放在父容器,而 Bean 都在子容器,那么父容器做代理时扫的是自己容器里的 Bean,容器里没几个 Bean,动态代理根本不会被创建,增强自然不生效;这类配置必须跟着 Bean 待在子容器里。

配置组合 映射解析 Controller 注入 Service 事务/AOP 结论
Service 在父、Controller 在子(常规) 正常 正常(子可见父) 配在父,作用在父容器 Bean 上,正常 推荐
Controller 全放父容器 拿不到 → 404 --- --- 默认不行,需开祖先扫描开关
全部塞进子容器 正常 正常(同容器) 必须一并搬到子容器,否则不生效 能跑,但丢了分层约束
AOP/事务配父、Bean 在子 正常 正常 代理对象创建不出来,增强失效 经典踩坑组合

6. 零配置 Spring MVC:SPI 把 web.xml 换成了 Java Config

视频里先解释了什么叫「零配置」:一个 Spring MVC 的 Web 应用,web.xml 没了,spring.xml、springmvc.xml 也没了,取而代之用 Java 代码来配,也就是 Java Config。作者特别指出这条能力早在 Spring 3 之后的版本就有了,只是大家一直没用,直到 Spring Boot 流行才熟悉起来------很多人从 XML 直接跳到 Boot,不适应的缺口就在这儿。(Boot 的自动配置属于另一条线,这里只交代关系,不展开。)

省略之前要清楚每个文件原来在干什么,替换才有的放矢:

原来在哪配 原作用 零配置下由谁承担 关键类
web.xml 的 ContextLoaderListener 创建父容器 SPI 入口实例化你的初始化类后注册监听器 AbstractContextLoaderInitializer
web.xml 的 DispatcherServlet 创建子容器、映射请求 同上,改用 API 动态注册 Servlet AbstractDispatcherServletInitializer
spring.xml 父容器扫哪些包、排除 Controller 父容器配置类 getRootConfigClasses() 返回的类
springmvc.xml 子容器只包含 Controller、配拦截器/文件上传 子容器配置类 getServletConfigClasses() 返回的类

原理的关键是一个字:SPI。视频里的描述是,Servlet 3.0 之后容器提供了一种 SPI 扩展方式,你在 META-INF/services 下放一个以接口全名命名的文件,文件里写实现类,容器启动时就会自动找到它并调用其 onStartup 方法------作者还类比了数据库驱动,「我们以前用的驱动方式一直也是 SPI」。这条链我核实过,在 spring-web 的源码与资源目录里躺得干干净净:

text 复制代码
Servlet 容器启动 → 扫描 META-INF/services/jakarta.servlet.ServletContainerInitializer
→ 加载 org.springframework.web.SpringServletContainerInitializer(类上标 @HandlesTypes(WebApplicationInitializer.class))
→ 容器把所有实现了该接口的【具体类】传进来 → 逐个实例化并调 onStartup(servletContext)
→ 你的子类 extends AbstractAnnotationConfigDispatcherServletInitializer
→ 父容器配置类 getRootConfigClasses() / 子容器配置类 getServletConfigClasses() → 两个容器建好

这里作者讲了一个很容易被忽略的机制:SPI 文件里维护的是 Spring 自带的那个入口类,而 AbstractDispatcherServletInitializer 这些是抽象类 ,抽象类不能被实例化,所以它当时不会起作用------只有你在应用里写一个继承它的子类,容器扫到具体类并实例化,onStartup 里注册监听器和 DispatcherServlet 的代码才真正跑起来。这句话我在源码里对上了:SpringServletContainerInitializer 的 onStartup 接收的是容器传进来的一组类型,它会过滤出 WebApplicationInitializer 的实现类逐个实例化调用。也正因如此,配置类不再靠 XML 文件,而是靠 @Configuration 类:父容器配置类里设置扫描包并排除 Controller 注解,子容器配置类里只包含 Controller 注解------这跟以前在 XML 里做 include/exclude 是一回事,作者现场也是这么对照着讲的。

面试时这道题的关键点点两个就够:原理是 SPI(容器启动时按 META-INF/services 找到入口类,再按 @HandlesTypes 把接口实现类交给它),落地是自己写一个继承 AbstractAnnotationConfigDispatcherServletInitializer 的子类,用它两个抽象方法分别交出父子容器的配置类。


7. 只有接口没有实现类的 Mapper,是怎么被容器管起来的

这是本篇的技术高点,也是视频里说「没看过源码听起来会坐飞机」的那道题。问题本身很朴素:MyBatis 的 Mapper 接口没有实现类,可你在 Service 里 @Autowired 一个接口就能用。答案分两层------接口能不能进容器 和进来的东西怎么实例化。

先把两条底层事实摆正。第一条,MyBatis 侧:Mapper 接口的核心实现是 JDK 动态代理,这点视频说得很明确,我在 MyBatis 源码里也核实了,MapperProxyFactory 创建实例时调的就是 Proxy.newProxyInstance(...)。第二条,Spring 侧:默认的组件扫描不会 为接口注册 BeanDefinition------扫描框架判断候选类的那个方法(转写里被识别成「Is candidate component」,真名是 isCandidateComponent)要求元数据「独立且具体」,抽象类只有在带特定注解时才放行,接口直接出局。

所以整合方案必须回答三个问题,串起来是这条流水线:

text 复制代码
MapperScannerConfigurer(一个 BeanDefinitionRegistryPostProcessor,能动态注册 BeanDefinition)
→ ClassPathMapperScanner(继承 Spring 内置扫描器,复用它的 class 文件解析能力)
→ 重写 isCandidateComponent:把「接口」放进来
→ 改写 BeanDefinition 的 beanClass:从「接口」换成 MapperFactoryBean   ← 视频称「偷天换日」
→ MapperFactoryBean.getObject():在这里创建 JDK 动态代理,代理对象进容器
→ getObjectType():告诉容器「这个 BeanDefinition 产出的类型是 UserMapper」

视频给的四步和这条流水线一一对应,我把它整理成表,并把官方叫法贴上:

关键点 要解决的问题 视频里的说法 官方文档/源码里的名字 是否核实
借扩展点注册 内置扫描不认接口,得自己注册 BeanDefinition 实现某个扩展接口,它会给你一个注册器 MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor 文档原话,已核实
复用扫描器 100 个接口不可能手动注册 100 次 继承 Spring 内置扫描器,重写排除接口的方法 ClassPathMapperScanner extends ClassPathBeanDefinitionScanner 已核实
放行接口 让接口成为候选组件 重写「排除接口」那个方法 覆写后条件为「是接口、独立、且不是注解类型」 已核实
偷天换日 接口 BeanDefinition 无法实例化 改掉 beanClass,改成 FactoryBean definition.setBeanClass(mapperFactoryBeanClass),默认 MapperFactoryBean 已核实
造代理 真正产出可调用的对象 在 getObject() 里自由控制实例化过程,创建 JDK 动态代理 MapperFactoryBean<T> implements FactoryBean<T> 已核实
Q5:getObjectType 为什么重要,它和「提前判型」是什么关系?

因为 FactoryBean 有两层身份:容器里真正存在的是 FactoryBean 这个 Bean,而你注入的是它 getObject() 生产出来的产品。问题就来了------当别人 @Autowired UserMapper 时,容器手里只有一个 beanClass = MapperFactoryBean 的 BeanDefinition,光看 BeanDefinition 根本不知道它会产出一个 UserMapper。getObjectType() 就是回答这个问题的:容器在不创建产品对象 的前提下询问 FactoryBean「你会产出什么类型」,用这个答案完成按类型匹配。这个机制在源码里能看到确证:AbstractBeanFactory 在做类型判断时,如果目标实例是 FactoryBean,就走 getTypeForFactoryBean(factoryBean),而 FactoryBeanRegistrySupport 里这个方法的实现就是老老实实调 factoryBean.getObjectType()(还顺带处理了它抛异常的情况,按契约返回 null)。

这里有一个和前面父子容器呼应的细节,很值得在面试里连起来讲:第 5 章里处理器映射器扫描候选用的是 getBeanNamesForType(Object.class)------任何按类型的查找都会触发判型,也就可能触发 FactoryBean 的类型判断 。MyBatis 的做法更进一步:它在改写 BeanDefinition 时,除了把 beanClass 换成 MapperFactoryBean,还额外往 BeanDefinition 上写了一个「FactoryBean 产物类型」的属性(源码里那个常量是 FACTORY_BEAN_OBJECT_TYPE),并把 mapperInterface 设进属性值。这一步的目的是让容器在更早、更便宜的时刻(还没实例化 FactoryBean 之前)就能知道类型,不至于为了判型被迫提前把 Bean 创建出来。为什么非做不可,涉及 Spring 内部判型与 BeanPostProcessor 时序,属于我的推断,视频没讲,我也没逐行核到,标在未核实里。

顺带把视频开头那句「Spring 内置扫描会把你排除掉」和这道题收个尾:Spring 不是不能管接口,而是它的默认注册路径不为接口买单;一旦有第三方通过合法扩展点把 BeanDefinition 注册进来、并把 beanClass 换成一个能产出对象的 FactoryBean,接口就和一个普通 Bean 没有任何区别了。 这也是「Spring 的扩展点」这类题最标准的一个答案范式。


最后总结

这组分 P 放在一起看,其实是同一条判断链:Web 层的每一个「为什么」,都取决于「这件事归谁做」。 DispatcherServlet 只做调度,映射归 HandlerMapping、调用归 HandlerAdapter、序列化归 HttpMessageConverter、渲染归 ViewResolver 和 View------搞混归属,就会答错 @ResponseBody 时视图解析器去哪了;乱码搞混归属,就会把过滤器当万能药,而 GET 查询串的解码根本发生在容器层;父子容器搞混归属,就解释不了「子能看到父、父看不到子」为什么会变成 404 和事务失效两个完全不同的症状;MyBatis 整合搞混归属,就会以为 Spring 天生能扫接口。

我个人更倾向于这样排这套连环问的答法:先用一句话给出主干流程(不超过 45 秒),然后主动挑一个有源码细节的点往下钻------detectHandlerMethodsInAncestorContexts 这个默认关掉的开关、getObjectType() 的提前判型、SPI 里「抽象类不会被实例化所以必须自己写子类」这三处,任何一处讲透,都比把七步流程背完更有说服力。视频作者反复强调「记住这几个关键点就够了」,我觉得这个取舍是对的:面试官要的是你能不能定位机制,不是你能不能默写流程。

本文覆盖分 P

分 P 视频原始标题 本文对应小节
P43 Spring如何管理Mybaits的Mapper接口的 7
P44 说说你是如何解决 get 和 post 乱码问题? 3
P45 Spring MVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 2
P46 spring MVC的工作流程 1
P47 SpringMVC中处理AJAX请求原理? 1(Q2 与消息转换器分支)
P48 Spring和SpringMVC为什么需要父子容器? 5
P49 是否可以把SpringMVC所有Bean都交给Spring容器来管理? 5(Q3)
P50 是否可以把Spring的Bean都交给Spring-mvc来管理 5(Q4)
P51 如何实现零配置的SpringMVC?原理是什么? 6
P52 SpringMVC的拦截器和过滤器的区别和执行顺序 4

事实说明

  • 视频里给的事实 (转写明确讲到,仅代表视频表述):DispatcherServlet 叫前端控制器/核心调度器;HandlerMapping 返回含拦截器的执行链,由 HandlerAdapter 调用处理方法;返回值最终封装成 ModelAndView,视图解析器加前缀后缀定位视图再渲染成 HTML;AJAX/JSON 走 HttpMessageConverter 的 read/write,后续视图解析不再有;Spring MVC 默认支持 Jackson,XML 加 annotation-driven 即自动注册,Fastjson 要手动配转换器;控制器默认单例,有状态成员变量才有并发问题,解法是方法级变量、ThreadLocal、同步锁(锁是下下策);Servlet 作用域对象可放心注入,框架已放进 ThreadLocal;CharacterEncodingFilter 设 UTF-8 是首要解法,其次 IDE 文件编码、Tomcat connector URIEncoding、数据库编码逐关对齐;拦截器不依赖 Servlet 容器、只作用于 action 请求、可访问容器 Bean,过滤器依赖 Servlet 容器、作用于所有请求;顺序是过滤器 → Servlet → 拦截器 → Controller,再反序退出;父子容器三条原因(划边界、规范架构、便于切换 Web 框架),外加作者自认牵强的「省重复 Bean 创建」;Controller 全放父容器会 404,因为解析处理方法拿的是当前容器的 BeanDefinition 名字而非 getBean;Bean 全放子容器可行,但 AOP/事务配置必须一起搬;零配置的关键字是 SPI,抽象类不能实例化所以必须自写子类,用 getRootConfigClasses / getServletConfigClasses 交出父子配置类;MyBatis 整合四个关键点(借 BeanDefinitionRegistryPostProcessor 注册、继承内置扫描器、重写排除接口的方法、偷天换日改 beanClass 为 FactoryBean 并在 getObject() 里造 JDK 动态代理)。
  • 官方文档 / 公开事实 (本次逐条核实):DispatcherServlet.doDispatch 内 getHandler → applyPreHandle → getHandlerAdapter → ha.handle → applyPostHandle → processDispatchResult 的调用顺序,且只有 mv != null 才 render(Spring Framework 源码);AbstractHandlerMethodMapping.getCandidateBeanNames() 在 detectHandlerMethodsInAncestorContexts(默认 false)时只调当前容器 getBeanNamesForType(Object.class),该开关的注释明确写着可识别祖先容器(通常是 Spring root WebApplicationContext)里的 handler Bean;@RequestBody 经 HttpMessageConverter 反序列化、@ResponseBody 经其序列化到响应体(Spring 官方参考文档,其中当前示例用的 JSON 转换器名为 JacksonJsonHttpMessageConverter);CharacterEncodingFilter 继承 OncePerRequestFilter,具备 forceRequestEncoding / forceResponseEncoding,forceEncoding 时连带设为响应默认编码(Spring 源码与类注释);StringHttpMessageConverter.DEFAULT_CHARSET 在源码当前主干上仍是 ISO_8859_1;RequestContextHolder 使用 NamedThreadLocal / NamedInheritableThreadLocal 持有请求属性;spring-web 中存在 META-INF/services/jakarta.servlet.ServletContainerInitializer,内容为 org.springframework.web.SpringServletContainerInitializer,该类标注 @HandlesTypes(WebApplicationInitializer.class) 并负责实例化实现类后调用其 onStartup;AbstractDispatcherServletInitializer.registerDispatcherServlet 通过 createServletApplicationContext() 建子容器再 servletContext.addServlet(...);Tomcat 官方文档中 HTTP Connector 的 URIEncoding 默认值为 UTF-8,useBodyEncodingForURI 默认 false,且请求字符集未知时默认按 ISO-8859-1;MyBatis-Spring 文档原话「MapperScannerConfigurer is a BeanDefinitionRegistryPostProcessor」,以及 MapperFactoryBean 注册 mapper、@MapperScan / <mybatis:scan> 扫描、mapper 线程安全可直接注入;MyBatis 源码 MapperProxyFactory 使用 Proxy.newProxyInstance;MyBatis-Spring 源码中 ClassPathMapperScanner 继承 ClassPathBeanDefinitionScanner、覆写后的候选条件为「是接口 + 独立 + 非注解类型」、definition.setBeanClass(mapperFactoryBeanClass)、写入 FACTORY_BEAN_OBJECT_TYPE 属性、未显式指定工厂时启用按类型自动装配;MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> 且实现 getObjectType();AbstractBeanFactory 判型时对 FactoryBean 走 getTypeForFactoryBean,其实现调用 factoryBean.getObjectType();Spring 默认扫描的候选条件为「独立且具体(或带特定注解的抽象类)」,接口被排除。
  • 笔者判断 / 未核实 :「不管返回什么都封装成 ModelAndView」我判断只在走视图的分支成立,@ResponseBody 场景源码里 mv 为 null(属对视频表述的修正,不是官方结论);「getBeanNamesForType 触发判型会让 MyBatis 额外写 FACTORY_BEAN_OBJECT_TYPE 以避免 FactoryBean 被提前实例化」是结合源码注释「for spring-native」做的推断,具体时序未逐行核实;「请求/会话对象以可解析依赖形式注入」这一步未核到注册位置,只核实了 ThreadLocal 那一半;MyBatis-Spring 文档版本号显示为 4.1.1-SNAPSHOT(开发中快照),文中未据其推断任何 Spring 版本兼容矩阵;HandlerAdapter 注解式实现的确切类名、DispatcherServlet.properties 里的默认策略清单,本次抓取超时未核对;各分 P 时长与顺序取自字幕文件元信息,视频画面里的示意图内容无法从转写还原,本文所有流程图均按文字描述重绘,不代表原图。

参考资料 & 致谢

1 Spring面经-哔哩哔哩(素材来源,共 77 个分 P,本文取自其中 P43~P52;文中一律称「视频作者」)

2 Spring Framework 官方项目页

3 Spring MVC 参考文档 - DispatcherServlet / @RequestBody / @ResponseBody

4 Spring Framework 源码仓库(DispatcherServlet、AbstractHandlerMethodMapping、CharacterEncodingFilter、SpringServletContainerInitializer)

5 MyBatis 官网

6 MyBatis-Spring 参考文档 - Injecting Mappers / Scanning for mappers

7 MyBatis 3 源码仓库(MapperProxyFactory)

8 Apache Tomcat 官网

9 Tomcat 9 - HTTP Connector 配置(URIEncoding / useBodyEncodingForURI)

10 Jackson 项目主页

11 Fastjson2 项目主页

12 Apache Struts 官网

13 Spring Boot 官方项目页(本文仅用于对照说明「单容器」,展开留给系列另一篇)

14 Oracle Java SE 文档(动态代理)

感谢视频作者把这组 Web 层连环问拆得这么细,尤其是「处理器映射扫的是 BeanDefinition 名字而不是 getBean」这一条,是全套题里最容易被讲错的地方;也感谢我自己在核对源码时发现的两个开关(detectHandlerMethodsInAncestorContexts 和 Tomcat URIEncoding 的默认值),它们正好说明口述内容和当前版本行为之间需要一步对照验证。

相关推荐
code2cat1 小时前
Java高级篇之JDBC:连接数据库,完成查询、更新与事务
java·开发语言·数据库
bksczm1 小时前
Redis数据结构之string
java·redis·mybatis
cc_瀚海知行1 小时前
资料检索 1|直接放入全文、按需读文件和先检索,有什么区别?
java·人工智能·ai
BioRunYiXue1 小时前
使用docker分析高通量测序数据
java·数据库·人工智能·spring cloud·docker·tomcat·html
小尹哥-程序员1 小时前
第9集:Spring AI Advisor链:像Spring MVC拦截器一样优雅
人工智能·spring·mvc
Shaoshing2 小时前
缓存一致问题及常见解决方案
java·缓存
聚美智数2 小时前
文档转换 API‑文档转 PDF‑PDF 转文档‑异步文档处理‑文件格式迁移‑转换结果回调推送 API 接口介绍
android·java·pdf
从零数据分析2 小时前
零基础学SQL 23:和上月比总对不上?LAG/LEAD 一步取出上一次(附 4 个模板)
java·数据库·sql·mysql·oracle·sqlite·database
yuzhiboyouye2 小时前
com.fasterxml.jackson.annotation 里最常用高频的注解,可以按“控制字段、控制格式、控制结构、控制流程”来记
java·开发语言