Spring Web MVC 深度解析
学习目标 :掌握 DispatcherServlet 请求处理全流程,能清晰回答「一个 HTTP 请求在 Spring MVC 中经历了什么」
前置知识 :Spring IoC/DI(第1期)、SpringBoot 自动配置(第2期)
技术栈:SpringBoot 2.7.x + Spring MVC 5.3.x + JDK 11/17
目录
- 一、面试考点速览
- [二、Spring MVC 核心架构](#二、Spring MVC 核心架构)
- [三、DispatcherServlet 请求处理全流程](#三、DispatcherServlet 请求处理全流程)
- 四、拦截器与过滤器
- 五、参数绑定与返回值处理
- [六、RESTful API 设计最佳实践](#六、RESTful API 设计最佳实践)
- [七、EduLearn 实战:课程搜索与下单接口](#七、EduLearn 实战:课程搜索与下单接口)
- 八、面试真题解析
- 九、面试追问链设计
- 十、本章总结

一、面试考点速览
| 考点 | 面试频率 | 难度 | 核心考察点 |
|---|---|---|---|
| DispatcherServlet 请求处理流程 | ⭐⭐⭐⭐⭐ | 中级 | 12 步完整链路、各组件职责 |
| 拦截器 vs 过滤器 | ⭐⭐⭐⭐⭐ | 初级 | 执行顺序、适用场景、注册方式 |
| RESTful API 设计 | ⭐⭐⭐⭐ | 初级 | 资源命名、HTTP 方法语义、状态码 |
| 参数绑定机制 | ⭐⭐⭐⭐ | 中级 | @RequestParam/@PathVariable/@RequestBody 区别 |
| 统一异常处理 | ⭐⭐⭐ | 中级 | @ControllerAdvice + @ExceptionHandler |
| HttpMessageConverter | ⭐⭐⭐ | 高级 | JSON/XML 序列化原理、自定义转换器 |
本期核心线索 :从一个 HTTP 请求到达 DispatcherServlet 开始,追踪它如何经过 HandlerMapping、HandlerAdapter、拦截器链、参数解析、返回值处理,最终变成 HTTP 响应返回给客户端。
二、Spring MVC 核心架构
2.1 前端控制器模式
Spring MVC 的核心设计模式是前端控制器(Front Controller) 。所有请求都先到达一个统一的入口------DispatcherServlet,由它分发给具体的处理器。
客户端请求
│
▼
DispatcherServlet(前端控制器)
│
├──→ HandlerMapping(处理器映射) → 找到哪个 Controller 处理
├──→ HandlerAdapter(处理器适配器) → 调用 Controller 方法
├──→ HandlerInterceptor(拦截器) → 前置/后置处理
├──→ ViewResolver(视图解析器) → 渲染视图(或 JSON)
└──→ HttpMessageConverter(消息转换器)→ 请求/响应体转换
为什么用前端控制器?
| 对比维度 | 传统 Servlet | Spring MVC |
|---|---|---|
| 入口 | 每个 Servlet 一个 URL 映射 | 统一入口 DispatcherServlet |
| 参数提取 | 手动 request.getParameter() |
自动绑定到方法参数 |
| 返回值处理 | 手动 response.getWriter().write() |
自动序列化为 JSON/XML |
| 异常处理 | 每个 Servlet 各自 try-catch | 全局 @ControllerAdvice |
| 拦截器 | 需要 Filter 实现 | 内置 HandlerInterceptor |
2.2 DispatcherServlet 在容器中的位置
java
// DispatcherServlet 继承链
HttpServlet
→ HttpServletBean
→ FrameworkServlet
→ DispatcherServlet
DispatcherServlet 本质上就是一个 Servlet 。在 SpringBoot 中,它由 DispatcherServletAutoConfiguration 自动配置并注册到内嵌 Tomcat 中(回顾第 2 期的自动配置原理)。
关键初始化方法:
java
// DispatcherServlet 初始化时加载的九大组件
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context); // 文件上传解析器
initLocaleResolver(context); // 国际化解析器
initThemeResolver(context); // 主题解析器
initHandlerMappings(context); // ★ 处理器映射器
initHandlerAdapters(context); // ★ 处理器适配器
initHandlerExceptionResolvers(context);// ★ 异常解析器
initRequestToViewNameTranslator(context);
initViewResolvers(context); // 视图解析器
initFlashMapManager(context);
}
面试回答模板:
"Spring MVC 采用前端控制器模式,
DispatcherServlet作为统一入口接收所有请求。它内部维护了九大组件(HandlerMapping、HandlerAdapter、ViewResolver 等),在初始化时从 Spring 容器中加载。请求到达后,DispatcherServlet 按固定流程依次调用这些组件完成请求处理。"
三、DispatcherServlet 请求处理全流程
这是本期最核心的章节。面试官问"一个请求在 Spring MVC 中经历了什么",你要能从头到尾讲清楚。
3.1 完整 12 步流程
① 请求到达 DispatcherServlet.doDispatch()
│
▼
② 检查是否为文件上传请求(checkMultipart)
│
▼
③ getHandler():遍历 HandlerMapping 列表,找到匹配的 Handler
│ └── RequestMappingHandlerMapping 根据 @RequestMapping 匹配
│
▼
④ getHandlerAdapter():找到支持该 Handler 的 HandlerAdapter
│ └── RequestMappingHandlerAdapter 处理 @Controller 方法
│
▼
⑤ 执行拦截器 preHandle()(正序)
│ └── 任一返回 false → 请求终止,执行 afterCompletion()
│
▼
⑥ handle():HandlerAdapter 调用真正的 Controller 方法
│ ├── 参数解析(HandlerMethodArgumentResolver)
│ │ ├── @RequestParam → RequestParamMethodArgumentResolver
│ │ ├── @PathVariable → PathVariableMethodArgumentResolver
│ │ ├── @RequestBody → RequestResponseBodyMethodProcessor
│ │ └── 自定义参数 → 自定义 HandlerMethodArgumentResolver
│ ├── 数据类型转换(WebDataBinder / Converter)
│ ├── 数据校验(@Valid / @Validated → Validator)
│ └── 反射调用 Controller 方法
│
▼
⑦ 获取返回值(ModelAndView 或 @ResponseBody 的返回值)
│
▼
⑧ 执行拦截器 postHandle()(逆序)
│
▼
⑨ processDispatchResult():处理返回值
│ ├── 有异常 → 进入异常解析器链(HandlerExceptionResolver)
│ │ └── @ExceptionHandler → @ControllerAdvice → DefaultHandlerExceptionResolver
│ └── 正常返回
│ ├── @ResponseBody → HttpMessageConverter 序列化为 JSON
│ └── 视图名 → ViewResolver 解析 → 渲染视图
│
▼
⑩ 执行拦截器 afterCompletion()(逆序)
│
▼
⑪ 触发 RequestHandledEvent(可选)
│
▼
⑫ 响应返回给客户端
3.2 核心源码走读
入口:doDispatch()
java
// DispatcherServlet.java(简化)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response)
throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
try {
ModelAndView mv = null;
Exception dispatchException = null;
// Step 1: 检查文件上传
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Step 2: 获取 Handler(HandlerMapping 匹配)
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response); // 404
return;
}
// Step 3: 获取 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Step 4: 拦截器 preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return; // preHandle 返回 false,直接终止
}
// Step 5: 真正执行业务方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// Step 6: 拦截器 postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception ex) {
dispatchException = ex;
}
// Step 7: 处理返回结果(含异常处理)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
getHandler():HandlerMapping 匹配
java
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler; // 返回第一个匹配的
}
}
}
return null;
}
SpringBoot 默认注册了两个 HandlerMapping:
| HandlerMapping | 作用 |
|---|---|
RequestMappingHandlerMapping |
匹配 @RequestMapping 注解的方法 |
BeanNameUrlHandlerMapping |
匹配 Bean 名称以 "/" 开头的 Bean |
handle():HandlerAdapter 执行
java
// RequestMappingHandlerAdapter.handleInternal()(简化)
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// 1. 调用 ServletInvocableHandlerMethod.invokeAndHandle()
// 内部执行:
// a) 参数解析:遍历 argumentResolvers,找到支持当前参数的解析器
// b) 反射调用:method.invoke(bean, args)
// c) 返回值处理:遍历 returnValueHandlers,找到支持返回值的处理器
// - @ResponseBody → RequestResponseBodyMethodProcessor
// → HttpMessageConverter.write() → JSON 写入 response
invocableMethod.invokeAndHandle(webRequest, mavContainer);
return getModelAndView(mavContainer, modelFactory, webRequest);
}
3.3 关键组件职责速查表
| 组件 | 接口 | 默认实现 | 职责 |
|---|---|---|---|
| HandlerMapping | HandlerMapping |
RequestMappingHandlerMapping |
根据请求 URL 找到处理器 |
| HandlerAdapter | HandlerAdapter |
RequestMappingHandlerAdapter |
调用处理器方法 |
| HandlerInterceptor | HandlerInterceptor |
自定义 | 前置/后置/完成拦截 |
| HandlerMethodArgumentResolver | HandlerMethodArgumentResolver |
26 个内置实现 | 解析方法参数 |
| HandlerMethodReturnValueHandler | HandlerMethodReturnValueHandler |
15 个内置实现 | 处理返回值 |
| HttpMessageConverter | HttpMessageConverter |
MappingJackson2HttpMessageConverter |
请求/响应体与对象互转 |
| HandlerExceptionResolver | HandlerExceptionResolver |
ExceptionHandlerExceptionResolver |
异常处理 |
四、拦截器与过滤器
4.1 执行顺序对比
这是面试中的高频对比题。先看一张图:

执行顺序:
请求到达
→ Filter1.doFilter() 前置
→ Filter2.doFilter() 前置
→ DispatcherServlet
→ Interceptor1.preHandle()
→ Interceptor2.preHandle()
→ Controller.method()
→ Interceptor2.postHandle()
→ Interceptor1.postHandle()
→ 视图渲染 / JSON 序列化
→ Interceptor2.afterCompletion()
→ Interceptor1.afterCompletion()
→ Filter2.doFilter() 后置
→ Filter1.doFilter() 后置
4.2 核心区别
| 对比维度 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 归属 | Servlet 规范(javax.servlet) | Spring 框架(org.springframework) |
| 容器 | Servlet 容器管理 | Spring IoC 容器管理 |
| 作用范围 | 所有请求(包括静态资源) | 仅 DispatcherServlet 处理的请求 |
| 执行时机 | 进入 Servlet 之前 / 之后 | Handler 执行前 / 后 / 完成 |
| 依赖注入 | 不能直接注入 Spring Bean(需额外配置) | 可以直接注入 Spring Bean |
| 细粒度 | 只能拿到 request/response | 可以拿到 Handler(哪个 Controller 方法) |
| 注册方式 | @WebFilter 或 FilterRegistrationBean |
addInterceptors(InterceptorRegistry) |
4.3 拦截器实战
java
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 1. 可以拿到 Handler,判断是否是特定 Controller
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
// 检查方法上是否有 @RequireLogin 注解
if (hm.hasMethodAnnotation(RequireLogin.class)) {
String token = request.getHeader("Authorization");
if (token == null) {
throw new UnauthorizedException("请先登录");
}
// 验证 token...
}
}
return true; // true=继续,false=终止
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
// Controller 执行后,视图渲染前
// 可以修改 ModelAndView
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
// 视图渲染完成后(或异常发生后)
// 适合做资源清理、日志记录
}
}
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/login", "/api/register") // 排除路径
.order(1); // 执行顺序
}
}
4.4 适用场景速查
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 字符编码设置 | Filter | 需要在最外层设置 |
| CORS 跨域处理 | Filter | 需要在进入 Spring 前处理 |
| 登录鉴权 | Interceptor | 可以注入 Service,可以拿到 Handler |
| 操作日志 | Interceptor | 可以拿到方法名、参数 |
| 性能监控 | Interceptor | preHandle 记录开始时间,afterCompletion 计算耗时 |
| XSS 过滤 | Filter | 需要包装 request 对象 |
五、参数绑定与返回值处理
5.1 参数绑定注解对比
| 注解 | 数据来源 | 示例 | 适用场景 |
|---|---|---|---|
@RequestParam |
URL 查询参数或表单 | ?name=Java |
GET 请求参数、表单提交 |
@PathVariable |
URL 路径变量 | /course/{id} |
RESTful 资源定位 |
@RequestBody |
HTTP 请求体(JSON) | {"name":"Java"} |
POST/PUT JSON 请求 |
@RequestHeader |
HTTP 请求头 | Authorization: Bearer xxx |
获取 Token 等头信息 |
@CookieValue |
Cookie | JSESSIONID=xxx |
获取 Cookie 值 |
@ModelAttribute |
请求参数绑定到对象 | ?name=Java&price=99 |
表单对象绑定 |
5.2 HttpMessageConverter 原理
@RequestBody 和 @ResponseBody 的背后是 HttpMessageConverter:
java
// HttpMessageConverter 接口
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, MediaType mediaType); // 能否反序列化
boolean canWrite(Class<?> clazz, MediaType mediaType); // 能否序列化
T read(Class<? extends T> clazz, HttpInputMessage inputMessage); // 请求体 → 对象
void write(T t, MediaType contentType, HttpOutputMessage outputMessage); // 对象 → 响应体
}
SpringBoot 默认注册的 HttpMessageConverter:
| 实现类 | 支持的 MediaType | 作用 |
|---|---|---|
MappingJackson2HttpMessageConverter |
application/json |
JSON ↔ 对象(Jackson) |
StringHttpMessageConverter |
text/plain |
String ↔ 文本 |
ByteArrayHttpMessageConverter |
*/* |
byte\[\] ↔ 二进制 |
FormHttpMessageConverter |
application/x-www-form-urlencoded |
表单数据 |
面试回答模板:
"
@RequestBody读取请求体时,Spring MVC 遍历所有HttpMessageConverter,找到第一个canRead()返回 true 的转换器来反序列化。@ResponseBody同理,遍历找到第一个canWrite()返回 true 的转换器来序列化。默认使用 Jackson 处理 JSON。"
5.3 统一返回格式设计
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private int code; // 状态码:200 成功,4xx 客户端错误,5xx 服务端错误
private String message; // 提示信息
private T data; // 响应数据
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
}
六、RESTful API 设计最佳实践
6.1 资源命名规范
| 操作 | 方法 | URL | 说明 |
|---|---|---|---|
| 查询所有课程 | GET | /api/courses |
复数名词 |
| 查询单个课程 | GET | /api/courses/{id} |
资源 ID 在路径中 |
| 创建课程 | POST | /api/courses |
请求体包含数据 |
| 更新课程 | PUT | /api/courses/{id} |
全量更新 |
| 部分更新 | PATCH | /api/courses/{id} |
部分字段更新 |
| 删除课程 | DELETE | /api/courses/{id} |
逻辑删除 |
| 课程下的评论 | GET | /api/courses/{id}/comments |
子资源 |
命名原则:
- 使用名词复数(
/courses而非/getCourses) - 层级不超过 3 层(
/courses/{id}/chapters/{cid}/lessons是上限) - 使用连字符而非下划线(
/course-categories而非/course_categories)
6.2 HTTP 状态码选择
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 OK | 成功 | GET、PUT、PATCH 成功 |
| 201 Created | 已创建 | POST 创建资源成功 |
| 204 No Content | 无内容 | DELETE 成功 |
| 400 Bad Request | 请求错误 | 参数校验失败 |
| 401 Unauthorized | 未认证 | 未登录或 Token 过期 |
| 403 Forbidden | 无权限 | 已登录但权限不足 |
| 404 Not Found | 未找到 | 资源不存在 |
| 409 Conflict | 冲突 | 资源状态冲突(如重复下单) |
| 500 Internal Server Error | 服务器错误 | 未知异常 |
6.3 统一异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(400, msg);
}
@ExceptionHandler(CourseNotFoundException.class)
public Result<Void> handleCourseNotFound(CourseNotFoundException e) {
return Result.error(404, e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleUnknown(Exception e) {
log.error("未知异常", e);
return Result.error(500, "服务器内部错误");
}
}
七、EduLearn 实战:课程搜索与下单接口
7.1 课程搜索接口
java
@RestController
@RequestMapping("/api/courses")
public class CourseController {
@Autowired
private CourseService courseService;
/**
* 多条件搜索课程
* GET /api/courses?keyword=Java&category=backend&minPrice=0&maxPrice=100&page=1&size=10&sort=price,asc
*/
@GetMapping
public Result<PageResult<CourseVO>> search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createTime,desc") String sort) {
CourseQuery query = CourseQuery.builder()
.keyword(keyword)
.category(category)
.minPrice(minPrice)
.maxPrice(maxPrice)
.page(page)
.size(size)
.sort(sort)
.build();
PageResult<CourseVO> result = courseService.search(query);
return Result.success(result);
}
/**
* 查询课程详情
* GET /api/courses/123
*/
@GetMapping("/{id}")
public Result<CourseDetailVO> getById(@PathVariable Long id) {
CourseDetailVO course = courseService.getDetailById(id);
return Result.success(course);
}
}
7.2 课程下单接口
java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
* POST /api/orders
*/
@PostMapping
public Result<OrderVO> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
OrderVO order = orderService.createOrder(request);
return Result.success(order);
}
}
// 请求 DTO
@Data
public class CreateOrderRequest {
@NotNull(message = "课程ID不能为空")
private Long courseId;
@NotNull(message = "用户ID不能为空")
private Long userId;
@Min(value = 1, message = "购买数量至少为1")
@Max(value = 10, message = "购买数量不能超过10")
private Integer quantity = 1;
}
java
@Service
public class OrderService {
public OrderVO createOrder(CreateOrderRequest request) {
// 1. 查询课程
Course course = courseMapper.selectById(request.getCourseId());
if (course == null) {
throw new CourseNotFoundException("课程不存在: " + request.getCourseId());
}
// 2. 检查库存
if (course.getStock() < request.getQuantity()) {
throw new BusinessException(409, "库存不足,剩余: " + course.getStock());
}
// 3. 创建订单
Order order = new Order();
order.setCourseId(request.getCourseId());
order.setUserId(request.getUserId());
order.setQuantity(request.getQuantity());
order.setTotalAmount(course.getPrice().multiply(
BigDecimal.valueOf(request.getQuantity())));
order.setStatus(OrderStatus.PENDING);
orderMapper.insert(order);
// 4. 扣减库存
courseMapper.decreaseStock(request.getCourseId(), request.getQuantity());
return OrderVO.from(order);
}
}
7.3 统一响应结构

