资讯动态

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

发布时间:2026/10/23 2:47:28 来源:尧图企业网站定制
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最终给你响应。整条主干可以压成一句横串浏览器请求 → 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。Q2AJAX / JSON 那条分支真的跳过视图解析了吗是的跳过的不是某一步而是后半整段。视频作者在讲完主流程后专门补了一句如果请求的是 JSON 数据后续的视图解析器、解析视图这些都不会有了基本直接把数据响应回去。这在框架文档里能对上ResponseBody的返回值是通过HttpMessageConverter序列化到响应体的RequestBody则相反是把请求体读取并反序列化成对象。【入】请求体 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就得手动把对应的转换器配进去框架不会替你猜。这里有一个坑要提醒具体转换器类名跟版本绑定早年是MappingJackson2HttpMessageConverterJackson 2 时代而 Spring 官方参考文档现在的示例里出现的是JacksonJsonHttpMessageConverterJackson 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-8Tomcat 9 文档前端正确 encode容器编码统一useBodyEncodingForURI以为加了过滤器就行——过滤器跑在它后面POST 表单体request.getParameter()请求没声明字符集时按 ISO-8859-1CharacterEncodingFilterUTF-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 过滤器谁属于谁决定谁先跑视频作者给这题定调是「非常简单」只要你搞懂两者的归属区别和顺序是能自己推出来的。他总结了三点突出差别我补成一张对照表对比维度HandlerInterceptorSpring MVCServlet FilterServlet 规范面试怎么答依赖容器依赖 Spring MVC不依赖 Servlet 容器依赖 Servlet 容器Tomcat归属决定生命周期长短作用范围只对 DispatcherServlet 映射到的请求生效视频里的「action 请求」对所有进入容器的请求生效静态资源、404 页只有过滤器能拦能否用 DI它本身是容器里的 Bean可以注入原生 Filter 不能访问容器中的 BeanBoot 里用容器注册的 Filter 可以作者自己补了这句三个时机preHandle/postHandle/afterCompletion只有doFilter一个入口靠链式调用前后夹拦截器能拿到 ModelAndViewFilter 拿不到顺序在 DispatcherServlet 之内在 DispatcherServlet 之前谁在外面谁先跑顺序这条最适合画成流水线进和出的方向是对称的【进】请求 → Tomcat → Filter#1 → Filter#2 → ServletDispatcherServlet→ 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的搭法就要分他给了三个原因外加一个他觉得牵强的第四个父容器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 MVCSPI 把 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 入口实例化你的初始化类后注册监听器AbstractContextLoaderInitializerweb.xml的DispatcherServlet创建子容器、映射请求同上改用 API 动态注册 ServletAbstractDispatcherServletInitializerspring.xml父容器扫哪些包、排除 Controller父容器配置类getRootConfigClasses()返回的类springmvc.xml子容器只包含 Controller、配拦截器/文件上传子容器配置类getServletConfigClasses()返回的类原理的关键是一个字SPI。视频里的描述是Servlet 3.0 之后容器提供了一种 SPI 扩展方式你在META-INF/services下放一个以接口全名命名的文件文件里写实现类容器启动时就会自动找到它并调用其onStartup方法——作者还类比了数据库驱动「我们以前用的驱动方式一直也是 SPI」。这条链我核实过在spring-web的源码与资源目录里躺得干干净净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要求元数据「独立且具体」抽象类只有在带特定注解时才放行接口直接出局。所以整合方案必须回答三个问题串起来是这条流水线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改成 FactoryBeandefinition.setBeanClass(mapperFactoryBeanClass)默认MapperFactoryBean已核实造代理真正产出可调用的对象在getObject()里自由控制实例化过程创建 JDK 动态代理MapperFactoryBeanT implements FactoryBeanT已核实Q5getObjectType 为什么重要它和「提前判型」是什么关系因为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视频原始标题本文对应小节P43Spring如何管理Mybaits的Mapper接口的7P44说说你是如何解决 get 和 post 乱码问题3P45Spring MVC的控制器是不是单例模式,如果是,有什么问题,怎么解决2P46spring MVC的工作流程1P47SpringMVC中处理AJAX请求原理1Q2 与消息转换器分支P48Spring和SpringMVC为什么需要父子容器5P49是否可以把SpringMVC所有Bean都交给Spring容器来管理5Q3P50是否可以把Spring的Bean都交给Spring-mvc来管理5Q4P51如何实现零配置的SpringMVC原理是什么6P52SpringMVC的拦截器和过滤器的区别和执行顺序4事实说明视频里给的事实转写明确讲到仅代表视频表述DispatcherServlet 叫前端控制器/核心调度器HandlerMapping 返回含拦截器的执行链由 HandlerAdapter 调用处理方法返回值最终封装成 ModelAndView视图解析器加前缀后缀定位视图再渲染成 HTMLAJAX/JSON 走HttpMessageConverter的 read/write后续视图解析不再有Spring MVC 默认支持 JacksonXML 加annotation-driven即自动注册Fastjson 要手动配转换器控制器默认单例有状态成员变量才有并发问题解法是方法级变量、ThreadLocal、同步锁锁是下下策Servlet 作用域对象可放心注入框架已放进 ThreadLocalCharacterEncodingFilter设 UTF-8 是首要解法其次 IDE 文件编码、Tomcatconnector URIEncoding、数据库编码逐关对齐拦截器不依赖 Servlet 容器、只作用于 action 请求、可访问容器 Bean过滤器依赖 Servlet 容器、作用于所有请求顺序是过滤器 → Servlet → 拦截器 → Controller再反序退出父子容器三条原因划边界、规范架构、便于切换 Web 框架外加作者自认牵强的「省重复 Bean 创建」Controller 全放父容器会 404因为解析处理方法拿的是当前容器的 BeanDefinition 名字而非getBeanBean 全放子容器可行但 AOP/事务配置必须一起搬零配置的关键字是 SPI抽象类不能实例化所以必须自写子类用getRootConfigClasses/getServletConfigClasses交出父子配置类MyBatis 整合四个关键点借BeanDefinitionRegistryPostProcessor注册、继承内置扫描器、重写排除接口的方法、偷天换日改 beanClass 为 FactoryBean 并在getObject()里造 JDK 动态代理。官方文档 / 公开事实本次逐条核实DispatcherServlet.doDispatch内getHandler→applyPreHandle→getHandlerAdapter→ha.handle→applyPostHandle→processDispatchResult的调用顺序且只有mv ! null才renderSpring Framework 源码AbstractHandlerMethodMapping.getCandidateBeanNames()在detectHandlerMethodsInAncestorContexts默认 false时只调当前容器getBeanNamesForType(Object.class)该开关的注释明确写着可识别祖先容器通常是 Spring root WebApplicationContext里的 handler BeanRequestBody经HttpMessageConverter反序列化、ResponseBody经其序列化到响应体Spring 官方参考文档其中当前示例用的 JSON 转换器名为JacksonJsonHttpMessageConverterCharacterEncodingFilter继承OncePerRequestFilter具备forceRequestEncoding/forceResponseEncodingforceEncoding时连带设为响应默认编码Spring 源码与类注释StringHttpMessageConverter.DEFAULT_CHARSET在源码当前主干上仍是ISO_8859_1RequestContextHolder使用NamedThreadLocal/NamedInheritableThreadLocal持有请求属性spring-web中存在META-INF/services/jakarta.servlet.ServletContainerInitializer内容为org.springframework.web.SpringServletContainerInitializer该类标注HandlesTypes(WebApplicationInitializer.class)并负责实例化实现类后调用其onStartupAbstractDispatcherServletInitializer.registerDispatcherServlet通过createServletApplicationContext()建子容器再servletContext.addServlet(...)Tomcat 官方文档中 HTTP Connector 的URIEncoding默认值为 UTF-8useBodyEncodingForURI默认 false且请求字符集未知时默认按 ISO-8859-1MyBatis-Spring 文档原话「MapperScannerConfigureris aBeanDefinitionRegistryPostProcessor」以及MapperFactoryBean注册 mapper、MapperScan/mybatis:scan扫描、mapper 线程安全可直接注入MyBatis 源码MapperProxyFactory使用Proxy.newProxyInstanceMyBatis-Spring 源码中ClassPathMapperScanner继承ClassPathBeanDefinitionScanner、覆写后的候选条件为「是接口 独立 非注解类型」、definition.setBeanClass(mapperFactoryBeanClass)、写入FACTORY_BEAN_OBJECT_TYPE属性、未显式指定工厂时启用按类型自动装配MapperFactoryBeanT extends SqlSessionDaoSupport implements FactoryBeanT且实现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本文取自其中 P43P52文中一律称「视频作者」[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和 TomcatURIEncoding的默认值它们正好说明口述内容和当前版本行为之间需要一步对照验证。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