09- [Web 核心] Spring MVC 与 RESTful API

1. [Web 核心] Spring MVC 与 RESTful API

摘要: 在掌握了 Spring Boot 的基础之后,本章我们将揭开 spring-boot-starter-web 的神秘面纱,深入其核心——Spring MVC 框架。我们将学会在 Spring Boot 的“羽翼”下,轻松创建出第一个 RESTful API 接口,为后续的实战项目打下坚实的基础。

1.1. 承上启下:Spring Boot 与 Spring MVC 的关系

在正式开始学习 Spring MVC 的具体功能前,我们首先需要精确理解其在 Spring Boot 项目中的角色与关系。请您回顾在 第一、二章中我们创建的 Spring Boot 项目,其 pom.xml 文件中包含一项关键依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

该依赖是 Spring Boot 提供的“场景启动器” (Starter)。它的核心作用主要有两点:依赖传递触发自动配置

  1. 依赖传递: spring-boot-starter-web 会将构建 Web 应用所需的一整套相关库(JAR 包)自动引入到我们的项目中。这其中包括了 spring-webmvcspring-web 等 Spring MVC 框架的核心,同时也内嵌了 Tomcat 服务器作为默认的 Servlet 容器。

  2. 触发自动配置: 更重要的是,当 Spring Boot 检测到 spring-boot-starter-web 存在于类路径中时,其强大的 自动配置 机制便会生效。它会在后台为我们自动配置好所有在传统 Spring MVC 开发中需要手动处理的核心组件,例如:

  • DispatcherServlet (前端控制器)
    * HandlerMapping (处理器映射器)
    * HandlerAdapter (处理器适配器)
    * 多种 HttpMessageConverter (用于处理 JSON、表单等数据的消息转换器)

因此,我们可以这样精准地定义二者的关系:

  • Spring MVC:是一个功能强大且成熟的 Web 框架,它提供了构建 Web 应用所需的全套组件和清晰的架构模式。
  • Spring Boot:是一个 集成与简化框架。它并非 Spring MVC 的替代品,而是通过自动配置技术,免去了我们手动配置 Spring MVC 的所有繁琐步骤,使我们能够直接专注于编写业务代码。

总结:我们接下来的学习,本质上是在 Spring Boot 提供的“全自动化”环境中,深入使用 Spring MVC 这个核心 Web 框架的各项功能。理解这一点,将有助于我们更好地把握后续所有知识点。


1.2. 第一个 API 接口

在第二章中,我们为了快速体验 Spring Boot 的 Web 功能,已经创建过一个 HelloController。当时我们只关注了运行结果,并未深究其工作原理。

现在,让我们以 Spring MVC 的专业视角,回顾并深度解析 这段我们已经很熟悉的代码。

文件路径: src/main/java/com/example/springbootdemo/controller/HelloController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @RestController
* 这是一个组合注解,它本身包含了 @Controller@ResponseBody 两个注解。
* 它的核心作用是:
* 1. @Controller:向 Spring IoC 容器声明这是一个控制器(Controller)组件,
* 使其能够被组件扫描(ComponentScan)机制发现并注册为 Bean。
* 2. @ResponseBody:告知 Spring MVC 框架,本类中的所有方法返回的都不是视图名,
* 而是需要直接写入 HTTP 响应体(Response Body)的数据。
* 这是构建 RESTful API 的关键,意味着我们将直接返回 JSON、XML 或纯文本等数据。
*/
@RestController
public class HelloController {

/**
* @GetMapping("/hello")
* 这是一个请求映射(Request Mapping)注解,用于处理 HTTP GET 请求。
* 它是 @RequestMapping(method = RequestMethod.GET) 的一个更简洁的缩写形式。
* 它的作用是将所有访问路径为 "/hello" 的 GET 请求,都路由到这个 sayHello() 方法上来进行处理。
*
* @return String
* 由于类上标注了 @RestController,此方法返回的 String "Hello..." 将被
* Spring MVC 视为响应体内容,通过 StringHttpMessageConverter 直接写入响应,
* 最终在浏览器或 API 测试工具中呈现。
*/
@GetMapping("/hello")
public String sayHello() {
return "Hello, 这是我的第一个 Spring MVC API!";
}
}

