SpringMVC 面试深度指南
SpringMVC 面试深度指南
本文面向已经用过 SpringMVC / SpringBoot Web、准备高级/资深 Java 岗位面试的同学。目标不是背"九大组件叫什么"这种应试八股,而是讲清楚一次 HTTP 请求真正经过了哪些对象、每个对象具体做了什么决策、面试官追问下去能不能接得住。
源码基准版本:Spring Framework 6.x(对应 Spring Boot 3.x,JDK 17,内嵌 Tomcat)。本文涉及的类名、方法名、字段结构均对照本地 Maven 仓库中的
spring-webmvc-6.1.6.jar、spring-web-6.1.6.jar用javap反编译签名核实过,可以认为是准确的;但不给出具体源码行号——DispatcherServlet#doDispatch、HandlerExecutionChain、RequestMappingHandlerMapping、RequestMappingHandlerAdapter、HandlerMethodArgumentResolver、RequestResponseBodyMethodProcessor、ExceptionHandlerExceptionResolver这些类的核心调用链在近几个大版本之间结构保持稳定,但方法内部的具体行号会随版本发布而变化,行号本身没有参考价值,且容易在版本更迭后失真。文中给出的是简化后忠于真实调用链语义的伪代码,不确定或可能随版本变化的细节会明确标注"建议对照本地源码复核",不做没有把握的断言。
目录
- 一、SpringMVC 整体请求处理流程
- 二、doDispatch 核心源码走读
- 三、HandlerMapping 如何把 URL 映射到 Controller 方法
- 四、参数解析与返回值处理
- 五、拦截器与过滤器
- 六、异常处理机制
- 七、常见面试高频问题清单
- 结语:面试回答的通用框架
一、SpringMVC 整体请求处理流程
要点:一次请求从进入 Tomcat 到返回响应,经过"Servlet 容器 → Filter 链 → DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → HttpMessageConverter/ViewResolver"这条纵向链路;DispatcherServlet 是整个 SpringMVC 的总调度入口,本身不处理业务,只负责按顺序编排下游组件。
1.1 一次 HTTP 请求经过哪些组件
一个 @RestController 的方法从被浏览器请求到返回 JSON,大致经过这些环节:
浏览器/客户端
|
| HTTP 请求
v
Tomcat(Servlet 容器)—— 完成 TCP 连接、HTTP 报文解析,构造 HttpServletRequest/HttpServletResponse
|
v
Servlet Filter 链(web.xml / FilterRegistrationBean 注册,属 Servlet 规范)
| 例如字符编码过滤器、CORS 过滤器、自定义鉴权过滤器
v
DispatcherServlet(SpringMVC 的核心 Servlet,实现了 Servlet 接口,本质仍是一个 Servlet)
|
|—① HandlerMapping:根据请求 URL 找到应该处理这个请求的 HandlerMethod,
| 连同匹配上的拦截器一起包装成 HandlerExecutionChain
|
|—② HandlerAdapter:不直接反射调用 Controller 方法,而是通过适配器模式统一调用入口
| (RequestMappingHandlerAdapter 负责处理 @RequestMapping 标注的方法)
|
|—③ HandlerInterceptor.preHandle():在真正调用 Controller 方法之前执行
|
|—④ 反射调用 Controller 方法本体:
| 参数由 HandlerMethodArgumentResolver 体系解析并绑定
| (@RequestParam/@PathVariable/@RequestBody 等)
|
|—⑤ HandlerInterceptor.postHandle():Controller 方法执行完、视图渲染前执行
|
|—⑥ 处理返回值:
| - 如果标注了 @ResponseBody(或类上 @RestController),走 HttpMessageConverter
| 直接把返回对象序列化写入响应体,不走视图渲染
| - 否则返回值被当作逻辑视图名,交给 ViewResolver 解析成具体 View 并渲染
|
|—⑦ HandlerInterceptor.afterCompletion():整个请求处理完成后执行(包括异常场景)
v
HttpServletResponse 写回,原路经过 Filter 链,最终由 Tomcat 把响应发送给客户端
这里要强调一个经常被面试问到的认知点:Filter 和 DispatcherServlet/HandlerInterceptor 不在同一个层级——Filter 是 Servlet 规范定义的组件,运行在 Servlet 容器层面,比 DispatcherServlet 更早介入、更晚退出(严格意义上包裹了整个 DispatcherServlet 的执行);HandlerMapping、HandlerAdapter、HandlerInterceptor 都是 DispatcherServlet 内部的 SpringMVC 概念,只有请求真正进入 DispatcherServlet 之后才谈得上。这个区别在第五节会展开细讲。
1.2 DispatcherServlet 的继承体系与初始化
要点:
DispatcherServlet通过HttpServletBean → FrameworkServlet → DispatcherServlet三层继承,把"Servlet 生命周期回调"和"Spring 容器管理"两件事解耦;容器刷新完成后触发的onRefresh()会调用initStrategies(),一次性初始化九大策略组件,这九个组件此后就是整个请求处理链路上被反复用到的角色。
继承体系(可用 javap 反编译核实,DispatcherServlet extends FrameworkServlet,FrameworkServlet extends HttpServletBean,HttpServletBean extends HttpServlet):
| 类 | 承担的职责 |
|---|---|
HttpServletBean(继承自 jakarta.servlet.http.HttpServlet) |
把 web.xml/ServletRegistrationBean 里配置的 init-param 转换成当前 Servlet 的 Bean 属性(PropertyValues 方式赋值),提供了"Servlet 配置参数 → Java Bean 属性"的桥接,不涉及 Spring 容器 |
FrameworkServlet |
负责创建/持有一个 WebApplicationContext(如果外部没有传入现成的容器,就自己初始化一个),并在容器刷新完成时回调 onRefresh();把标准 Servlet 生命周期方法(doGet/doPost 等)统一收敛到 doService() 这个模板方法 |
DispatcherServlet |
重写 onRefresh() 触发 initStrategies(),初始化九大组件;重写 doService(),内部核心是 doDispatch()——真正的请求分发逻辑 |
FrameworkServlet 的定位可以理解成"把 Servlet 生命周期和 Spring 容器生命周期对接起来的适配层":Servlet 容器(Tomcat)按 Servlet 规范调用 init()/service()/destroy(),FrameworkServlet 在这些回调里驱动 Spring 容器的创建、刷新、销毁,DispatcherServlet 则在容器刷新完成这个时机挂钩子,把 SpringMVC 特有的初始化逻辑接进去——这也是为什么 SpringMVC 能够"随 Spring 容器一起启动"而不需要额外的启动入口。
九大组件是怎么被初始化的
DispatcherServlet#initStrategies(ApplicationContext) 依次调用 9 个 initXxx() 私有方法(方法名可用 javap 核实:initMultipartResolver、initLocaleResolver、initThemeResolver、initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers、initRequestToViewNameTranslator、initViewResolvers、initFlashMapManager),对应九大组件:
| 组件 | 接口 | 作用 |
|---|---|---|
| MultipartResolver | MultipartResolver |
判断并解析文件上传请求(multipart/form-data) |
| LocaleResolver | LocaleResolver |
国际化:从请求中解析出 Locale(基于 Header/Cookie/Session 等策略) |
| ThemeResolver | ThemeResolver |
主题解析,现代 SpringBoot 项目基本不再使用 |
| HandlerMapping | HandlerMapping |
URL → Handler 的映射,最核心的组件之一,第三节详细展开 |
| HandlerAdapter | HandlerAdapter |
用适配器模式统一调用不同类型的 Handler(注解方法、Controller 接口实现类等) |
| HandlerExceptionResolver | HandlerExceptionResolver |
统一异常处理,ExceptionHandlerExceptionResolver 是其中之一,第六节详细展开 |
| RequestToViewNameTranslator | RequestToViewNameTranslator |
Controller 方法没有显式返回视图名时,按约定从请求路径推导一个默认视图名 |
| ViewResolver | ViewResolver |
逻辑视图名 → 具体 View 对象(JSP、Thymeleaf 等),纯 REST 项目里这个组件基本不会被走到 |
| FlashMapManager | FlashMapManager |
管理重定向场景下跨请求传递的 FlashMap 数据(RedirectAttributes) |
每个 initXxx() 方法遵循同一套查找逻辑:先尝试从当前 WebApplicationContext(以及可能的祖先容器)里按类型查找用户自定义配置的 Bean;如果没找到,就回退去加载 DispatcherServlet.properties(该文件与 DispatcherServlet.class 同目录)里配置的默认实现类并实例化——这也是为什么开发者平时几乎感知不到 HandlerMapping/HandlerAdapter 这些组件的存在:没有自定义配置时,SpringBoot 自动装配和这份默认策略文件已经把它们准备好了。具体默认实现类的清单以 DispatcherServlet.properties 文件内容为准,不同版本可能有增减,建议对照本地源码/依赖复核,不在此处罗列具体类名以免与实际版本脱节。
二、doDispatch 核心源码走读
要点:
doDispatch()是 SpringMVC 面试"手画流程图"题的标准答案原型,核心骨架是getHandler() → 拦截器 preHandle → HandlerAdapter.handle() → 拦截器 postHandle → processDispatchResult()(渲染视图/统一异常处理)→ 拦截器 afterCompletion,理解这个方法比背九大组件的名字重要得多。
2.1 doDispatch 的整体骨架
以下是对 DispatcherServlet#doDispatch(HttpServletRequest, HttpServletResponse) 语义还原的简化伪代码(真实源码里还夹杂了 multipart 请求的判断与清理、异步请求处理等分支,这里只保留面试最需要讲清楚的主干逻辑):
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerExecutionChain mappedHandler = null;
Exception dispatchException = null;
try {
// ① 根据请求找到匹配的 Handler,连同拦截器一起包装成 HandlerExecutionChain
mappedHandler = getHandler(request);
if (mappedHandler == null) {
noHandlerFound(request, response); // 404
return;
}
// ② 找到能执行这个 Handler 的适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// ③ 执行拦截器链的 preHandle,任意一个返回 false 就中断整个链路
if (!mappedHandler.applyPreHandle(request, response)) {
return;
}
// ④ 真正执行 Controller 方法,得到 ModelAndView
ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
// ⑤ 执行拦截器链的 postHandle(逆序执行,见第五节)
mappedHandler.applyPostHandle(request, response, mv);
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// ⑥ 统一处理渲染视图 or 异常解析,内部还会触发 afterCompletion
processDispatchResult(request, response, mappedHandler, mv, dispatchException);
}
processDispatchResult() 内部的核心逻辑(同样是语义还原):
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
if (exception != null) {
// 交给 HandlerExceptionResolver 链处理异常,
// ExceptionHandlerExceptionResolver 就在这个环节介入(详见第六节)
mv = processHandlerException(request, response, mappedHandler.getHandler(), exception);
}
if (mv != null && !mv.wasCleared()) {
render(mv, request, response); // 视图渲染:ViewResolver 解析视图名 + View.render()
}
if (mappedHandler != null) {
// 无论成功还是异常,afterCompletion 都会被调用(见第五节的回溯机制)
mappedHandler.triggerAfterCompletion(request, response, exception);
}
}
2.2 逐步拆解九个关键步骤
| 步骤 | 涉及的核心方法 | 关键决策 |
|---|---|---|
| ① 查找 Handler | DispatcherServlet#getHandler() → 遍历 handlerMappings 列表,调用每个 HandlerMapping#getHandler(request) |
谁先注册、谁先尝试匹配,第一个匹配成功的 HandlerMapping 胜出并返回 HandlerExecutionChain;SpringBoot 默认场景下命中的通常是 RequestMappingHandlerMapping(详见第三节) |
| ② 查找适配器 | DispatcherServlet#getHandlerAdapter() → 遍历 handlerAdapters,找第一个 supports(handler) 返回 true 的 |
注解方法(HandlerMethod)对应 RequestMappingHandlerAdapter;这里是典型的适配器模式,DispatcherServlet 不关心 Handler 具体类型,只依赖 HandlerAdapter 接口 |
| ③ preHandle | HandlerExecutionChain#applyPreHandle() |
按拦截器注册顺序正向执行,任意一个返回 false 立即中断,详见 5.2 节的索引回溯机制 |
| ④ 执行方法 | RequestMappingHandlerAdapter#handle() → 内部委托 invokeHandlerMethod() |
这一步内部又是一整套参数解析→反射调用→返回值处理的流程,详见第四节 |
| ⑤ postHandle | HandlerExecutionChain#applyPostHandle() |
按拦截器注册顺序逆序执行,可以在这里修改 ModelAndView |
| ⑥ 异常兜底 | DispatcherServlet#processHandlerException() → 遍历 handlerExceptionResolvers |
只要 Controller 方法执行过程中抛出未捕获异常,就会进入这里,详见第六节 |
| ⑦ 视图渲染 | DispatcherServlet#render() |
对 @ResponseBody/@RestController 场景,返回值处理其实已经在④这一步通过 HttpMessageConverter 把响应体写完了,ModelAndView 为 null,这里会被跳过 |
| ⑧ afterCompletion | HandlerExecutionChain#triggerAfterCompletion() |
无论④是否抛异常都会执行,但只对preHandle 已经返回 true 的拦截器回调,详见 5.2 节 |
| ⑨ 收尾 | cleanupMultipart() 等 |
清理 multipart 请求过程中创建的临时资源 |
2.3 对照一个 RestController 示例走一遍链路
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public UserVO getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
一次 GET /api/users/1 请求对照上面的骨架走一遍:
getHandler()遍历handlerMappings,命中RequestMappingHandlerMapping,内部通过MappingRegistry按路径/api/users/1匹配到UserController#getUser对应的HandlerMethod,连同匹配上该路径的拦截器一起包装成HandlerExecutionChain返回。getHandlerAdapter()拿到的是RequestMappingHandlerAdapter(因为 Handler 类型是HandlerMethod)。- 拦截器
preHandle()依次执行(如果配置了登录校验拦截器,未登录会在这里直接返回false中断,getUser方法根本不会被调用)。 RequestMappingHandlerAdapter#handle()内部:HandlerMethodArgumentResolver体系发现参数id标注了@PathVariable,由PathVariableMethodArgumentResolver从 URL 模板变量里取出字符串"1"并转换成Long;反射调用getUser(1L),得到返回值UserVO。- 因为类上标注了
@RestController(等价于@Controller + @ResponseBody),返回值处理走的是RequestResponseBodyMethodProcessor#handleReturnValue():不会把UserVO当作视图名,而是选中一个匹配的HttpMessageConverter(通常是MappingJackson2HttpMessageConverter)把UserVO序列化成 JSON,直接写入响应体;这一步执行完,ModelAndView为null。 - 拦截器
postHandle()逆序执行(此时ModelAndView为null,能修改的东西有限,实践中postHandle对纯 REST 接口场景意义不大)。 processDispatchResult()发现mv == null,跳过渲染。- 拦截器
afterCompletion()逆序执行,通常用于记录访问日志、清理ThreadLocal。
三、HandlerMapping 如何把 URL 映射到 Controller 方法
要点:
RequestMappingHandlerMapping在容器启动阶段扫描所有@Controller/@RequestMapping标注的 Bean,把"请求条件(路径/方法/参数等)→ HandlerMethod"的映射关系维护在内部的MappingRegistry里;运行时的路径匹配依赖AntPathMatcher(传统方式)或PathPattern(新一代实现),两者本质都是把 URL 模板编译成可复用的匹配规则。
3.1 RequestMappingHandlerMapping 与 MappingRegistry
RequestMappingHandlerMapping 的类继承关系是 AbstractHandlerMapping → AbstractHandlerMethodMapping<T> → RequestMappingInfoHandlerMapping → RequestMappingHandlerMapping(javap 反编译可核实 AbstractHandlerMethodMapping 是这条继承链的核心抽象层)。启动时的注册流程大致是:
- 容器刷新完成后,
AbstractHandlerMethodMapping#afterPropertiesSet()(实现了InitializingBean)触发initHandlerMethods()。 - 遍历容器里所有 Bean 名称,用
isHandler(Class<?>)判断该 Bean 是否是"处理器"——RequestMappingHandlerMapping里的具体判定标准是类上是否存在@Controller或@RequestMapping注解。 - 对每个命中的 Handler Bean,用反射遍历其方法,调用抽象方法
getMappingForMethod(Method, Class<?>)为每个候选方法生成一个RequestMappingInfo(封装了路径、HTTP 方法、参数条件、Header 条件、Content-Type 条件等匹配规则,这个类型就是AbstractHandlerMethodMapping<T>里的类型参数T)。 - 调用
registerHandlerMethod()把RequestMappingInfo → HandlerMethod的映射关系登记进MappingRegistry。
MappingRegistry(AbstractHandlerMethodMapping 的内部类,可用 javap 核实字段结构)内部维护了几张互补的表:
| 字段 | 类型 | 作用 |
|---|---|---|
registry |
Map<T, MappingRegistration<T>> |
主表:RequestMappingInfo → 完整注册信息(含 HandlerMethod、直接路径集合等) |
pathLookup |
MultiValueMap<String, T> |
按"直接路径"(不含通配符的精确路径段)做的一份索引,用来在请求到来时快速圈定候选的 RequestMappingInfo 集合,避免每次请求都遍历全部映射做正则匹配 |
nameLookup |
Map<String, List<HandlerMethod>> |
支持按 @RequestMapping(name=...) 定义的映射名反查 HandlerMethod,用于视图层生成 URL(如 MvcUriComponentsBuilder) |
corsLookup |
Map<HandlerMethod, CorsConfiguration> |
缓存每个 HandlerMethod 对应的跨域配置 |
配合一把 ReentrantReadWriteLock:注册/注销走写锁(@RequestMapping 支持运行时动态注册的场景,比如某些网关式应用会在运行时动态注册路由),请求匹配走读锁,保证高并发读(每次请求都要查找)和低频写之间不互相阻塞。
请求到来时的查找流程(getHandlerInternal() → lookupHandlerMethod()):先用请求路径去 pathLookup 里做一次精确/前缀查找,收窄出一批候选 RequestMappingInfo;再对每个候选调用 getMatchingMapping() 做精细匹配(校验 HTTP 方法、参数条件、Header 条件、Content-Type 等是否都满足);如果有多个候选都匹配上,用 getMappingComparator() 返回的比较器排序,选择"更精确"的那一个(比如精确路径优先于通配符路径,指定 HTTP 方法的优先于没指定的);如果最终一个都没匹配上但路径本身在 pathLookup 里存在(说明路径对但方法/参数条件不满足),会抛出 HttpRequestMethodNotSupportedException(对应常见的 405)而不是简单地当作 404。
3.2 路径匹配 AntPathMatcher 与 PathPattern
SpringMVC 的路径匹配经历过一次实现方式的演进:
| 实现 | 特点 |
|---|---|
AntPathMatcher |
传统实现,基于字符串逐段比较 + 正则表达式回退,支持 ?(单字符)、*(任意字符,不跨路径段)、**(任意字符,可跨路径段)等 Ant 风格通配符;每次匹配都要重新做一遍字符串解析,量大时有性能开销 |
PathPattern + PathPatternParser |
Spring 5 引入的新一代实现,先把 URL 模板预编译成一棵 PathPattern 结构(拆分成多个 PathElement 节点),匹配时直接对已编译的结构做遍历,避免重复解析字符串,性能明显优于 AntPathMatcher,且对 {variable} 风格的路径变量提取更高效 |
两者的语义并不完全等价(比如对某些边界通配符组合的解析结果历史上存在过差异),SpringBoot 通过 spring.mvc.pathmatch.matching-strategy 配置项在 ANT_PATH_MATCHER 和 PATH_PATTERN_PARSER 之间切换。关于具体 SpringBoot 版本下的默认策略(据了解 Spring Boot 较新版本已经把默认值切换为 PATH_PATTERN_PARSER),建议对照实际所用 SpringBoot 版本的官方文档或本地源码核实,不在此处给出绝对结论,但可以确定的是:DispatcherServlet 内部有一个 parseRequestPath 字段(javap 可核实该字段存在)用来标记是否启用预解析的 RequestPath,这是支撑 PathPattern 高效匹配的基础设施之一。
面试时如果被问到"Ant 风格路径匹配的通配符规则",可以直接给出这张表:
| 通配符 | 含义 | 示例 |
|---|---|---|
? |
匹配任意单个字符 | /user/? 匹配 /user/1,不匹配 /user/12 |
* |
匹配任意数量字符,但不跨路径分隔符 / |
/user/* 匹配 /user/list,不匹配 /user/list/1 |
** |
匹配任意数量字符,可以跨多级路径 | /user/** 同时匹配 /user/list 和 /user/list/1 |
{name} |
具名路径变量,配合 @PathVariable 提取 |
/user/{id} 匹配 /user/1,id=1 |
四、参数解析与返回值处理
要点:Controller 方法的每一个参数由
HandlerMethodArgumentResolverComposite组合而成的一组HandlerMethodArgumentResolver逐一尝试解析,不同注解对应不同的 Resolver 实现;@RequestBody/@ResponseBody都是通过HttpMessageConverter完成对象与报文之间的转换,这一整套体系是理解"SpringMVC 怎么把注解和实际行为对应起来"的关键。
4.1 HandlerMethodArgumentResolver 体系
RequestMappingHandlerAdapter 在反射调用 Controller 方法之前,需要先把方法签名里的每一个参数都"填上值",这件事由 InvocableHandlerMethod(ServletInvocableHandlerMethod 是其子类)内部持有的 HandlerMethodArgumentResolverComposite 完成——它本质是一个组合模式的容器,内部维护一个 List<HandlerMethodArgumentResolver>,对每个方法参数依次调用每个 Resolver 的 supportsParameter(MethodParameter),第一个返回 true 的 Resolver 负责该参数的 resolveArgument()。
常见注解与对应 Resolver 的映射关系(类名可用 javap 核实存在于 spring-web/spring-webmvc 中):
| 注解/参数类型 | 负责解析的 Resolver | 数据来源 |
|---|---|---|
@RequestParam |
RequestParamMethodArgumentResolver |
Query String / Form 表单参数 |
@PathVariable |
PathVariableMethodArgumentResolver |
URL 模板变量(如 /user/{id} 里的 id) |
@RequestHeader |
RequestHeaderMethodArgumentResolver |
HTTP 请求头 |
@CookieValue |
ServletCookieValueMethodArgumentResolver |
Cookie |
@RequestBody |
RequestResponseBodyMethodProcessor |
请求体,经 HttpMessageConverter 反序列化 |
@ModelAttribute / 无注解的普通对象 |
ServletModelAttributeMethodProcessor |
Query String/Form 参数按属性名逐一绑定到对象字段 |
HttpServletRequest/HttpServletResponse/HttpSession 等 Servlet API 类型 |
ServletRequestMethodArgumentResolver 等 |
由容器直接透传 |
注意到 RequestResponseBodyMethodProcessor 这个类名同时出现在参数解析和返回值处理两个场景——因为它实现了两个接口:HandlerMethodArgumentResolver(负责 @RequestBody 参数解析)和 HandlerMethodReturnValueHandler(负责 @ResponseBody 返回值处理),两件事共享同一套"对象 ↔ 报文"的转换基础设施(HttpMessageConverter),这也是它的父类命名为 AbstractMessageConverterMethodProcessor 的原因。
4.2 RequestBody 怎么把 JSON 转成对象
RequestResponseBodyMethodProcessor#resolveArgument() 的核心逻辑(语义还原):
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 1. 委托给父类 AbstractMessageConverterMethodProcessor#readWithMessageConverters
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());
// 2. 如果标注了 @Valid / @Validated,触发 JSR-303 参数校验,
// 校验失败会抛出 MethodArgumentNotValidException(对应常见的参数校验报错场景)
if (arg != null) {
validateIfApplicable(binderFactory, parameter, arg);
}
return arg;
}
readWithMessageConverters() 内部的关键决策:
- 从请求头
Content-Type得到本次请求的媒体类型(如application/json)。 - 遍历
RequestMappingHandlerAdapter持有的List<HttpMessageConverter<?>>,找出第一个满足两个条件的 Converter——canRead(targetType, contentType)返回true:既能处理目标 Java 类型,又支持这个Content-Type。 - 对于 JSON 场景,命中的通常是
MappingJackson2HttpMessageConverter(内部委托 Jackson 的ObjectMapper完成真正的反序列化:读取请求体输入流 →ObjectMapper.readValue(inputStream, targetType)→ 得到目标对象)。 - 如果没有任何 Converter 能处理这个
Content-Type,抛出HttpMediaTypeNotSupportedException(对应常见的 415 Unsupported Media Type)。
一句话讲清楚 @RequestBody 的原理:它不是"魔法",本质是"读取请求体字节流 → 按 Content-Type 选中一个合适的 HttpMessageConverter → 调用该 Converter 的反序列化能力,得到目标类型对象",MappingJackson2HttpMessageConverter 只是这套通用机制在 JSON 场景下的一个具体实现,同样的机制换成 XML 就是 Jaxb2RootElementHttpMessageConverter。
4.3 ResponseBody 返回值怎么被写回响应体
返回值处理的入口在 RequestMappingHandlerAdapter#invokeHandlerMethod() 内部:方法执行得到返回值后,交给 HandlerMethodReturnValueHandlerComposite(同样是组合模式,逻辑与参数解析对称)挑出第一个 supportsReturnType() 返回 true 的 HandlerMethodReturnValueHandler,调用其 handleReturnValue()。
对 @ResponseBody(或 @RestController)标注的方法,命中的正是 RequestResponseBodyMethodProcessor#handleReturnValue(),核心逻辑(语义还原自 AbstractMessageConverterMethodProcessor#writeWithMessageConverters):
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
mavContainer.setRequestHandled(true); // 标记:这次请求不需要走视图渲染了
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
writeWithMessageConverters() 内部:
- 结合请求头
Accept做内容协商(Content Negotiation)——客户端声明自己能接受哪些媒体类型(如Accept: application/json)。 - 遍历
HttpMessageConverter列表,找出既能处理返回值的 Java 类型、又能产出客户端Accept所要求的媒体类型的 Converter(同一套 Converter 列表,读写两个方向复用)。 - 命中的 Converter(通常仍是
MappingJackson2HttpMessageConverter)调用ObjectMapper.writeValue(outputStream, returnValue)把对象序列化成 JSON 字节流,设置好响应头Content-Type,写入HttpServletResponse的输出流。 - 如果找不到任何一个 Converter 能满足客户端
Accept要求的媒体类型,抛出HttpMediaTypeNotAcceptableException(对应常见的 406 Not Acceptable)。
关键点:mavContainer.setRequestHandled(true) 这一步标记了"这次请求的响应体已经在这里写完了,DispatcherServlet 拿到的 ModelAndView 会是 null",回到 2.1 节的 processDispatchResult(),mv == null 会导致 render() 被跳过——这就是为什么 @ResponseBody 场景下"返回值处理"和"视图渲染"是互斥的两条路径。
4.4 自定义参数解析器的原理与实现
自定义参数解析器是面试里常见的加分项,原理是实现 HandlerMethodArgumentResolver 接口的两个方法并注册进 RequestMappingHandlerAdapter:
// 1. 自定义一个注解,标注在需要特殊解析的参数上
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
// 2. 实现 HandlerMethodArgumentResolver
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 只有同时满足"标注了 @CurrentUser"且"参数类型是 UserPrincipal"才由本 Resolver 处理
return parameter.hasParameterAnnotation(CurrentUser.class)
&& parameter.getParameterType().equals(UserPrincipal.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
// 典型实现:从 SecurityContext / 请求属性 / ThreadLocal 里取出当前登录用户
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
return request.getAttribute("CURRENT_USER"); // 具体来源按业务需要调整,比如结合网关透传的用户信息、JWT 解析结果等
}
}
// 3. 注册进 SpringMVC 配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver());
}
}
使用效果:
@GetMapping("/profile")
public UserVO getProfile(@CurrentUser UserPrincipal user) {
return userService.toVO(user);
}
WebMvcConfigurer#addArgumentResolvers() 最终会被 WebMvcConfigurationSupport 收集起来,追加到 RequestMappingHandlerAdapter 内部 HandlerMethodArgumentResolverComposite 持有的列表末尾——注意是追加在内置 Resolver 之后,这意味着如果自定义的判定条件和某个内置 Resolver 的判定条件有重叠(比如同样处理 HttpServletRequest 类型),内置 Resolver 会先被匹配上,自定义的反而生效不了;因此自定义 Resolver 的 supportsParameter() 判定条件通常要设计得足够"独特"(比如强绑定一个自定义注解),避免和内置 Resolver 冲突。
五、拦截器与过滤器
要点:Filter 属于 Servlet 规范、由 Servlet 容器驱动,感知不到 Spring 容器和 Controller 方法这一层概念;HandlerInterceptor 属于 SpringMVC 框架层面、由
DispatcherServlet驱动,能拿到具体的 Handler 对象;多个拦截器靠HandlerExecutionChain内部一个interceptorIndex索引变量做正向执行 + 索引回溯,保证preHandle失败时不会对"还没执行过 preHandle"的拦截器错误地触发afterCompletion。
5.1 HandlerInterceptor 与 Servlet Filter 的本质区别
| 维度 | Servlet Filter | HandlerInterceptor |
|---|---|---|
| 所属规范/容器 | Java Servlet 规范定义,运行在 Servlet 容器层面,DispatcherServlet 本身也是被 Filter 链包裹执行的一个环节 |
SpringMVC 框架自身的概念,工作在 DispatcherServlet 内部 |
| 能拿到的上下文 | 只能拿到 ServletRequest/ServletResponse(或它们的 HTTP 子类型),不知道请求最终会落到哪个 Controller 方法 |
能拿到具体的 Object handler(可以强转成 HandlerMethod 拿到方法、注解等元信息),能感知 ModelAndView |
| 是否依赖 Spring 容器 | 不依赖,脱离 Spring 也能独立工作(部署在任意 Servlet 容器) | 依赖 Spring 容器,是 SpringMVC 的框架级扩展点 |
| 拦截粒度 | 基于 URL Pattern(web.xml/FilterRegistrationBean 配置) |
基于 HandlerMapping 注册时关联的 pathPatterns/excludePathPatterns,可以做到"排除某个具体 Controller 方法"这种更细粒度控制 |
| 典型使用场景 | 字符编码设置、跨域预检、请求日志(希望在最外层统一处理、和具体是不是 SpringMVC 请求无关的场景) | 登录鉴权、权限校验、Controller 级别的日志埋点、统一给 Model 注入公共数据(能拿到 Handler 元信息的场景) |
| 异常处理时机 | 无法感知 SpringMVC 内部的 HandlerExceptionResolver 是否已经处理过异常,异常穿透到 Filter 层时已经是 DispatcherServlet 处理完(或处理失败)之后的状态 |
afterCompletion 能拿到处理过程中抛出的异常对象,可以在这里做统一的异常日志记录 |
一句话总结本质区别:Filter 是"Servlet 容器认识的组件",HandlerInterceptor 是"SpringMVC 框架认识的组件"——Filter 站在更外层,包裹了整个 DispatcherServlet 的执行;HandlerInterceptor 站在 DispatcherServlet 内部,能感知到具体的 Handler,这也是为什么"需要拿到 Controller 方法上的注解做判断"这类需求只能用拦截器(或更细粒度的 AOP)实现,Filter 做不到。
5.2 多个拦截器的执行顺序原理
多个拦截器的注册顺序即 preHandle 的执行顺序(正序),postHandle/afterCompletion 则是逆序执行——这个"正序进、逆序出"的模式和责任链/栈的语义是一致的(类似方法调用栈:先进后出)。
HandlerExecutionChain 内部靠一个 int interceptorIndex 字段实现索引回溯机制(字段可用 javap 核实存在),语义还原:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
// 关键:只记录"已经成功执行过 preHandle"的最后一个拦截器下标
triggerAfterCompletion(request, response, null);
return false;
}
// 每成功执行一个,索引前移一位
this.interceptorIndex = i;
}
}
return true;
}
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex) {
if (!ObjectUtils.isEmpty(getInterceptors())) {
// 只从 interceptorIndex 开始逆序回溯,而不是无脑对所有拦截器调用 afterCompletion
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = getInterceptors()[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
这段逻辑解释了一个高频追问:如果第 2 个拦截器的 preHandle 返回 false,第 1 个拦截器的 preHandle 已经执行成功过,那第 1 个拦截器的 afterCompletion 会不会被调用?会不会调用第 2、第 3 个拦截器的 afterCompletion?
- 会调用第 1 个拦截器的
afterCompletion——因为它的preHandle已经成功执行过,interceptorIndex已经推进到了下标 0。 - 不会调用第 2 个及之后拦截器的
afterCompletion——它们的preHandle根本没有成功执行(第 2 个直接返回 false,第 3 个压根没被调用到),interceptorIndex没有推进到它们的下标,triggerAfterCompletion的回溯循环从interceptorIndex开始,天然不会碰到它们。
这个设计背后的原则是:afterCompletion 只对"确实执行过 preHandle 且返回了 true"的拦截器负责,语义上对称——既然某个拦截器的 preHandle 都没有正常放行,那它就不应该在"收尾"阶段被当作"已经开始处理这个请求"的一员来回调,避免拦截器内部因为缺少 preHandle 阶段设置的上下文(比如某些拦截器习惯在 preHandle 里往 ThreadLocal 塞东西,afterCompletion 里再清理)而出现空指针等异常。
正常无异常路径下完整的执行顺序示例(假设注册了拦截器 A、B,请求正常处理成功):
A.preHandle → B.preHandle → Controller方法 → B.postHandle → A.postHandle
→ 视图渲染 → B.afterCompletion → A.afterCompletion
六、异常处理机制
要点:
@ExceptionHandler/@ControllerAdvice的运行时基础是ExceptionHandlerExceptionResolver,它实现了HandlerExceptionResolver接口,在doDispatch()捕获到未处理异常后被processHandlerException()遍历到;具体匹配时会先在异常抛出的 Controller 类本身找@ExceptionHandler,找不到再去全局@ControllerAdvice里按异常类型的继承关系找最匹配的一个。
6.1 ExceptionHandler 与 ControllerAdvice 的原理
在 2.1 节的 processDispatchResult() 骨架里,一旦 doDispatch() 主流程捕获到异常,就会调用 DispatcherServlet#processHandlerException(),遍历 handlerExceptionResolvers 列表逐一尝试,第一个能处理(返回非 null 的 ModelAndView)的 Resolver 胜出。SpringBoot 默认装配的 HandlerExceptionResolver 里,专门负责 @ExceptionHandler 语义的正是 ExceptionHandlerExceptionResolver(javap 反编译可核实其继承自 AbstractHandlerMethodExceptionResolver,实现了 InitializingBean)。
它的核心工作分两个阶段:
启动阶段(afterPropertiesSet() → initExceptionHandlerAdviceCache()):扫描容器里所有标注了 @ControllerAdvice(或 @RestControllerAdvice)的 Bean,对每一个都构建一个 ExceptionHandlerMethodResolver(内部维护"异常类型 → 处理方法"的映射),缓存进字段 exceptionHandlerAdviceCache(类型是 Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>,可用 javap 核实)。这一步是启动时一次性完成的,运行时不需要每次异常都重新扫描。
请求阶段(doResolveHandlerMethodException() → getExceptionHandlerMethod()):拿到抛出异常的 HandlerMethod 和异常对象后,按以下优先级查找处理方法:
- 先看抛出异常的 Controller 类自身:如果这个 Controller 类里就定义了匹配的
@ExceptionHandler方法,直接使用(局部优先于全局)。 - 再遍历全局
@ControllerAdvice:@ControllerAdvice支持basePackages/assignableTypes/annotations等属性限定作用范围,ExceptionHandlerExceptionResolver会先筛出"作用范围覆盖当前 Controller"的那些ControllerAdviceBean,再在其中查找匹配的处理方法。 - 异常类型的匹配遵循"最具体优先"原则:如果一个
@ControllerAdvice里同时定义了handleIllegalArgumentException(IllegalArgumentException e)和handleException(Exception e),抛出的是IllegalArgumentException时优先匹配前者——这是ExceptionHandlerMethodResolver内部按异常类型继承距离排序实现的,具体排序算法建议对照本地源码ExceptionHandlerMethodResolver#getMappedMethod()(或同等语义的方法)复核,不同版本实现细节可能有出入。
找到匹配的处理方法后,会包装成 ServletInvocableHandlerMethod 并执行——这意味着 @ExceptionHandler 方法本身也享受和普通 Controller 方法一样的参数解析、返回值处理能力(可以直接在参数里声明要捕获的异常类型、HttpServletRequest,返回值同样可以标注 @ResponseBody 或直接返回 ResponseEntity)。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(new ErrorResponse("PARAM_ERROR", ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
return ResponseEntity.internalServerError().body(new ErrorResponse("SYSTEM_ERROR", "系统繁忙"));
}
}
6.2 异常处理器的匹配范围与优先级
| 场景 | 是否会被匹配到 |
|---|---|
Controller 自身定义了 @ExceptionHandler |
优先匹配,即使全局 @ControllerAdvice 里也有能处理同类异常的方法 |
@ControllerAdvice 未指定 basePackages/assignableTypes 等限定属性 |
默认对所有 Controller 生效(全局兜底) |
@ControllerAdvice(basePackages = "com.example.order") |
只对该包(含子包)下的 Controller 抛出的异常生效 |
同一个 @ControllerAdvice 里存在多个可能匹配的 @ExceptionHandler |
选择异常类型继承关系上"距离最近"的那个(子类异常优先匹配声明为子类的处理方法) |
异常发生在 Filter 层,尚未进入 DispatcherServlet |
ExceptionHandlerExceptionResolver 完全无法感知,需要额外的 Filter 级异常处理或依赖 Servlet 容器的错误页面机制(web.xml 的 error-page 或 SpringBoot 的 ErrorController) |
最后一行是一个容易被面试追问出来的边界:@ExceptionHandler/@ControllerAdvice 只能捕获 DispatcherServlet 分发之后、Controller 方法执行过程中抛出的异常,如果异常发生在 Filter 里(比如自定义鉴权 Filter 校验失败直接抛异常),根本不会进入这套机制,需要 Filter 自己 try-catch 处理,或者依赖 SpringBoot 提供的 BasicErrorController/自定义 ErrorController 作为容器级别的兜底。
七、常见面试高频问题清单
要点:这一节把前六节的内容按"问题 → 参考答案"的形式重新组织,答题时优先讲清楚调用链和关键类名,而不是罗列概念。
说一下 SpringMVC 的工作流程
要点:按"请求先到 DispatcherServlet,DispatcherServlet 找 Handler、找 Adapter、执行拦截器、反射调用方法、处理返回值、(可能)渲染视图"这个顺序组织语言,能画出
getHandler → applyPreHandle → handle → applyPostHandle → processDispatchResult → triggerAfterCompletion这条链路是加分项。
参考答案框架:请求先经过 Servlet 容器和 Filter 链到达 DispatcherServlet;DispatcherServlet#doDispatch() 依次做几件事——① 遍历 HandlerMapping 找到匹配的 HandlerExecutionChain(Handler + 拦截器列表);② 遍历 HandlerAdapter 找到能执行这个 Handler 的适配器;③ 执行拦截器 preHandle;④ 用 RequestMappingHandlerAdapter 反射调用 Controller 方法,参数由 HandlerMethodArgumentResolver 体系解析;⑤ 执行拦截器 postHandle;⑥ 如果过程中抛异常,交给 HandlerExceptionResolver(如 ExceptionHandlerExceptionResolver)处理;⑦ 如果不是 @ResponseBody 场景,走 ViewResolver 渲染视图,@ResponseBody 场景则已经在④这一步通过 HttpMessageConverter 写完响应体;⑧ 执行拦截器 afterCompletion 收尾。详见第二节。
RequestBody 和 ResponseBody 分别是怎么工作的
要点:两者共享同一套
HttpMessageConverter机制,@RequestBody是"按 Content-Type 选 Converter 做反序列化",@ResponseBody是"按 Accept 头做内容协商后选 Converter 做序列化",实现类都在RequestResponseBodyMethodProcessor里。
参考答案框架:@RequestBody 由 RequestResponseBodyMethodProcessor(作为 HandlerMethodArgumentResolver)处理,根据请求头 Content-Type 从 HttpMessageConverter 列表里选出能处理该类型的 Converter(JSON 场景通常是 MappingJackson2HttpMessageConverter),调用其反序列化能力把请求体转成目标 Java 对象,之后如果标了 @Valid 还会触发参数校验。@ResponseBody 由同一个类(作为 HandlerMethodReturnValueHandler)处理,根据请求头 Accept 做内容协商选出 Converter,把返回对象序列化写入响应体,并标记 ModelAndView 不需要视图渲染。详见第四节 4.2、4.3。
拦截器和过滤器的区别,各自的使用场景
要点:本质区别是所属层级不同——Filter 是 Servlet 规范的概念,Interceptor 是 SpringMVC 框架内部的概念;能不能拿到具体 Handler/方法元信息是最直观的判断标准。
参考答案框架:Filter 属于 Servlet 规范,运行在 DispatcherServlet 外层,只能操作 ServletRequest/ServletResponse,感知不到请求最终会落到哪个 Controller 方法,适合做跨域、编码设置这类和具体业务无关的通用处理;HandlerInterceptor 是 SpringMVC 框架自身的扩展点,工作在 DispatcherServlet 内部,能拿到具体的 Handler 对象(可以强转成 HandlerMethod 读取注解等元信息),适合做登录鉴权、权限校验这类需要感知"这是哪个 Controller 方法"的场景。多个拦截器的执行顺序是"正序 preHandle、逆序 postHandle/afterCompletion",HandlerExecutionChain 内部用 interceptorIndex 索引回溯保证只对"已成功执行过 preHandle"的拦截器触发 afterCompletion。详见第五节。
一个请求进来怎么找到对应的 Controller 方法的
要点:核心是容器启动时
RequestMappingHandlerMapping建立的MappingRegistry,运行时按路径先做一次索引收窄,再做精细匹配。
参考答案框架:容器启动阶段,RequestMappingHandlerMapping(继承自 AbstractHandlerMethodMapping)扫描所有 @Controller/@RequestMapping 标注的 Bean,为每个方法生成 RequestMappingInfo(封装路径、HTTP 方法、参数/Header 条件等),登记进内部的 MappingRegistry——包括按精确路径索引的 pathLookup、主映射表 registry 等结构。请求到来时,先用请求路径在 pathLookup 里做一次快速收窄得到候选集合,再对候选逐一做精细匹配(校验 HTTP 方法、参数条件等),如果有多个都匹配则用比较器选出最精确的一个;路径匹配底层依赖 AntPathMatcher 或更高性能的 PathPattern。详见第三节。
如何自定义一个参数解析器
要点:实现
HandlerMethodArgumentResolver接口的supportsParameter/resolveArgument两个方法,再通过WebMvcConfigurer#addArgumentResolvers()注册;自定义的会被追加在内置 Resolver 之后,判定条件要设计得足够独特避免被内置 Resolver "抢先"匹配。
参考答案框架:定义一个自定义注解标注在目标参数上,实现 HandlerMethodArgumentResolver——supportsParameter() 里判断参数是否同时满足"标了自定义注解"和"类型匹配",resolveArgument() 里实现具体的取值逻辑(比如从 NativeWebRequest 拿到 HttpServletRequest,从中取出登录用户信息);再通过实现 WebMvcConfigurer 接口的 addArgumentResolvers() 方法把自定义 Resolver 注册进去。底层原理是 RequestMappingHandlerAdapter 内部的 HandlerMethodArgumentResolverComposite 持有一个 Resolver 列表,对每个方法参数依次询问每个 Resolver 能不能处理,命中第一个返回 true 的就交给它解析,自定义的 Resolver 只是被追加进了这个列表。详见第四节 4.4。
结语:面试回答的通用框架
回答 SpringMVC 相关问题时,比较稳妥的表达顺序是:这个环节在整体请求链路里处于什么位置 → 对应的核心类/接口是什么 → 关键方法内部具体做了什么决策 → 有没有可以对照的真实使用场景或容易踩的坑。比如被问"@RequestBody 是怎么工作的",不要只回答"能把 JSON 转成对象",而是完整讲清楚"它是 HandlerMethodArgumentResolver 体系里的一环 → 具体由 RequestResponseBodyMethodProcessor 处理 → 内部根据 Content-Type 从 HttpMessageConverter 列表里选中匹配的 Converter(JSON 场景是 MappingJackson2HttpMessageConverter)完成反序列化 → 选不中会抛 415,这也是排查'请求体转换失败'类问题时该看的方向"。这个"位置 → 类/接口 → 内部决策 → 场景/坑"的框架同样适用于 doDispatch 流程、拦截器执行顺序、异常处理机制等几乎所有 SpringMVC 面试题,比单纯背诵"九大组件"或"注解列表"更容易在面试官追问下去时接得住。

浙公网安备 33010602011771号