Note 07. Spring MVC 初探:从 HTTP 请求到控制器注解详解
Note 07. Spring MVC 初探:从 HTTP 请求到控制器注解详解
ProriseNote 07. Spring MVC 初探:从 HTTP 请求到控制器
摘要:本章是我们从后端逻辑走向前台交互的起点。我们将暂时搁置底层原理,聚焦于一个核心任务:如何让我们的 Spring Boot 应用能够接收来自浏览器的请求,并给予响应。我们将通过构建一个用户管理的 API,亲手实践 @RestController、@RequestMapping 及其衍生注解,并掌握处理 URL 路径变量和查询参数的各种技巧,为你真正进入 Web 开发领域打下坚实的基础。
本章学习路径
- 第一个 Web API:我们将创建第一个
Controller,使用@RestController和@GetMapping注解,让我们的应用能通过浏览器访问并返回一句 “Hello, World!”,建立对 Web 请求最直观的认识。 - HTTP 请求方法详解:我们将系统学习
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping这四个核心注解,理解它们分别对应的 HTTP 操作(查询、创建、更新、删除),并明确各自的使用场景。 - URL 设计的艺术:我们将深入探索
@RequestMapping的强大功能,学习如何通过类级别注解组织接口、如何使用@PathVariable设计出优雅的 RESTful 风格 URL,例如/users/101。 - 请求参数的精准捕获:我们将重点掌握两种最常用的参数接收方式。第一种是使用
@RequestParam注解,精准获取 URL 中的查询参数(如?keyword=admin),并学习处理可选参数和默认值。第二种是使用普通 Java 对象(POJO)来自动封装多个请求参数,体验 Spring MVC 带来的开发便利。
7.1. 编写你的第一个 API 接口
在上一章中,我们已经了解了 spring-boot-starter-web 这个依赖。它就像一个工具包,为我们提供了构建 Web 应用所需的一切,包括一个内置的 Web 服务器(默认为 Tomcat)。现在,我们要做的就是告诉这个服务器:当收到某个特定 URL 请求时,应该执行哪一段 Java 代码。这个“告诉”的过程,就是通过编写一个“控制器”(Controller)类来完成的。
7.1.1. 创建 Controller 类
首先,我们需要在一个统一的包下管理所有的 Controller。这是一种行业通用的规范,便于项目结构的维护。
操作步骤:在 com.example.demo 主包下创建一个名为 controller 的新包。
接着,在这个新包里,我们来创建第一个控制器 HelloController。
文件路径:src/main/java/com/example/demo/controller/HelloController.java
现在,我们来编写这个类的初始代码:
1 | package com.example.demo.controller; |
代码核心要点解读:
@RestController: 这是一个“复合注解”,它告诉 Spring Boot 两件事:第一,HelloController是一个控制器,请把它交由 Spring 容器管理;第二,这个控制器专门用来返回数据(如字符串、JSON),而不是用来跳转页面。这是现代前后端分离架构中最常用的注解。@GetMapping("/hello"): 这个注解精确地定义了一个“路由规则”。它告诉 Spring MVC,如果有一个GET类型的 HTTP 请求,其访问路径是/hello,那么就应该由sayHello()这个方法来处理。
7.1.2. 启动与验证
现在,让我们启动 DemoApplication 主类的 main 方法。当控制台输出 Tomcat started on port(s): 8080 时,就代表我们的内嵌 Web 服务器已经带着我们刚编写的 HelloController 成功启动了。
打开你的浏览器,在地址栏输入 http://localhost:8080/hello 并回车。
如果页面上成功显示了 “你好,Spring Boot 3!”,恭喜你!你已经成功编写并运行了你的第一个 Web API 接口。这个看似简单的过程,背后实际上是 spring-boot-starter-web 自动配置的 Tomcat、Spring MVC 等一系列组件协同工作的结果。
7.2. HTTP 请求方法:不仅仅是 GET
在 Web 世界中,HTTP 协议定义了一套“动词”(Verbs)来表达对资源的不同操作意图。我们刚刚使用的 GET 只是其中之一,它通常用来表示“获取”或“查询”资源。在一个完整的应用中,我们还需要处理创建、更新、删除等操作。Spring MVC 为此提供了一系列一一对应的注解。
为了更好地演示,我们创建一个 UserController,专门用于处理与用户相关的操作。
文件路径:src/main/java/com/example/demo/controller/UserController.java
1 | package com.example.demo.controller; |
在这个 UserController 中,我们接触到了几个新的、至关重要的注解:
| 注解 | HTTP 方法 | 惯例用途 | 示例 URL | 描述 |
|---|---|---|---|---|
@GetMapping | GET | 查询 资源 | /users | 用于获取资源列表或单个资源的详情。它是幂等的(Idempotent),即多次调用返回相同的结果。 |
@PostMapping | POST | 创建 资源 | /users | 用于提交数据以创建一个新的资源。它不是幂等的,多次调用会创建多个资源。 |
@PutMapping | PUT | 更新 资源(完整替换) | /users/101 | 用于更新一个已存在的资源,通常需要提供该资源的完整信息。它是幂等的。 |
@DeleteMapping | DELETE | 删除 资源 | /users/101 | 用于删除一个指定的资源。它也是幂等的。 |
什么是幂等性 (Idempotence)?
简单来说,同一个请求,执行一次和执行 N 次,对资源产生的影响是完全相同的。查询、更新(完整替换)和删除操作都具备此特性,而创建操作则不具备。理解幂等性对于设计健壮的、可重试的 API 至关重要。
由于浏览器地址栏直接访问默认都是 GET 请求,要测试 POST, PUT, DELETE 请求,我们需要使用专业的 API 测试工具,例如 Postman 或 IntelliJ IDEA 内置的 HTTP Client。
7.3. URL 设计:@RequestMapping 与 @PathVariable
一个设计良好、易于理解的 URL 结构是 API 的门面。Spring MVC 提供了强大的工具来帮助我们组织和设计 URL。
7.3.1. 类级别 @RequestMapping:为接口分组
我们遇到了什么问题?
随着 UserController 中的接口越来越多,如果我们为每个方法都编写完整的 URL,如 @GetMapping("/users")、@PostMapping("/users"),会出现大量重复的 /users 前缀。这不仅繁琐,而且如果未来需要将 /users 修改为 /api/v1/users,就需要修改每一个注解,极易出错。
解决方案:
将共同的路径前缀提取到类级别的 @RequestMapping 注解上。这样,方法级别的路径会自动拼接在类级别路径之后。
代码解读(回顾 UserController):
1 |
|
通过这种方式,我们将 /users 这个“命名空间”与具体的 GET, POST 操作解耦,使得代码结构更清晰,维护也更便捷。
7.3.2. 动态 URL:使用 @PathVariable 捕获路径变量
我们遇到了什么新需求?
在上面的 updateUser 和 deleteUser 方法中,我们需要明确操作的是“哪一个”用户。在 RESTful API 设计风格中,资源的唯一标识符(如用户 ID)通常直接放在 URL 路径中,形成类似 /users/101(操作 ID 为 101 的用户)这样的动态 URL。
解决方案:
Spring MVC 使用 {占位符} 的语法来定义路径中的变量,并使用 @PathVariable 注解将这部分动态的值绑定到方法的参数上。
代码解读(回顾 UserController):
1 | /** |
常见错误示例:
如果 @PathVariable 注解中的名称与路径占位符不一致,会怎么样?
1 | // 错误示例: 占位符是 {id},但注解想找 "userId" |
在这种情况下,启动项目并访问 /users/101,后台会抛出 Missing URI template variable 'userId' for method parameter of type Long 异常。
正确修正:
要么保持参数名与占位符一致,省略注解的 value:@PutMapping("/{id}") public String updateUser(@PathVariable Long id)
要么在注解中显式指定占位符的名称:@PutMapping("/{id}") public String updateUser(@PathVariable("id") Long userId)
7.4. 捕获请求参数:@RequestParam 与 POJO 封装
除了路径变量,Web 开发中更常见的是通过 URL 查询参数来传递数据,例如进行搜索、筛选和分页。
7.4.1. 单个参数捕获:@RequestParam
场景:我们需要实现一个用户搜索接口,URL 可能是这样的:/users/search?keyword=admin&page=1。其中 keyword 和 page 就是查询参数。
解决方案:
使用 @RequestParam 注解,可以精准地从 URL 的 Query String (问号 ? 之后的部分) 中提取参数值。
第一步:在 UserController 中添加新方法
1 | /** |
代码解读:
@RequestParam("keyword") String keyword: 从查询参数中寻找名为keyword的项,并将其值赋给keyword字符串变量。
第二步:处理可选参数与默认值
我们遇到了什么新问题?
如果用户搜索时只提供了关键词,没有提供页码(/users/search?keyword=admin),那么上面的代码会因为找不到 page 参数而报错(Required request parameter 'page' for method parameter type Integer is not present)。我们希望 page 参数是可选的,并且在用户不提供时默认为第 1 页。
解决方案:
@RequestParam 注解提供了 required 和 defaultValue 两个非常有用的属性。
第三步:优化 searchUsers 方法
1 | /** |
现在,这个接口就变得更加健壮了。
- 访问
/users/search_v2?keyword=test-> 正常工作,page取默认值1。 - 访问
/users/search_v2?keyword=test&page=5-> 正常工作,page取值5。 - 访问
/users/search_v2?page=5-> 报错,因为必需的keyword参数缺失。
7.4.2. 多个参数的优雅封装:POJO 对象绑定
我们遇到了什么痛点?
想象一个复杂的报表查询接口,可能有十几个筛选条件:开始时间、结束时间、用户状态、部门 ID、订单类型…… 如果全部使用 @RequestParam,Controller 方法的参数列表会变得极长,代码显得非常臃肿且难以维护。
public String searchReport(@RequestParam String startDate, @RequestParam String endDate, @RequestParam Integer status, ...)
解决方案:
Spring MVC 支持直接使用一个普通的 Java 对象(Plain Old Java Object, POJO)来接收一组相关的请求参数。它会自动将请求中的参数名与 POJO 中的属性名进行匹配,并完成赋值。
第一步:创建用于数据传输的 DTO 对象
我们通常会创建一个专门的类来承载这些参数,这类对象被称为 DTO(Data Transfer Object,数据传输对象)。
文件路径:src/main/java/com/example/demo/dto/UserQueryDTO.java
1 | package com.example.demo.dto; |
最佳实践:在项目中引入 Lombok 依赖可以极大地简化实体类和 DTO 的代码,避免手写大量样板代码。这是 Java 企业级项目开发的标准配置。
第二步:在 Controller 中使用 DTO 接收参数
现在,我们可以在 UserController 中创建一个新的接口,直接使用这个 DTO 对象作为方法参数。
1 | import com.example.demo.dto.UserQueryDTO; // 别忘了导入 |
代码解读:
filterUsers(UserQueryDTO query): 方法参数是一个UserQueryDTO对象,并且没有@RequestBody注解(这个注解我们将在下一章讲解 JSON 时用到)。- 当请求
/users/filter?keyword=manager&status=1到达时,Spring MVC 会在后台默默地做以下事情:- 创建一个
UserQueryDTO的空对象实例。 - 查找请求中的参数,找到
keyword。 - 在
UserQueryDTO对象中寻找名为setKeyword的方法,并调用它,将值"manager"传入。 - 查找请求中的参数,找到
status。 - 在
UserQueryDTO对象中寻找名为setStatus的方法,并调用它,将值"1"转换为Integer类型后传入。 - 所有参数匹配完毕后,将这个填充好数据的
query对象传递给我们的filterUsers方法。
- 创建一个
这种方式让我们的 Controller 代码保持了极高的整洁度和可读性,是处理多参数场景下的不二之选。
7.5. 本章总结与 Web API 速查
摘要回顾
本章我们完成了从 0 到 1 的跨越,成功让我们的 Java 程序“上网”了。我们没有陷入复杂的理论,而是通过动手实践,掌握了构建一个 Web API 最核心、最常用的几个武器。我们理解了如何使用 @RestController 声明一个数据接口,如何通过 @GetMapping、@PostMapping 等注解来响应不同的 HTTP 操作,并通过 @RequestMapping 和 @PathVariable 设计出了清晰的 URL 结构。最后,我们还学习了 @RequestParam 和 POJO 绑定这两种处理请求参数的强大技巧。
遇到以下 4 种 Web API 场景时,请直接参考下方的标准代码模版:
7.5.1. 场景一:提供一个简单的列表查询接口
- 需求:获取所有商品列表。
- 设计:使用
GET方法,URL 为/products。 - 方案:
1 |
|
7.5.2. 场景二:获取特定 ID 资源的详情
- 需求:根据 ID 获取单个用户的详细信息。
- 设计:使用
GET方法,URL 为/users/{userId},其中{userId}是动态的。 - 方案:
1 |
|
7.5.3. 场景三:处理带可选参数的分页查询
- 需求:搜索文章,可以根据关键词
q搜索,并支持分页参数page,page不传时默认为 1。 - 设计:使用
GET方法,URL 为/articles/search?q=...&page=...。 - 方案:
1 |
|
7.5.4. 场景四:处理包含多个筛选条件的复杂查询
- 需求:根据订单状态、创建起始时间、客户 ID 等多个条件筛选订单。
- 设计:使用
GET方法,将所有筛选条件作为查询参数,并用一个 DTO 对象来接收。 - 方案:
1 | // 1. 定义 DTO |