如代码注释所示,@RestController@GetMapping 这两个注解的组合,便构成了 Spring MVC 中最基础的 API 接口。前者负责声明类的身份和数据响应模式,后者负责将具体的 URL 路径映射到处理方法上。

正是因为 Spring Boot 的自动配置为我们处理了所有底层细节,我们才能如此简洁地实现这一功能。在下一节,我们将简要地探讨一下这个请求在其内部的流转过程。


1.3. 自动配置的背后:DispatcherServlet 流程简述

在上一节,我们看到仅用两个注解就成功创建了一个 API 接口。现在,我们自然会产生一个疑问:当我们启动应用并在浏览器中访问 /hello 时,这个请求是如何精确地找到并执行我们编写的 sayHello() 方法的?

答案的核心,在于一个由 Spring Boot 自动为我们配置和注册的组件——DispatcherServlet

您可以将它理解为 Spring MVC 框架在 Web 应用中的 总调度中心前端控制器。所有进入我们应用的 HTTP 请求,都会首先被它拦截。它接收到请求后,会像一位经验丰富的交通警察,遵循一套固定的、高效的流程来处理和分发请求。

我们可以将这个流程简化为以下几个关键步骤:

SpringMVC 流程

接收请求

浏览器或 API 工具发出 GET /hello 请求,被 Spring Boot 内嵌的 Tomcat 服务器接收。

转交DispatcherServlet

Tomcat 将请求转交给 Spring MVC 的总指挥 DispatcherServlet

查找处理器

DispatcherServlet 询问:“谁能处理 /hello 这个请求?”。它通过查询 HandlerMapping (处理器映射器),找到了 HelloController.sayHello() 方法这个最终的处理器。

调用处理方法

DispatcherServlet 通过 HandlerAdapter (处理器适配器),去适配并调用我们编写的 sayHello() 方法。

返回数据

sayHello() 方法执行,并返回字符串 "Hello, 这是我的第一个 Spring MVC API!"

返回响应

由于 HelloController 类上有 @RestController 注解,DispatcherServlet 知道这是一个 API 请求。它会选择一个合适的 HttpMessageConverter (消息转换器),将返回的字符串直接写入 HTTP 响应体。

完成请求

最终,完整的 HTTP 响应被送回给客户端。

这个看起来复杂的流程,在 Spring Boot 的帮助下,我们一行配置代码都不需要写。DispatcherServlet 及其配套的 HandlerMappingHandlerAdapter 等所有组件,都由 spring-boot-starter-web 自动配置完成。我们只需要专注编写 @RestController 里的业务逻辑即可。


1.4. 深入请求映射:@RequestMapping 全方位解析

我们已经掌握了 @GetMapping 的基础用法,但 Spring MVC 的请求映射远不止于此。@RequestMapping 及其衍生注解提供了一套强大而灵活的工具集,能够让我们应对各种复杂的 URL 映射场景。接下来,我们将逐一探索这些高级用法。

1.4.1. 组合运用:类级别与方法级别映射

为什么需要它?
当项目逐渐变大,一个模块(如用户管理)可能会包含多个相关的 API 接口。如果所有 URL 映射都直接写在方法上,会显得杂乱且容易产生路径冲突。通过在类上添加 @RequestMapping,我们可以为该控制器的所有接口定义一个统一的“命名空间”或“父路径”。

如何使用?
在控制器类上添加 @RequestMapping("/some-prefix"),那么该类中所有方法的映射路径都会自动带上这个前缀。

代码示例
我们创建一个 UserController,并将其所有接口都归属在 /users 路径下。

