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 整体请求处理流程

要点:一次请求从进入 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 请求对照上面的骨架走一遍:

  1. getHandler() 遍历 handlerMappings,命中 RequestMappingHandlerMapping,内部通过 MappingRegistry 按路径 /api/users/1 匹配到 UserController#getUser 对应的 HandlerMethod,连同匹配上该路径的拦截器一起包装成 HandlerExecutionChain 返回。
  2. getHandlerAdapter() 拿到的是 RequestMappingHandlerAdapter(因为 Handler 类型是 HandlerMethod)。
  3. 拦截器 preHandle() 依次执行(如果配置了登录校验拦截器,未登录会在这里直接返回 false 中断,getUser 方法根本不会被调用)。
  4. RequestMappingHandlerAdapter#handle() 内部:HandlerMethodArgumentResolver 体系发现参数 id 标注了 @PathVariable,由 PathVariableMethodArgumentResolver 从 URL 模板变量里取出字符串 "1" 并转换成 Long;反射调用 getUser(1L),得到返回值 UserVO。
  5. 因为类上标注了 @RestController(等价于 @Controller + @ResponseBody),返回值处理走的是 RequestResponseBodyMethodProcessor#handleReturnValue():不会把 UserVO 当作视图名,而是选中一个匹配的 HttpMessageConverter(通常是 MappingJackson2HttpMessageConverter)把 UserVO 序列化成 JSON,直接写入响应体;这一步执行完,ModelAndView 为 null。
  6. 拦截器 postHandle() 逆序执行(此时 ModelAndView 为 null,能修改的东西有限,实践中 postHandle 对纯 REST 接口场景意义不大)。
  7. processDispatchResult() 发现 mv == null,跳过渲染。
  8. 拦截器 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 是这条继承链的核心抽象层)。启动时的注册流程大致是:

  1. 容器刷新完成后,AbstractHandlerMethodMapping#afterPropertiesSet()(实现了 InitializingBean)触发 initHandlerMethods()。
  2. 遍历容器里所有 Bean 名称,用 isHandler(Class<?>) 判断该 Bean 是否是"处理器"——RequestMappingHandlerMapping 里的具体判定标准是类上是否存在 @Controller 或 @RequestMapping 注解。
  3. 对每个命中的 Handler Bean,用反射遍历其方法,调用抽象方法 getMappingForMethod(Method, Class<?>) 为每个候选方法生成一个 RequestMappingInfo(封装了路径、HTTP 方法、参数条件、Header 条件、Content-Type 条件等匹配规则,这个类型就是 AbstractHandlerMethodMapping<T> 里的类型参数 T)。
  4. 调用 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() 内部的关键决策:

  1. 从请求头 Content-Type 得到本次请求的媒体类型(如 application/json)。
  2. 遍历 RequestMappingHandlerAdapter 持有的 List<HttpMessageConverter<?>>,找出第一个满足两个条件的 Converter——canRead(targetType, contentType) 返回 true:既能处理目标 Java 类型,又支持这个 Content-Type。
  3. 对于 JSON 场景,命中的通常是 MappingJackson2HttpMessageConverter(内部委托 Jackson 的 ObjectMapper 完成真正的反序列化:读取请求体输入流 → ObjectMapper.readValue(inputStream, targetType) → 得到目标对象)。
  4. 如果没有任何 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() 内部:

  1. 结合请求头 Accept 做内容协商(Content Negotiation)——客户端声明自己能接受哪些媒体类型(如 Accept: application/json)。
  2. 遍历 HttpMessageConverter 列表,找出既能处理返回值的 Java 类型、又能产出客户端 Accept 所要求的媒体类型的 Converter(同一套 Converter 列表,读写两个方向复用)。
  3. 命中的 Converter(通常仍是 MappingJackson2HttpMessageConverter)调用 ObjectMapper.writeValue(outputStream, returnValue) 把对象序列化成 JSON 字节流,设置好响应头 Content-Type,写入 HttpServletResponse 的输出流。
  4. 如果找不到任何一个 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 和异常对象后,按以下优先级查找处理方法:

  1. 先看抛出异常的 Controller 类自身:如果这个 Controller 类里就定义了匹配的 @ExceptionHandler 方法,直接使用(局部优先于全局)。
  2. 再遍历全局 @ControllerAdvice:@ControllerAdvice 支持 basePackages/assignableTypes/annotations 等属性限定作用范围,ExceptionHandlerExceptionResolver 会先筛出"作用范围覆盖当前 Controller"的那些 ControllerAdviceBean,再在其中查找匹配的处理方法。
  3. 异常类型的匹配遵循"最具体优先"原则:如果一个 @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 面试题,比单纯背诵"九大组件"或"注解列表"更容易在面试官追问下去时接得住。

posted @ 2026-07-20 15:44  zhangph  阅读(14)  评论(0)    收藏  举报