摘要:从一条 @GetMapping 映射出发,看清 Spring、Spring MVC、Spring Boot 的分工。用一个最小接口观察请求匹配、参数转换和 JSON 返回,不陷入容器源码。
范围说明:以多门店预约系统作教学案例,不代表作品集项目已按此部署;示例数字不是生产测量。版本相关用法以文末官方资料及实际依赖为准。
我们写一个 @GetMapping,启动应用,浏览器竟然就能执行这个方法。是谁看到注解、保存了地址,又在请求到来时找到它?这不是 Java 方法突然拥有了联网能力,而是应用里已经有人替我们接收和分发请求。
一、先把三个经常一起出现的名字分开
Spring 提供对象管理、依赖注入等基础能力;Spring MVC 在 Servlet Web 应用中组织请求处理;Spring Boot 帮助配置和启动应用,按依赖和条件建立许多常用设施。它们是协作关系,不是请求要依次访问的三个远程服务。^boot^mvc
可以把 Boot 看成减少启动装配工作的入口,而不是把整个应用等同于一个注解。自动配置有条件,也允许被自定义配置替换。遇到问题时,应该检查实际生效的配置,而不是猜"Boot 肯定替我配了"。
本篇只讨论常见的 Servlet / Spring MVC 路线,不把 WebFlux 的响应式处理混进来。同样叫 Java Web 应用,内部技术路线可以不同,先把一条路线学清楚就够了。

二、方法地址是在启动时登记的
假设类声明了 /api/stores,方法声明了 /{id}。MVC 会根据控制器映射元数据建立匹配关系,条件不只包括路径,还可以包括 HTTP 方法、请求头和内容类型。请求到来后,不是逐个扫描所有 Java 文件找字符串。\^mapping
在这条路线里,Web 容器接收 HTTP;DispatcherServlet 作为前端控制器协调处理;映射组件查找处理方法,适配组件组织方法调用。参数转换、异常处理和响应转换也都有专门设施。\^mvc
这些细节不需要第一天就全部记住。先记住"启动时准备映射,请求时查找并调用"。因此访问一个不存在的路径、用错请求方法,与业务代码主动报错,是不同层面的问题。
三、Controller、Service、Mapper 为什么不写成一个类?
Controller 处理接口形态:门店编号怎么接收、参数是否合法、响应怎样表达。Service 处理业务语义:用户能否查看这家门店、哪些字段对当前身份可见。Mapper 处理 SQL 与结果映射。\^mybatis
把职责拆开后,同一段业务可以被 HTTP 接口、任务或其他入口调用;测试业务规则也不必每次启动完整 HTTP 链路。不过分层不是要求每个方法都机械套三层空壳,简单功能可以保持简单。
还要区分三种"看起来都是判断"的工作:参数是不是合法数字,是输入问题;当前账号能否查看门店,是权限问题;门店是否营业,是业务问题。将它们塞进同一个超长 Controller,会让修改越来越难定位。

四、做一个不依赖数据库的小实验
在已有 Spring Boot MVC 项目中,把下面类放在启动类扫描包内。示例适用于 Java 17 以上、已引入 MVC Web 依赖的项目;它只演示映射,不是完整门店接口。
java
package example.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/stores")
public class StoreController {
record StoreView(long id, String name) {}
@GetMapping("/{id}")
public StoreView detail(@PathVariable("id") long id) {
return new StoreView(id, "教学门店");
}
}
启动后请求 GET /api/stores/1001,观察返回 JSON;再访问 /api/stores/abc,观察参数转换失败;最后对同一路径发送 POST,观察方法不匹配。先记录真实状态码,若项目有全局异常处理或安全过滤器,表现可能不同。
这个实验故意没有 Service 和 Mapper:目的是隔离 HTTP 分发问题。先证明请求能到方法,再引入业务和数据库,才容易知道哪一步带来了新问题。

五、接口没到方法,不要立刻检查数据库
如果 Controller 断点没触发,应先确认请求实际地址、端口、HTTP 方法和应用是否收到请求。反向代理可能重写路径,过滤器可能提前拒绝,控制器也可能没有被扫描。不要在入口还没确认时就开始加索引。
如果方法已经执行,但客户端收到错误,再核对返回对象转换、全局异常处理和连接状态。HTTP 链路还有响应半程;"代码运行到 return"不意味着用户必然已经收到完整结果。
同样,Service 中有一个对象字段,不代表它一定由 Spring 正确创建。手工 new 与由容器管理的对象不是同一种使用方式。关于代理和事务的进一步影响,留给后续专题,不在这里堆源码概念。
六、回到整体地图:注解只标出了你的业务入口
我们写的业务方法位于应用内部。Boot 帮助装配,MVC 负责分发,容器负责对象管理,工具和数据库承担各自职责。一个请求真正成功,是这些部分共同完成工作的结果。
在自己的项目里找一个接口,分别确认类级路径、方法级路径、请求方法和扫描范围。再把一个正常请求与一个错误请求并排记录:谁返回了错误,它发生在业务之前还是之后?
还可以把同一请求分别从浏览器和命令行发送,核对二者实际使用的路径和方法。有时不是后端行为不一致,而是页面拼接了不同地址,或者请求在代理层被改写。把输入对齐之后,再比较结果才有意义。
理解这条链路后,@GetMapping 不再是"加上就能访问"的魔法,而是告诉运行系统:满足哪些条件的请求,可以交给这个方法。下一步,我们把视角移到 Java 服务前面,看看 DNS、CDN 和 Nginx。