文件路径: src/main/java/com/example/demo/controller/UserController.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.springbootdemo.controller;

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

@RestController
@RequestMapping("/users") // 在类上定义父路径 /users
public class UserController {

/**
* 此方法的完整访问路径是类上的 @RequestMapping + 方法上的 @GetMapping
* 即: /users + /all = /users/all
*/
@GetMapping("/all")
public String getAllUsers() {
return "返回所有用户列表";
}
}

运行验证

启动 Spring Boot 应用,使用 cURL 或 Postman 访问 http://localhost:8080/users/all

1
curl http://localhost:8080/users/all

1.4.2. 动态路径:@PathVariable 与路径占位符

为什么需要它?
在 RESTful API 设计中,我们经常需要通过 URL 来指定要操作的资源,例如,通过用户 ID 来获取特定用户的信息。这时,URL 中就会包含动态变化的部分。

如何使用?
@RequestMapping 的路径中使用 {} 来定义一个路径变量(占位符),然后在方法参数中使用 @PathVariable 注解来获取这个变量的值。

代码示例
我们在 UserController 中添加一个根据 ID 查询用户的方法。

文件路径: src/main/java/com/example/demo/controller/UserController.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ... 省略 import 和类注解 ...
@RestController
@RequestMapping("/users")
public class UserController {

// ... 已有的 getAllUsers() 方法 ...

/**
* {id} 是一个路径占位符。
* @PathVariable("id") 注解会将 URL 中占位符 {id} 的实际值,
* 绑定到方法的 Long id 参数上。
* 例如,当请求 /users/101 时,id 参数的值就是 101。
*/
@GetMapping("/{id}")
public String getUserById(@PathVariable("id") Long id) {
return "正在查询 ID 为: " + id + " 的用户";
}
}

如果方法参数名与路径占位符的名称完全相同,@PathVariable("id") 部分可以省略,直接写 @PathVariable Long id 即可。

运行验证

访问 http://localhost:8080/users/101

1
curl http://localhost:8080/users/101

1.4.3. 模糊匹配:Ant 风格路径

为什么需要它?
有时我们需要一个方法能处理一类相似但不完全相同的 URL,而不是为每个 URL 都写一个方法。Ant 风格的通配符就提供了这种模糊匹配的能力。

如何使用?
@RequestMapping 支持三种 Ant 风格的通配符:

  • ?:匹配任意 单个 字符。
  • *:匹配任意数量(0 或多个)的字符,但不包括 /
  • **:匹配任意数量(0 或多个)的字符,可以包括 /

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.springbootdemo.controller;

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

@RestController
public class AntController {

// 匹配 /ant/testA, /ant/testB, 但不匹配 /ant/testAB
@GetMapping("/ant/test?")
public String testAnt1() {
return "Ant-style match: ?";
}

// 匹配 /ant/test, /ant/testABC, 但不匹配 /ant/test/abc
@GetMapping("/ant/test*")
public String testAnt2() {
return "Ant-style match: *";
}

// 匹配 /ant/any/path/can/be/here
// 这个在 Springboot3 是会报错的!
@GetMapping("/ant/**/any")
public String testAnt3() {
return "Ant-style match: **";
}
}

重要: 在 Spring Framework 6.x (Spring Boot 3.x) 及更高版本中,出于安全性考虑,不再允许 ** 通配符出现在路径的中间部分。它通常只能用在末尾,例如 /ant/**

运行验证

访问 http://localhost:8080/ant/testABC

1
curl http://localhost:8080/ant/testABC

1.4.4. 精准匹配:params 与 headers 属性

为什么需要它?
有时,仅通过 URL 路径还不足以区分请求。我们可能需要根据请求中是否包含 特定的参数特定的请求头,来决定由哪个方法处理。这在 API 版本控制或根据特定条件路由时非常有用。

如何使用?
@RequestMapping 及其衍生注解中,使用 paramsheaders 属性来添加匹配条件。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.springbootdemo.controller;

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

@RestController
public class PreciseController {

/**
* 只有当请求 URL 中包含名为 "version" 的参数时,此方法才会被调用。
* 例如: /precise?version = 1
*/
@GetMapping(value = "/precise", params = "version")
public String testParams() {
return "Match with params 'version'";
}

