【Spring】面试突击系列(三):Spring Web MVC 深度解析

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

目录

一、面试考点速览

考点 面试频率 难度 核心考察点
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 方法)
注册方式 @WebFilterFilterRegistrationBean 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 的请求处理流程是怎样的?

频次 :⭐⭐⭐⭐⭐

难度:中级

标准回答

  1. 请求到达 DispatcherServletdoDispatch() 方法
  2. 调用 getHandler() 遍历 HandlerMapping 列表,根据 URL 找到匹配的 Handler(@RequestMapping 对应的方法)
  3. 调用 getHandlerAdapter() 找到支持该 Handler 的 HandlerAdapterRequestMappingHandlerAdapter
  4. 执行拦截器的 preHandle()(正序),任一返回 false 则终止
  5. HandlerAdapter.handle() 真正执行 Controller 方法:
    • 参数解析(HandlerMethodArgumentResolver
    • 反射调用
    • 返回值处理(HandlerMethodReturnValueHandler
  6. 执行拦截器的 postHandle()(逆序)
  7. processDispatchResult() 处理结果:
    • 正常:@ResponseBodyHttpMessageConverter 序列化为 JSON
    • 异常:HandlerExceptionResolver 链处理
  8. 执行拦截器的 afterCompletion()(逆序)
  9. 响应返回客户端

加分回答 :能说出 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 支持异步请求吗?原理是什么?

→ 支持。通过 CallableDeferredResult 返回值。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 + @ServletComponentScanFilterRegistrationBean 注册到 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 类型的参数?

→ 通过 ConverterFormatter。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 层

十、本章总结

核心要点回顾

  1. 前端控制器模式DispatcherServlet 是 Spring MVC 的统一入口,内部维护九大组件
  2. 12 步请求处理流程doDispatch() → HandlerMapping → HandlerAdapter → 拦截器链 → 参数解析 → 反射调用 → 返回值处理 → 异常处理 → 响应返回
  3. Filter 先于 Interceptor:Filter 在 Servlet 容器层,Interceptor 在 Spring 层
  4. 参数绑定三剑客@RequestParam(查询参数)、@PathVariable(路径变量)、@RequestBody(请求体 JSON)
  5. HttpMessageConverter@RequestBody@ResponseBody 的底层支撑,默认使用 Jackson
  6. RESTful 设计:资源导向、HTTP 方法语义、统一异常处理

面试 checklist

  • 能画出 DispatcherServlet 的 12 步请求处理流程图
  • 能说清 Filter 和 Interceptor 的 5 个核心区别
  • 能解释 @RequestParam / @PathVariable / @RequestBody 的数据来源
  • 能写出统一异常处理的 @ControllerAdvice 代码
  • 能设计符合 RESTful 规范的 API URL
  • 能回答 HttpMessageConverter 的选择机制

下一步学习建议

  1. 动手实践:基于 EduLearn 课程模块,实现完整的 CRUD + 搜索 + 下单接口
  2. 深入源码 :阅读 DispatcherServlet.doDispatch()RequestMappingHandlerAdapter.handleInternal() 的完整源码
  3. 扩展知识 :了解 Spring MVC 的异步请求(Callable / DeferredResult)和 WebFlux 响应式编程

下期速递 :第 4 期《Spring AOP 深度解析》,将深入 JDK 动态代理与 CGLIB 的区别、切点表达式、AOP 失效场景与避坑指南。


上期速递【Spring】面试突击系列(二):SpringBoot 入门与自动配置原理

相关推荐
colofullove2 小时前
小说上传中心与异步处理进度展示设计
前端
Marst Code2 小时前
⚙️ 2026 年推荐技术方案
前端
qq_366086222 小时前
测试接口传参数时,放在Header和Body中后台接收参数的区别
java·开发语言·前端
biubiubiu07062 小时前
SpringBoot 3.5.4 整合Quartz 定时任务
java·spring boot·spring
li星野2 小时前
FAISS 详解:原理、使用与面试指南——向量检索的基石
面试·职场和发展·faiss
whatever who cares2 小时前
Vue3中vue文件和composables的分工
前端·javascript·vue.js
袋鼠云数栈UED团队2 小时前
基于 superpowers 实现复杂前端改造
前端
袋鼠云数栈前端2 小时前
基于 superpowers 实现复杂前端改造
前端·ai
sugar__salt2 小时前
LLM服务HTTP接口实战:前端HTTP请求全解与项目落地
前端·网络协议·http