八、面试真题解析
真题 1:Spring MVC 的请求处理流程是怎样的?
频次 :⭐⭐⭐⭐⭐
难度:中级
标准回答:
- 请求到达
DispatcherServlet的doDispatch()方法 - 调用
getHandler()遍历HandlerMapping列表,根据 URL 找到匹配的 Handler(@RequestMapping对应的方法) - 调用
getHandlerAdapter()找到支持该 Handler 的HandlerAdapter(RequestMappingHandlerAdapter) - 执行拦截器的
preHandle()(正序),任一返回 false 则终止 HandlerAdapter.handle()真正执行 Controller 方法:- 参数解析(
HandlerMethodArgumentResolver) - 反射调用
- 返回值处理(
HandlerMethodReturnValueHandler)
- 参数解析(
- 执行拦截器的
postHandle()(逆序) processDispatchResult()处理结果:- 正常:
@ResponseBody→HttpMessageConverter序列化为 JSON - 异常:
HandlerExceptionResolver链处理
- 正常:
- 执行拦截器的
afterCompletion()(逆序) - 响应返回客户端
加分回答 :能说出 DispatcherServlet 的九大组件初始化策略,以及 RequestMappingHandlerMapping 在容器启动时扫描所有 @Controller 和 @RequestMapping 注解建立映射表。
真题 2:拦截器和过滤器有什么区别?
频次 :⭐⭐⭐⭐⭐
难度:初级
标准回答:
| 维度 | Filter | Interceptor |
|---|---|---|
| 规范 | Servlet 规范 | Spring 框架 |
| 容器 | Servlet 容器管理 | Spring IoC 管理 |
| 范围 | 所有请求 | 仅 DispatcherServlet 处理的请求 |
| 依赖注入 | 不能直接注入 Bean | 可以直接注入 Bean |
| 细粒度 | 只能拿到 request/response | 可以拿到 Handler 对象 |
| 生命周期 | init → doFilter → destroy | preHandle → postHandle → afterCompletion |
加分回答 :Filter 基于回调函数 ,Interceptor 基于AOP 思想 和反射机制。Filter 在进入 Servlet 之前执行,Interceptor 在进入 Handler 之前执行,所以 Filter 先于 Interceptor。
真题 3:@RequestParam 和 @RequestBody 的区别?
频次 :⭐⭐⭐⭐
难度:初级
标准回答:
@RequestParam:从 URL 查询参数(?key=value)或表单数据(application/x-www-form-urlencoded)中获取参数值@RequestBody:从 HTTP 请求体中获取数据,通常用于接收 JSON 格式(application/json),由HttpMessageConverter反序列化为 Java 对象@PathVariable:从 URL 路径中提取变量(/api/courses/{id})
加分回答 :@RequestBody 底层依赖 HttpMessageConverter,默认使用 Jackson 的 ObjectMapper 进行 JSON 反序列化。一个请求只能有一个 @RequestBody 参数(因为请求体只能读一次),但可以有多个 @RequestParam。
九、面试追问链设计
9.1 DispatcherServlet 请求处理流程(5 层追问)
第一层 :Spring MVC 的请求处理流程是怎样的?
→ 12 步完整链路。详见 [三、DispatcherServlet 请求处理全流程](#三、DispatcherServlet 请求处理全流程)
第二层 :HandlerMapping 是怎么找到 Controller 的?
→ RequestMappingHandlerMapping 在容器初始化时(afterPropertiesSet()),扫描所有 @Controller 注解的 Bean,解析 @RequestMapping 注解,建立 URL Pattern → HandlerMethod 的映射表(MappingRegistry)。请求到达时,遍历所有 HandlerMapping,调用 getHandler(request),内部用 AntPathMatcher 做 URL 匹配。
第三层 :HandlerAdapter 为什么需要适配器模式?
→ 因为 Spring MVC 支持多种 Handler 类型:@Controller 方法(HandlerMethod)、HttpRequestHandler 接口、Servlet 等。每种 Handler 的调用方式不同,HandlerAdapter 通过适配器模式统一调用接口。supports(handler) 判断是否支持,handle() 执行调用。
第四层 :HttpMessageConverter 是怎么选出来的?
→ RequestResponseBodyMethodProcessor 处理 @ResponseBody 返回值时,遍历所有注册的 HttpMessageConverter,调用 canWrite(type, mediaType) 判断。第一个返回 true 的转换器被使用。默认 MappingJackson2HttpMessageConverter 处理 JSON,StringHttpMessageConverter 处理 String。
第五层 :Spring MVC 支持异步请求吗?原理是什么?
→ 支持。通过 Callable 或 DeferredResult 返回值。WebAsyncManager 管理异步处理:主线程释放回 Servlet 容器,异步任务在独立线程执行,完成后通过 AsyncListener 回调将结果写回响应。底层依赖 Servlet 3.0 的 AsyncContext。
9.2 拦截器 vs 过滤器(4 层追问)
第一层 :拦截器和过滤器有什么区别?
→ 详见 八、面试真题解析-真题2
第二层 :它们的执行顺序是怎样的?
→ Filter → Interceptor。Filter 在 Servlet 容器层执行,Interceptor 在 Spring 层执行。请求先经过 Filter 链,到达 DispatcherServlet 后再经过 Interceptor 链。详见 四、拦截器与过滤器
第三层 :如果 preHandle 返回 false,后续的拦截器和过滤器会怎样?
→ 当前拦截器的 afterCompletion() 会被调用(仅限已执行过 preHandle 的拦截器),但后续拦截器的 preHandle()、postHandle() 都不会执行。Controller 方法也不会执行。Filter 链不受影响------Filter 的 doFilter() 后置代码仍会执行(因为 Filter 在更外层)。
第四层 :Filter 和 Interceptor 的注册机制有什么不同?
→ Filter 通过 @WebFilter + @ServletComponentScan 或 FilterRegistrationBean 注册到 Servlet 容器。Interceptor 通过 WebMvcConfigurer.addInterceptors() 注册到 Spring MVC 的 InterceptorRegistry。Filter 的注册发生在 Servlet 容器启动时,Interceptor 的注册发生在 Spring 容器初始化时。
9.3 RESTful API 设计(3 层追问)
第一层 :RESTful API 的设计原则是什么?
→ 资源导向(URL 表示资源而非操作)、HTTP 方法语义(GET/POST/PUT/DELETE)、无状态、统一接口。详见 [六、RESTful API 设计最佳实践](#六、RESTful API 设计最佳实践)
第二层 :POST 和 PUT 的区别?
→ POST 用于创建资源(非幂等),PUT 用于全量更新资源(幂等)。POST /api/courses 创建课程,多次调用创建多个。PUT /api/courses/1 更新课程,多次调用结果相同。
第三层 :API 版本控制怎么做?
→ 三种方式:URL 路径(/api/v1/courses)、请求头(Accept: application/vnd.edulearn.v1+json)、请求参数(/api/courses?version=1)。URL 路径最直观常用,请求头最符合 REST 理念(不污染 URL)。
9.4 参数绑定(4 层追问)
第一层 :@RequestParam 和 @PathVariable 的区别?
→ @RequestParam 从查询参数获取(?id=1),@PathVariable 从 URL 路径获取(/courses/1)。详见 五、参数绑定与返回值处理
第二层 :Spring MVC 怎么把字符串 "123" 转成 Long 类型的参数?
→ 通过 Converter 或 Formatter。Spring 内置了 StringToNumberConverterFactory,将 String 转为各种数字类型。WebDataBinder 在绑定参数时自动调用合适的 Converter。
第三层 :如果我想自定义参数解析器,怎么做?
→ 实现 HandlerMethodArgumentResolver 接口,重写 supportsParameter() 和 resolveArgument(),然后通过 WebMvcConfigurer.addArgumentResolvers() 注册。
第四层 :@Valid 参数校验是怎么工作的?
→ RequestResponseBodyMethodProcessor 在处理 @RequestBody 参数时,检查参数上是否有 @Valid 或 @Validated 注解。如果有,调用 Validator(默认是 Hibernate Validator)进行校验。校验失败抛出 MethodArgumentNotValidException,由 @ControllerAdvice 统一处理。
面试追问链速查表
| 入口问题 | 追问方向 | 深度 |
|---|---|---|
| 请求处理流程? | → HandlerMapping 映射机制 → HandlerAdapter 适配器模式 → HttpMessageConverter 选择 → 异步请求原理 | 5 层 |
| 拦截器 vs 过滤器? | → 执行顺序 → preHandle=false 连锁反应 → 注册机制差异 | 4 层 |
| RESTful 设计? | → POST vs PUT → API 版本控制 | 3 层 |
| 参数绑定? | → Converter 类型转换 → 自定义参数解析器 → @Valid 校验链 | 4 层 |
十、本章总结
核心要点回顾
- 前端控制器模式 :
DispatcherServlet是 Spring MVC 的统一入口,内部维护九大组件 - 12 步请求处理流程 :
doDispatch()→ HandlerMapping → HandlerAdapter → 拦截器链 → 参数解析 → 反射调用 → 返回值处理 → 异常处理 → 响应返回 - Filter 先于 Interceptor:Filter 在 Servlet 容器层,Interceptor 在 Spring 层
- 参数绑定三剑客 :
@RequestParam(查询参数)、@PathVariable(路径变量)、@RequestBody(请求体 JSON) - HttpMessageConverter :
@RequestBody和@ResponseBody的底层支撑,默认使用 Jackson - RESTful 设计:资源导向、HTTP 方法语义、统一异常处理
面试 checklist
- 能画出 DispatcherServlet 的 12 步请求处理流程图
- 能说清 Filter 和 Interceptor 的 5 个核心区别
- 能解释 @RequestParam / @PathVariable / @RequestBody 的数据来源
- 能写出统一异常处理的 @ControllerAdvice 代码
- 能设计符合 RESTful 规范的 API URL
- 能回答 HttpMessageConverter 的选择机制
下一步学习建议
- 动手实践:基于 EduLearn 课程模块,实现完整的 CRUD + 搜索 + 下单接口
- 深入源码 :阅读
DispatcherServlet.doDispatch()和RequestMappingHandlerAdapter.handleInternal()的完整源码 - 扩展知识 :了解 Spring MVC 的异步请求(
Callable/DeferredResult)和 WebFlux 响应式编程
下期速递 :第 4 期《Spring AOP 深度解析》,将深入 JDK 动态代理与 CGLIB 的区别、切点表达式、AOP 失效场景与避坑指南。