/**
* 只有当请求 URL 中包含名为 "version" 且其值等于 "2" 的参数时,才会被调用。
* 例如: /precise?version = 2
*/
@GetMapping(value = "/precise", params = "version=2")
public String testParamsWithValue() {
return "Match with params 'version=2'";
}

/**
* 只有当请求头中包含名为 "X-API-VERSION" 的头信息时,此方法才会被调用。
*/
@GetMapping(value = "/precise", headers = "X-API-VERSION")
public String testHeaders() {
return "Match with header 'X-API-VERSION'";
}
}

运行验证

使用 cURL 的 -H 选项来添加请求头,验证 headers 属性。

1
curl -H "X-API-VERSION: v1.0" http://localhost:8080/precise

如果请求不满足 paramsheaders 的匹配条件,客户端通常会收到一个 404 Not Found400 Bad Request 的错误,因为 Spring MVC 认为没有找到合适的处理器方法来处理该请求。


1.5. 优雅地获取请求参数

我们已经学会了如何将不同的 URL 映射到控制器方法上。接下来的关键一步,是学习如何从这些请求中 获取客户端传递过来的数据。无论是 URL 中的查询参数,还是请求体中的 JSON 数据,Spring MVC 都提供了极为便捷的方式来获取它们。

1.5.1. 获取 URL 参数:@RequestParam

为什么需要它?
URL 查询参数是在 ? 之后,以 key=value 形式拼接的参数,是 GET 请求传递少量数据的最常见方式。例如,在一个搜索功能中,URL 可能是 /users/search?keyword=admin。我们需要一种方式来获取 keyword 的值。

如何使用?
@RequestParam 注解可以精确地将 URL 查询参数绑定到控制器方法的参数上。

代码示例
我们在 UserController 中添加一个搜索方法来演示。

文件路径: src/main/java/com/example/springbootdemo/controller/UserController.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.springbootdemo.controller;

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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class UserController {

// ... 此前已有的方法 ...

/**
* @RequestParam 用于获取 URL 查询参数 (Query Parameter)。
* * @param keyword 绑定 URL 中名为 "keyword" 的参数。如果 URL 是 /users/search?keyword = admin, keyword 的值就是 "admin"。
* @param page 绑定名为 "page" 的参数。它有两个额外属性:
* - required = false: 表示这个参数不是必需的。如果请求中不包含 page 参数,程序不会报错。
* - defaultValue = "1": 如果请求中没有传递 page 参数,则默认为 "1"。
* @return 响应字符串
*/
@GetMapping("/search")
public String searchUsers(
// 这里一样的,可以省略
@RequestParam String keyword,
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page) {

return "正在搜索用户,关键词: " + keyword + ", 页码: " + page;
}
}

运行验证

使用 cURL 访问 /users/search 路径,并附带查询参数。

1
2
# 测试必需参数和默认值参数
curl "http://localhost:8080/users/search?keyword=admin"
1
2
# 测试同时提供两个参数
curl "http://localhost:8080/users/search?keyword=admin&page=3"

1.5.2. 处理请求体:@RequestBody 与 JSON

为什么需要它?
当需要提交的数据结构比较复杂时(例如创建一个新用户,包含姓名、密码、邮箱等多个字段),通常会将这些数据作为一个整体,放在 HTTP 请求体(Request Body)中发送,而 JSON 是当今最主流的数据格式。

如何使用?
@RequestBody 注解告诉 Spring MVC:请获取完整的请求体内容,并使用内置的 HttpMessageConverter(通常是 Jackson)将其反序列化成一个指定的 Java 对象(POJO)。

