
1. 从一次请求说起整体链路设计1.1 为什么需要一个统一前端控制器很多刚接触 Java Web 的开发者最早写的是 Servlet一个请求进来根据 URL 手动判断走哪个类、调用哪个方法再到最后手动 setContentType、getWriter() 输出 HTML。这种方式最大的问题不在代码量大而在“职责混乱”路由、参数解析、业务调用、响应输出全都揉在一个类里改一个 URL 规则就得动业务代码耦合高到让人头疼。Spring MVC 的出现本质上就是想解决这个混乱。它把所有请求先交到一个统一入口——DispatcherServlet再去分发到具体的业务方法。这就是“前端控制器”模式。其实同类型的思想在很多框架里都能看到比如 Struts、JSF 里也有类似的集中分发组件但 Spring MVC 把这件事做得更干净组件拆得更细扩展点也更多。面试中如果被问到“Spring MVC 请求流程”大概率不是想听你背几个类的名字而是想确认你是否清楚一条请求从浏览器发起到响应返回中间被拆成了哪些阶段、每个阶段由谁负责、哪个环节是固定流程、哪个环节可以灵活扩展。把这些回答透了比单纯背出 DispatcherServlet、HandlerMapping、HandlerAdapter 这几个单词要有说服力得多。1.2 请求流程的核心链路总览先给一个最简版的总览方便后面逐层展开。一个标准 Spring MVC 请求大致会经过下面几个阶段请求到达 DispatcherServlet框架拿到 HttpServletRequest 和 HttpServletResponse。DispatcherServlet 调用 HandlerMapping根据请求路径、请求方法等条件找到能处理这个请求的 Handler通常是一个 Controller 方法。DispatcherServlet 调用 HandlerAdapter 去执行这个 Handler。在执行前后还可能经过拦截器链Interceptor Chain。Controller 方法执行业务逻辑返回一个 ModelAndView或者使用 ResponseBody 直接写出响应体。DispatcherServlet 把结果交给 ViewResolver 解析得到 View 对象并渲染输出如果走的是 JSON 等消息转换则由 HttpMessageConverter 完成输出。简单理解DispatcherServlet 就像公司前台它不干具体业务只负责接待、分配、协调资源HandlerMapping 相当于内部通讯录负责查哪个部门来办这件事HandlerAdapter 相当于部门里具体干活的人不同的 Controller 方法形态需要不同的执行方式ViewResolver 则像排版师傅负责把结果整理成客户看得懂的样子。这个流程用大白话说并不难难的是把几个关键组件之间的协作和边界说清。很多开发者能说出大概链条但不能解释 HandlerAdapter 为什么要存在或者说不出拦截器到底在哪个阶段执行。这些细节才是面试官区分你“背过答案”和“真懂原理”的关键点。1.3 请求流程的组件职责划分先把核心组件的职责讲透后面才能顺畅地看源码和实操。DispatcherServlet 是 Spring MVC 的门面它继承自 FrameworkServlet再往上继承 HttpServletBean 和 HttpServlet。它在初始化时会收集 WebApplicationContext 中所有相关组件包括 HandlerMapping、HandlerAdapter、ViewResolver、LocaleResolver、ThemeResolver、HandlerExceptionResolver 等并在 doDispatch 中把它们组合起来完成一次请求处理。HandlerMapping 负责“请求到方法的映射”。最常见的是 RequestMappingHandlerMapping它扫描 RequestMapping 注解把请求路径、HTTP 方法、参数条件等内容映射到 Controller 的 Method并缓存起来。匹配时需要处理路径变量、通配符、produces/consumes 条件等。Spring 会持有多个 HandlerMapping按顺序逐个尝试拿到第一个能处理的就返回。HandlerAdapter 负责“调用真正的处理逻辑”。有人会问既然 Handler 都已经找到 Controller 方法了为什么不直接反射调用还要再套一层适配器原因是 Controller 方法的定义方式太灵活了可能是普通的无参方法可能带 RequestBody 参数可能返回 String、ModelAndView、ResponseEntity也可能是 Callable 或 DeferredResult 异步返回。Adapter 的作用就是把不同的方法形态统一适配成 ModelAndView 或 ResponseBody 结果。RequestMappingHandlerAdapter 是核心实现它内部会处理参数解析器和返回值处理器。ViewResolver 负责把逻辑视图名解析成具体的 View。常见的有 InternalResourceViewResolver它把 “user/list” 拼接成 “/WEB-INF/views/user/list.jsp” 这样的真实路径。还有 Thymeleaf 的视图解析器、FreeMarker 的视图解析器等。View 对象拿到模型数据后做模板渲染然后输出到 Response。这四类组件是 Spring MVC 的核心骨架。理解它们之间的层次关系再去看请求流程就不会糊。下面的章节我会逐阶段拆流程把配置、源码、实操、踩坑揉在一起讲。2. 请求流程各阶段细节解析与实操要点2.1 请求到达与处理器映射先看请求进来那一刻发生了什么。容器在 web.xml 或通过 Servlet 注解把 DispatcherServlet 映射到 “/” 路径所以所有请求都会先落到它的 service 方法。FrameworkServlet 重写了 service()把 GET、POST、PUT、DELETE 等请求统一交给 processRequest() 处理。processRequest 里做几件重要的事设置 LocaleContext、装配 RequestAttributes、注册可销毁回调然后调用 doService()最后恢复现场。这里有个容易被忽视的细节请求参数、Locale 等上下文会被绑定到当前线程方便后续代码通过 RequestContextHolder 访问。接下来进入 DispatcherServlet 的 doService()。它把一些常用属性塞进 request 的 attribute 中比如 webApplicationContext、localeResolver、themeResolver 等然后调用 doDispatch()。doDispatch 是整个流程的核心几乎所有关键组件的调用都发生在这里。从这一层开始你才能在源码里真正看清 HandlerMapping 和 HandlerAdapter 是如何协作的。HandlerMapping 大多数项目里只有一个 RequestMappingHandlerMapping但在复杂工程中可能有多个。Spring 会维护一个按 order 排序的列表依次调用 getHandler(request) 方法拿到第一个非空结果就用。getHandler 内部会让每个 HandlerMapping 去计算 handler 和拦截器链最后返回一个 HandlerExecutionChain。注意HandlerExecutionChain 里除了 handler 本身还包含匹配到的所有 HandlerInterceptor。HandMapping 匹配时需要处理路径变量提取、正则表达式匹配、consumes 与 produces 条件判断。比如 RequestMapping(value /api/user/{id}, method RequestMethod.GET, produces application/json)那么请求路径 /api/user/123 且 Accept 头包含 application/json 时才能命中。处理失败则返回 null继续下一个映射器。实操中比较常见的坑是路径匹配规则和静态资源冲突。很多项目在配置 DispatcherServlet 时映射到 “/”导致静态资源 JS/CSS 也被当成动态请求处理出现 404。解决办法是使用 mvc:resources 或 WebMvcConfigurer 中的 addResourceHandlers 显式声明静态资源映射让默认 Servlet 处理。另一个坑是 URL 大小写问题Spring 默认区分大小写如果你在 Linux 上部署把 /Api/User 写成 /api/user 就会匹配失败但本地 Windows 又发现没问题容易让人摸不着头脑。2.2 参数绑定与类型转换在 HandlerMapping 找到目标方法之后真正调用方法之前需要先解决一个问题方法的参数从哪里来Spring MVC 在这儿设计了一套非常灵活的机制那就是 HandlerMethodArgumentResolver。每一种参数类型都可以有对应的解析器比如 PathVariable 由 PathVariableMethodArgumentResolver 处理RequestParam 由 RequestParamMethodArgumentResolver 处理RequestBody 由 RequestResponseBodyMethodProcessor 处理Model 类型由 ModelMethodProcessor 处理。参数解析并不是单纯从 request 里 getParameter 那么简单。类型转换才是核心难点。比如一个查询参数 age18目标方法的参数类型是 intSpring 会找到合适的转换器把字符串转为 Integer再比如前端传一个日期字符串 2025-05-20目标是 LocalDate就需要注册 DateTimeFormatter 才能正确转换。如果转换失败框架抛出 TypeMismatchException最后会被异常解析器处理成 400 错误。这里我说一个很多人不重视但面试容易问的点标准 Servlet 规格里HTTP 请求参数和 multipart 文件上传不是一回事。Spring 在处理文件上传时需要配置 MultipartResolver。请求进来后DispatcherServlet 会先调用 multipartResolver 的 isMultipart() 判断请求是否包含文件如果包含就包装成 MultipartHttpServletRequest。这个步骤发生在 doDispatch 调用 HandlerMapping 之前也就是说文件上传的请求在路由阶段就已经被特殊处理过了。在参数绑定的链路上还有两个容易被忽略的细节。一是 Valid 注解的校验逻辑它在参数解析时通过 DataBinder 完成校验结果会存到 BindingResult 里如果方法形参中有 BindingResult 参数Spring 会把校验错误注入进去而不是像无参绑定那样直接抛异常。二是自定义参数解析器如果想要一个特殊对象比如当前登录用户的信息可以在 WebMvcConfigurer 里添加自定义 HandlerMethodArgumentResolver这样每个 Controller 方法都能直接声名一个参数来获取登录信息避免写一堆从 Session 取数据的重复代码。参数绑定这一阶段日常开发里出问题最多的就是日期格式、枚举转换、以及 JSON 对象反序列化。比如前端传的日期是 “yyyy/MM/dd”后端默认按 “yyyy-MM-dd” 解析就会抛 FormatException。解决方法是全局配置一个格式转换器或者使用 JsonFormat 直接在实体类字段上标注格式。这块建议在实际项目里统一约定时间格式别每个接口各搞一套不然维护成本很高。2.3 处理器调用与返回值处理参数准备好了接下来就是真正执行目标方法。RequestMappingHandlerAdapter 的核心处理逻辑是从 HandlerExecutionChain 中提取出抬 headler 对象然后 invokeHandlerMethod。这里要先说拦截器链在 DispatcherServlet 执行 handler 之前会先调用 chain.applyPreHandle()也就是按顺序执行每个拦截器的 preHandle 方法。如果某个拦截器返回 false请求就中断不再执行后面的拦截器链和目标方法直接走 afterCompletion 的清理逻辑。真正调用 handler 时RequestMappingHandlerAdapter 会创建 ServletInvocableHandlerMethod它负责把请求参数映射成方法参数再通过反射调用方法。Controller 方法执行完返回值需要被处理成 ModelAndView 或响应体。这里涉及 HandlerMethodReturnValueHandler。常见的返回值类型包括 String逻辑视图名、ModelAndView、View、Map、Model、ResponseBody 标注的对象等。其中 ResponseBody 对象的处理由 RequestResponseBodyMethodProcessor 完成它会把对象交给 HttpMessageConverter 序列化成 JSON、XML 或其它格式。项目里常用 Jackson 做 JSON 序列化Spring Boot 默认已经自动配置了 MappingJackson2HttpMessageConverter。需要自定义序列化规则时可以配置 ObjectMapper 的模块比如对日期字段做全局序列化格式统一避免前端拿到一个末端带数字的乱糟糟时间戳数组。再说拦截器的执行时机很多人说“拦截器在 Controller 之前执行”这话不完整。完整的顺序是preHandle在 handler 执行前按顺序执行所有拦截器如果有返回 false 则中断、然后在 handler 执行完成后执行 postHandle此时已经在目标方法返回、正准备渲染视图之前、最后在完整请求处理完成后执行 afterCompletion无论是否有异常都会执行常用于释放资源、清理上下文。这套顺序在日常排查问题时非常有用比如遇到“日志打了 preHandle 但业务方法没执行”的情况基本可以断定是前面某个拦截器 preHandle 返回了 false或者抛了未处理异常。返回值处理这块我建议大家在本地用日志或者断点看一下实际返回路径。说个我看到的案例某同事在一个 Controller 方法上同时写了 ResponseBody 和返回值 String一直以为返回的字符串会被解析成 JSON 字符串结果前端拿到的是带引号的原始 JSON 格式。问题就出在返回值解析器选择逻辑ResponseBody 优先走消息转换器String 类型会被 StringHttpMessageConverter 输出原样字符串并不会再次序列化成 JSON。这种细节看一遍源码比背十篇博客有用。2.4 视图解析与响应输出Spring MVC 的请求并不是都会走视图解析。如果一个方法标注了 ResponseBody或者返回的对象是 ResponseEntity框架会直接用消息转换器写出响应体不经过 ViewResolver。但如果方法返回的是逻辑视图名String、View、ModelAndView那么 DispatcherServlet 就会调用 viewResolver 去解析出 View。这里再展开讲一下 ViewResolver 的解析流程DispatcherServlet 默认使用 ViewResolver 列表中的第一个能返回 View 的解析器。InternalResourceViewResolver 是最常用的它会将逻辑视图名拼接成服务器内部资源路径比如视图名 “user/detail” 会被解析成 “/WEB-INF/views/user/detail.jsp”然后交给 RequestDispatcher 转发到该资源。所以当 JSP 文件不存在时常见表现是 404因为实际转发到一个不存在的路径。除了 JSP 还有 Thymeleaf、FreeMarker 等模板引擎它们也实现了 ViewResolver。使用 Thymeleaf 时需要配置 SpringTemplateEngine、模板解析器、视图解析器Spring Boot 里加了依赖和配置项后自动装配省了很多事。不过要注意多个视图解析器的顺序比如同时存在 Thymeleaf 和 JSP 解析器顺序不对可能导致视图名被先解析成 JSP 路径结果找不到文件。老项目里如果从 JSP 迁移到 Thymeleaf建议把 Thymeleaf 解析器的 order 设为更靠前的值并保留 JSP 解析器作为兼容。View 渲染的过程中也会涉及一步“隐藏”的操作那就是 HandlerExceptionResolver。如果 Controller 方法抛了异常DispatcherServlet 在 processDispatchResult 里会调用 processHandlerException逐个询问异常解析器谁能处理当前异常。ExceptionHandler 注解、ControllerAdvice 全局异常处理本质上都注册成了 ExceptionHandlerExceptionResolver。异常处理结果也可能返回 ModelAndView或者通过 ResponseBody 直接输出错误 JSON。所以我一直强调把全局异常处理当成请求流程的一环来理解而不是独立的“装饰性代码”你会更容易定位很多线上问题的根因。最终View 渲染的结果写入 HttpServletResponse。这中间涉及 response 的字符编码、Content-Type、缓存控制头等看似琐碎但业务一旦出现乱码、下载文件内容损坏等问题排查点往往就藏在响应输出这一步。比如用 StringHttpMessageConverter 输出中文时默认按 ISO-8859-1 编码就会出现乱码解决办法通常是配置消息转换器的默认字符集为 UTF-8或者统一在 yml 里设置 server.servlet.encoding。很多新人踩这个坑花一天查代码查不到原因其实问题出在框架层默认编码。3. 手撕一个请求的完整旅程练一遍源码级流程3.1 模拟场景与关键配置光讲原理终究不如亲手推一遍。下面用一个小场景把上面的流程串起来也方便你在本地跟着 Debug 过一遍。假设有一个简单的订单接口Controller 定义如下RestController RequestMapping(/api/order) public class OrderController { GetMapping(/{id}) public Order queryOrder(PathVariable(id) Long id) { Order order new Order(); order.setId(id); order.setName(sample); return order; } }启动项目后浏览器访问GET http://localhost:8080/api/order/10086请求发出后我们按阶段给每一步打上标记然后对照 Spring MVC 源码看整个过程。这里先给出一段配置确保项目是标准的 Spring Boot Web 工程没有额外干扰因素spring: application: name: order-service web: resources: add-mappings: true server: port: 8080有人可能会觉得这个场景太简单没有 JSP、没有拦截器、没有异常处理。实际上一个纯 REST 接口反而更适合理解核心链路因为视图解析和复杂参数解析都被简化了剩下的就是框架最基本的协作逻辑。基础链路搞清了再往复杂边界上延伸会更轻松。3.2 核心阶段追踪阶段一请求到达 DispatcherServlet。容器将请求交给 DispatcherServlet 的 service 方法FrameworkServlet 将其转为 processRequest。你在断点里看到的第一个关键方法通常是 doService再往下是 doDispatch。doDispatch 第一件事就是检查是否包含文件上传请求这里没有 multipart 文件直接跳过。接着遍历 HandlerMappings 查找 handler。阶段二HandlerMapping 匹配。RequestMappingHandlerMapping 解析到 /api/order/{id} 这个映射提取路径变量 id 10086。同时会匹配 HTTP 方法 GET。如果请求路径匹配了但方法不允许比如 POST 请求访问这个 GET 接口框架会抛出 HttpRequestMethodNotSupportedException。这个细节容易被忽略HandlerMapping 返回 handler 成功不代表就可以执行HTTP 方法也是映射条件的重要组成部分。阶段三适配器执行。HandlerAdapter 拿到 HandlerExecutionChain先生成一个 HandlerMethodArgumentResolverComposite遍历所有参数解析器找到 PathVariableMethodArgumentResolver 可处理参数 id然后从 request 中提取路径变量并转换类型反射调用 queryOrder 方法。调用前拦截器 chain.applyPreHandle 先执行这里没有配置拦截器直接放行。阶段四返回值处理。queryOrder 返回一个 Order 对象由于类上是 RestController等价于类和方法上都标了 ResponseBody所以返回值处理器选择 RequestResponseBodyMethodProcessor。它查找消息转换器找到 MappingJackson2HttpMessageConverter把 Order 序列化成 JSON写入 Response。此时整个过程并没有经历 ViewResolver 和 View 渲染。阶段五收尾。DispatcherServlet 处理完成执行 afterCompletion 清理释放线程绑定的一些上下文资源响应以 JSON 形式返回给浏览器。整个调用链从 doDispatch 到返回没有出现视图解析器。3.3 关键类级逻辑对照我建议你本地打开 Spring 源码将几个关键类打上断点逐个看DispatcherServlet.doDispatch(HttpServletRequest request, HttpServletResponse response)RequestMappingHandlerMapping.lookupHandlerMethod(String lookupPath, HttpServletRequest request)RequestMappingHandlerAdapter.handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod)ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... provideArgs)如果你用的是 Spring Boot 3.x 或 2.7.x 等常用版本类名基本都是一致的。逐个断点走一遍后你会有一个很直观的感受Spring MVC 把请求过程拆成了特别清晰的链式调用每个阶段都是一段不太长的代码组合起来就形成了一个功能完备、扩展点极多的大流程。多看几遍源码还有一个额外收获你会发现在 doDispatch 中即使发生异常框架也会尝试寻找异常处理器来处理而不是直接把异常抛给容器。这就是为什么 Controller 里抛异常前端能收到友好的 JSON 错误提示而不是一堆堆栈信息。理解了这一点你就可以围绕 HandlerExceptionResolver 做很多自定义工作比如统一响应包装结构、错误码映射、日志埋点。我个人的建议是不要试图一次看懂全部源码。找一个最简单的 GET 请求把上面四个关键方法断点打上来回看两三遍然后试着关闭断点自己画一遍时序脑子里画也行。等你画到第 2 遍的时候会发现很多曾经需要死记硬背的组件和顺序已经自然记住了。4. 常见问题与排查技巧实录4.1 404 与映射失败404 是 Spring MVC 开发里出现概率最高的问题但原因五花八门。最常见的有这么几类路径明明写对了但请求进入的上下文路径不对导致 DispatcherServlet 根本没接收到Controller 类没有加 RestController 或 Controller扫描不到方法上的 RequestMapping 路径拼错了还有静态资源被 DispatcherServlet 拦截URL 对应不上任何 RequestMapping直接 404。排查这类问题我习惯先看后台日志有没有 DispatcherServlet 的接收痕迹。如果日志里根本没有 Spring MVC 的请求处理记录说明DispatcherServlet 的映射路径没有覆盖这个请求问题出在 servlet 配置或拦截器上。如果有记录但没有匹配到 handler日志一般会输出类似 “No mapping for GET /api/xxx” 的提示。看到这类日志优先去扫 Controller 类的映射配置再检查是不是 HandlerMapping 没有扫描到相关包名。另外还有一个特别隐蔽的坑类上用了 RequestMapping(/api)方法上又用了 GetMapping(/order/{id})但项目里某些路径变量包含斜杠或特殊字符比如 id 本身是一个带 / 的令牌值那么在路径匹配时会因为无法正确拆分而 404。解决方式通常是改用 PathPattern 或调整路径结构如将参数放到 query string 而非路径中。我在实际业务里遇到过接口参数里带 URL 编码后的斜杠修了很久才发现是路径匹配和参数传递的边界问题。4.2 参数绑定与类型转换失败参数绑定失败通常是报 400 Bad Request消息里会提示是哪个参数无法匹配或转换。比较典型的是前端传了一个空字符串给 int 类型字段或者把字符串 “a” 当成 Long 类型传递。Spring 的默认类型转换器对这类输入没有 AB 心理null 和空字符串转基本类型会有差异有时会转成 0。这个行为在不同项目里可能不同因为 Spring Boot 对 String 到 int 的处理有自己的策略但总体趋势是如果转换失败会抛 TypeMismatchException。遇到参数绑定问题我建议先看异常堆栈它会明确说出是哪个 converter 失败以及原始值是什么。如果没有明确提示可以在参数解析加断点看看 HandlerMethodArgumentResolver 走到了哪个实现。很多时候问题出在日期格式解决办法是全局注册 ConverterString, LocalDate 或 DateTimeFormatterRegistrar。枚举类型也经常出状况前端传的是枚举名字后端枚举定义的是中文 key 或数字 code就需要注册一个自定义 Converter 来实现字符串到枚举的映射。我还遇到过一个比较有意思的问题同一个 Controller 方法里参数名是 userId前端传的也是 userId但注入后是 null。排查了半天发现是项目里开启了编译参数 -parameters 但 IDE 没勾选保留参数名导致 Spring 只能通过反射获取参数名拿到的是 arg0。Java 8 之后JDK 默认不会在字节码中保存方法参数名所以很多工程会用 -parameters 编译选项。如果没开Spring 会退化成用注解或调试信息来解析。建议在 pom.xml 里显式配置 -parameters同时配合 RequestParam、PathVariable 等注解明确名字避免这类问题。4.3 视图渲染与响应问题这类问题主要出现在服务端渲染项目中。经典症状是返回的视图名正确页面却 404或者页面渲染出来了但拿到模型数据是空还有一种更常见的用 Controller 没有 ResponseBody返回 JSON结果得到的是视图名对应的页面前端解析 JSON 时直接报错。排查视图渲染 404建议第一步看 ViewResolver 的 prefix 和 suffix 是否配置正确。InternalResourceViewResolver 的 prefix 填错比如少了开头的斜杠或者目录和真实结构不一致就会转发到一个不存在的路径。第二步看 ViewResolver 的顺序如果配置了多个解析器顺序不对前面的解析器可能把视图名当成不可解析的逻辑名直接抛异常。第三步看模型数据是否在渲染时仍然有效比如用了重定向 redirect: 前缀模型数据如果不是用 RedirectAttributes 传递到下一次请求自然就丢失了。还有响应乱码这类问题在纯 JSON 项目里不常见但在老 JSP 项目里很常见。乱码一般源于编码不一致请求本身是 UTF-8响应却按 ISO-8859-1 输出。如果项目整体都是 UTF-8又出现部分接口乱码往往是某个过滤器或拦截器在输出响应前改了字符编码。还有一种情况是 ResponseBody 中文被转码成了 ? 号这个问题多半是映射层的编码配置不对。建议在配置中统一指定 server.servlet.encoding.forcetrue以及为 HttpMessageConverter 设置 UTF-8 字符集从根上解决。4.4 面试追问备考清单既然题目是面试场景这里我把高频追问整理了一下。面试官不会只问“流程是什么”更可能接着问“为什么这里要这么做”“某个环节是否可替换”“出现异常如何流转”。我把自己比较认可的答法要点列一下为什么需要 HandlerAdapter直接把 Controller 方法拿来反射调用不行吗 答因为 Spring MVC 的方法形态非常丰富RequestBody、PathVariable、Callable、StreamingResponseBody 等都需要不同的处理逻辑Adapter 把这些差异收束到统一的执行接口上便于扩展和维护。HandlerMapping 匹配失败的是怎么处理的 答返回 null继续下一个 HandlerMapping。全部没有匹配到时抛出 NoHandlerFoundException这个异常只有配置了 throwExceptionIfNoHandlerFound 时才会明确抛出否则可能直接返回 404 页面。拦截器 preHandle 返回 false 之后会做什么 答不会继续执行后续拦截器和目标方法但会执行前面已经创建好的拦截器的 afterCompletion 回调中已完成 preHandle 的部分。注意afterCompletion 里有 try-catch它主要用于清理资源。ControllerAdvice 和 HandlerExceptionResolver 的关系是什么 答ControllerAdvice 最终会注册为 ExceptionHandlerExceptionResolver 的一个全局配置通过它可以把不同异常分发到对应的方法处理并支持返回视图或响应体。异步请求 DeferredResult 和 Callable 在流程上有什么差别 答Callable 会让 Spring MVC 把请求提交到任务执行器异步计算DeferredResult 则允许业务线程自己决定何时设置结果。流程上都会从 DispatcherServlet 回归到容器线程做响应但中间会释放 Tomcat 工作线程。ResponseBody 和 ResponseEntity 有什么区别 答ResponseBody 表示把返回值写进响应体ResponseEntity 可以同时控制 HTTP 状态码、响应头和响应体。两者在返回值处理器上走的是不同的处理器实现ResponseEntity 还要额外处理 header。这类问题答顺了会让面试官觉得不是死记硬背而是真正写过多轮请求链路代码、排查过相关故障的人。我建议在回答时搭配一两个自己真正调试过的小例子比如“我遇到过 preHandle 返回 false 导致接口无响应的问题从日志里看到业务方法没执行一路排查才定位到拦截器逻辑”。这种细节比任何理论都有说服力。最后再分享一个小技巧读源码时不要只看一个版本建议拿 Spring Framework 5.3.x 和 Spring Boot 2.7.x 的组合起步这套组合的源码在本地加断点非常顺。等把 doDispatch 的路径看熟了再去看 Spring Boot 3 基于 jakarta.servlet 的变化你会发现核心设计并没有变变的只是包名和个别参数类型。这也是面试时能自信说“我基于源码验证过”的基础。