← 返回 Java 后端知识路线
阶段 04Spring 与后端

Spring MVC 请求链:DispatcherServlet 到 JSON 响应

沿着一个文章详情请求穿过过滤器、DispatcherServlet、Controller、Service 和消息转换器。

第 27 / 35 篇
Spring MVCDispatcherServletControllerJSON

先看这一课值不值得学

学完后,你手里多了哪些代码积木

把 HTTP 请求映射到 Controller 参数,再把返回值写成响应。

本课正式新增

语法 / API / 命令你必须会到什么程度
@Controller / @RestController声明 MVC 处理器
@RequestMapping / @GetMapping把路径和方法映射到 Java 方法
@RequestParam / @PathVariable / @RequestBody从不同请求位置绑定参数
ResponseEntity<T>显式返回状态码、响应头和正文

本课只借用,先别硬背

  • 参数校验和全局异常在第 28、30 课

学完必须能独立写

  • 实现文章查询和创建接口
  • 沿 DispatcherServlet 调用链定位 404/400/500
本课目录
  1. 1. 现实问题:一个 404 可能发生在十几个位置
  2. 2. 最小可运行示例:清晰的输入与输出类型
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:用分层测试验证调用链
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:一个 404 可能发生在十几个位置

请求路径没匹配、Controller 没扫描、参数类型转换失败、Service 抛异常、返回对象不能序列化,都可能被用户概括成“接口不通”。Spring MVC 的学习重点是把一条请求从网络边界追到方法返回,而不是只记 @GetMapping 的拼法。

我们做一个文章详情 Controller,路径参数进入方法,Service 返回 DTO,@RestController 让返回值成为响应体。随后按调用链列出每一层能观察到的对象。

2. 最小可运行示例:清晰的输入与输出类型

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ArticleController {
    private final ArticleService service;

    ArticleController(ArticleService service) { this.service = service; }

    @GetMapping("/api/articles/{id}")
    ArticleView find(@PathVariable long id) {
        return service.find(id);
    }
}

record ArticleView(long id, String title, String status) {}

在 Boot 4.1 中,Spring MVC 根据类路径和自动配置注册 DispatcherServlet、HandlerMapping、HandlerAdapter 和 JSON HttpMessageConverter。示例省略 Service 实现,只展示 Controller 的边界;long 转换失败时会在进入方法前产生参数错误,不应由 Service 再解析一次。

3. 调用链与对象变化

请求先经过 Servlet Filter,进入 DispatcherServlet。HandlerMapping 根据 HTTP 方法和路径找到 HandlerMethod,HandlerAdapter 准备参数:PathVariable 的字符串被转换成 long,其他参数可能来自 query、header 或 body。Controller 调用 Service,拿到 ArticleView 记录。

返回值离开 Controller 后,HandlerAdapter 选择消息转换器,根据 Accept 和 Content-Type 把 ArticleView 读取成字段,再编码为 JSON;同时设置状态码和响应头。若任一步异常,异常解析器链决定返回哪个错误响应,业务方法可能根本没有执行。

4. 为什么这样设计

DispatcherServlet 把协议分发与业务方法隔离,HandlerMapping 负责路由,转换器负责表示格式,Controller 只组合输入和业务。这样可以替换 JSON、增加校验或统一异常,而不让每个方法重复解析。分层不是为了增加文件数量,而是把变化和错误定位到合理边界。

Controller 返回 DTO 而不是数据库实体,避免暴露内部字段、懒加载和可变状态。路径参数、查询参数和请求体各自有语义,不能因为都能写成 String 就混在一起。状态码和响应结构要通过测试锁定。

5. 项目落点:用分层测试验证调用链

MockMvc 测试可以验证路由、状态码、JSON 字段和校验错误;Service 单测使用 fake Repository;Repository 集成测试验证 SQL。日志里记录 handler、耗时、status 和 trace id,参数只记录安全摘要。统一异常处理把 MethodArgumentTypeMismatch、校验失败和领域异常变成稳定错误码。

练习:为详情接口增加 ?includeTags=true,比较 query 参数为 null、true、false 时 Service 调用;再写不存在 id、非法 id、Accept 不支持和 Service 抛异常的测试,确认每种情况在哪一层结束。

6. 易错排查

  • 404:检查方法、路径、context path、组件扫描和代理前缀,先看映射日志。
  • 415:请求 Content-Type 与消息转换器不匹配,确认客户端发送 JSON。
  • 400:参数类型或 body 绑定失败,读取字段路径和原始请求摘要。
  • 返回 JSON 字段缺失:检查 DTO 访问器、Jackson 配置、响应实际类型和视图过滤。

7. 一页复习

请求链是 Filter → DispatcherServlet → HandlerMapping/Adapter → 参数绑定 → Controller → Service → 返回值转换 → 响应。每一层都改变了数据形态:字节、字符串、参数、DTO、领域结果、JSON。用请求日志和层级测试找第一次偏离,不要从最后一层猜。

把请求链落到一个具体输入:字节流包含 /api/articles/42 和 Accept JSON,HandlerMapping 输出 HandlerMethod,参数解析器把 "42" 变成 long,Service 输出 ArticleView,消息转换器把记录字段写成 JSON。若 42 不是数字,失败发生在 Controller 执行前;若数据库查不到,失败发生在 Service;若 DTO 不能序列化,失败发生在返回阶段。