代码示例
首先,我们需要创建一个 User 类来承载数据。

文件路径: src/main/java/com/example/springbootdemo/model/User.java (新增文件)

1
2
3
4
5
6
7
8
9
10
package com.example.springbootdemo.model;

import lombok.Data;

@Data // Lombok 注解,自动生成 Getter, Setter, toString, equals, hashCode 等方法
public class User {
private Long id;
private String username;
private String email;
}

然后,在 UserController 中添加一个创建用户的方法。

文件路径: src/main/java/com/example/springbootdemo/controller/UserController.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.springbootdemo.controller;

import com.example.springbootdemo.model.User;
import org.springframework.web.bind.annotation.*;

// 注意,这里的 RestController 其实就是 @Controller + @ResponseBody
@RestController
@RequestMapping("/users") // 在类上定义父路径 /users
public class UserController {

// ....其余的所有代码

/**
* @RequestBody 注解告诉 Spring MVC 将请求体中的 JSON 数据
* 自动转换为一个 User 对象
* @param user Spring MVC 自动实例化的 User 对象
* @return 响应字符串
*/
@PostMapping("/create")
public String createUser(@RequestBody User user) {
// 在实际项目中,这里会调用 Service 层将 user 对象存入数据库
return "成功创建用户: " + user.toString();
}
}

重要信息: @RestController 是类级注解,让类成为返回数据的控制器;@RequestBody 是方法参数注解,能把请求体数据转成对象供方法使用,这两者是不同的,需要严格区分!

运行验证

我们使用 cURL 模拟一个 POST 请求,并通过 -H 指定 Content-Typeapplication/json,使用 -d 传入 JSON 数据。

1
2
3
4
curl -X POST \
http://localhost:8080/users/create \
-H 'Content-Type: application/json' \
-d '{"id":1, "username":"zhangsan", "email":"zhangsan@example.com"}'

1.5.3. 自动封装:使用 POJO 接收参数

为什么需要它?
除了接收 JSON 请求体,Spring MVC 还提供了一种更便捷的方式来处理多个普通的 URL 查询参数或表单参数——直接用一个 POJO 对象来接收。

如何使用?
当控制器方法的参数是一个 没有@RequestBody 注解的 POJO 时,Spring MVC 会自动尝试将请求中的 同名参数(无论是 URL 查询参数还是 x-www-form-urlencoded 表单参数)赋值给这个 POJO 对象的相应属性。

代码示例
我们为 UserController 添加一个更复杂的、支持多条件筛选的查询方法。

文件路径: src/main/java/com/example/springbootdemo/controller/UserController.java (修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.springbootdemo.controller;

import com.example.springbootdemo.model.User;
import org.springframework.web.bind.annotation.GetMapping;
// ... 其他 import ...

@RestController
@RequestMapping("/users")
public class UserController {

// ... 此前已有的方法 ...

/**
* 方法参数是一个 POJO (User user),且没有 @RequestBody 注解。
* Spring MVC 会自动将 URL 中的查询参数 ?username =...&email =...
* 绑定到 user 对象的 username 和 email 属性上。
* @param user Spring MVC 自动实例化的 User 对象
* @return 响应字符串
*/
@GetMapping("/filter")
public String filterUsers(User user) {
return "根据条件筛选用户: " + user.toString();
}
}

运行验证

我们像调用普通 GET 请求一样,在 URL 后面附上多个查询参数。

1
curl "http://localhost:8080/users/filter?username=lisi&email=lisi@example.com"

核心区别: @RequestBody 用于处理一个 单一的、完整的请求体(通常是 JSON 或 XML)。而 POJO 直接接收参数的方式,则用于处理 零散的、多个的请求参数(通常是 URL 查询参数或表单)。一个方法中,@RequestBody 注解最多只能使用一次。