项目文件按层落点:web/article/ArticleController 只声明 HTTP,application/article/ArticleService 读公开规则,persistence/article/ArticleMapper 读数据库,web/error/ApiExceptionHandler 统一翻译。MockMvc 可以只启动 Web 层,Service 单测不启动 MVC,集成测试再验证真实 Bean 和数据库。这样一个 404 的定位不会依赖猜测。

排错时 404 查 mapping、context path 和方法;400 查 converter、路径类型、JSON 字段和校验;415 查 Content-Type/Accept;500 查 Service 异常、序列化和异常解析器。Controller 方法没被调用时不要改 SQL;Service 已返回正确对象但响应字段错时不要改 Repository。每层打印一次安全的输入/输出摘要即可,避免重复全文日志。

练习是增加列表 query 参数和 includeTags 开关,写四个 MockMvc 测试:成功、非法参数、不存在和不支持的 Accept;再用日志标出 HandlerMethod、Service 耗时和响应状态。将同一请求用 curl 和浏览器发送,比较它们的头差异,解释为什么 CORS 只在浏览器脚本场景出现。

MVC 链中的每次转换都值得命名:网络字节先成为 ServletRequest,路径字符串成为 path variable,JSON body 成为 DTO,DTO 成为 Service 输入,领域结果成为 View,View 又成为 JSON 字节。错误也有位置:mapping 前是 404,参数转换/校验是 400,Service 是领域错误,消息转换器可能是序列化失败。日志按这些阶段记录,比只打印 Controller 进入/退出更能定位。

项目文件里 HandlerMapping 是框架能力,不需要复制;Controller 只负责声明路径和调用,Assembler 负责 Entity/Record 到 View,异常处理器负责错误 JSON。@RestController@RequestBody@PathVariable 的语义要与请求 Content-Type、路径和字段命名一致。API 测试锁定状态码和字段,Repository 测试锁定 SQL,不把数据库查询写进 Web 层。

排错时 404 先查 method/path/context path/扫描,400 查 converter 和字段路径,415 查 Content-Type,406 查 Accept,500 查异常解析和序列化;响应少字段不一定是数据库少字段,可能是 DTO 映射或视图配置。异步 Controller 还要查超时、客户端断开和事务连接是否提前关闭。练习是为详情和列表各写一条请求时序并用 MockMvc 逐段断言。

再用真实 curl 发送同一个请求,保存 URL、header、body、状态码和响应;浏览器再发送一次,比较 Origin、Cookie 和 preflight。若 curl 成功而浏览器失败,优先看 CORS/CSRF/缓存;若两者都失败,回到 mapping/绑定/Service。这个差异分析是连接 Web 基础与 Security 的实际桥梁。

验证清单:为 GET /api/articles/42 准备正常文章、未知 id、非数字 id、错误 Accept 和 Service 异常五组请求,逐组记录 HandlerMethod、参数对象、Service 输入、领域结果或异常、消息转换器输出和最终状态码。正常路径应从路径字符串“42”变成 Long 42,再从持久化 Row 变成不含内部字段的 ArticleView;非数字路径在 Controller 调用前结束,未知 id 在 Service/Repository 边界变成 404,序列化失败不能伪装成 200。用 MockMvc 断言 status、Content-Type、JSON 字段和异常响应,再用 curl 保存真实报文做一次端到端对照。

请求体还要测试对象的时间顺序:网络字节先由消息转换器读取,校验器得到带字段路径的错误,Controller 只在校验通过后调用 Service;Service 生成领域命令,Repository 返回记录,Assembler 生成 View,响应转换器最后写 JSON。若 Controller 已经打印出正确 DTO 而页面字段缺失,应查 View/序列化;若 Service 没有进入,应查 mapping、Content-Type 或参数绑定,不要先改 SQL。

项目验收可以按四层证据收集:浏览器/ curl 的原始请求,MVC 的 Handler 与绑定日志,Service 的业务输入输出,数据库查询和最终响应。每份证据用同一个 trace id 关联并只记录安全摘要。再增加 includeTags 和分页参数,确认默认值、非法范围、空结果和权限过滤在同一条链上,避免列表接口只因多一个 query 参数就绕过原有规则。

练习复盘:把详情请求的五个 fixture 固定下来:正常 200、路径转换 400、资源不存在 404、Accept 不支持 406、Service 异常 500;每个 fixture 同时保存 URL、header、body、HandlerMethod、Service 输入、响应 Content-Type 和 JSON 错误结构。再用 MockMvc 做层级断言,用 curl 做真实报文对照,用浏览器确认 CORS 只影响脚本读取。若状态码正确但字段错误,沿 DTO 到 View 到消息转换器追第一次对象偏离,而不是回头改路由。

进阶附录:异步与流式 MVC

Spring MVC 可以返回 CompletionStage 或流式响应,但线程释放、超时、客户端断开和事务边界会变化。异步返回不能把数据库连接一直持有到响应结束;先明确资源生命周期,再采用异步 API。

本课按「Spring Framework 7 Web MVC 与 Spring Boot 4.1 Web API」的学习范围组织,正文与示例均为本站原创整理。