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

09- [Web 核心] Spring MVC 与 RESTful API
Prorise1. [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 | <dependency> |
该依赖是 Spring Boot 提供的“场景启动器” (Starter)。它的核心作用主要有两点:依赖传递 与 触发自动配置。
依赖传递: spring-boot-starter-web 会将构建 Web 应用所需的一整套相关库(JAR 包)自动引入到我们的项目中。这其中包括了
spring-webmvc
、spring-web
等 Spring MVC 框架的核心,同时也内嵌了 Tomcat 服务器作为默认的 Servlet 容器。触发自动配置: 更重要的是,当 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 | package com.example.demo.controller; |
如代码注释所示,@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 及其配套的 HandlerMapping、HandlerAdapter 等所有组件,都由 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 | package com.example.springbootdemo.controller; |
运行验证
启动 Spring Boot 应用,使用 cURL 或 Postman 访问 http://localhost:8080/users/all
。
1 | curl http://localhost:8080/users/all |
1
返回所有用户列表
1.4.2. 动态路径:@PathVariable 与路径占位符
为什么需要它?
在 RESTful API 设计中,我们经常需要通过 URL 来指定要操作的资源,例如,通过用户 ID 来获取特定用户的信息。这时,URL 中就会包含动态变化的部分。
如何使用?
在 @RequestMapping
的路径中使用 {}
来定义一个路径变量(占位符),然后在方法参数中使用 @PathVariable
注解来获取这个变量的值。
代码示例
我们在 UserController
中添加一个根据 ID 查询用户的方法。
文件路径: src/main/java/com/example/demo/controller/UserController.java
(修改)
1 | // ... 省略 import 和类注解 ... |
如果方法参数名与路径占位符的名称完全相同,@PathVariable
的 ("id")
部分可以省略,直接写 @PathVariable Long id
即可。
运行验证
访问 http://localhost:8080/users/101
。
1 | curl http://localhost:8080/users/101 |
1
正在查询 ID 为: 101 的用户
1.4.3. 模糊匹配:Ant 风格路径
为什么需要它?
有时我们需要一个方法能处理一类相似但不完全相同的 URL,而不是为每个 URL 都写一个方法。Ant 风格的通配符就提供了这种模糊匹配的能力。
如何使用?@RequestMapping
支持三种 Ant 风格的通配符:
?
:匹配任意 单个 字符。*
:匹配任意数量(0 或多个)的字符,但不包括/
。**
:匹配任意数量(0 或多个)的字符,可以包括/
。
代码示例
1 | package com.example.springbootdemo.controller; |
重要: 在 Spring Framework 6.x (Spring Boot 3.x) 及更高版本中,出于安全性考虑,不再允许 **
通配符出现在路径的中间部分。它通常只能用在末尾,例如 /ant/**
。
运行验证
访问 http://localhost:8080/ant/testABC
。
1 | curl http://localhost:8080/ant/testABC |
1
Ant-style match: *
1.4.4. 精准匹配:params 与 headers 属性
为什么需要它?
有时,仅通过 URL 路径还不足以区分请求。我们可能需要根据请求中是否包含 特定的参数 或 特定的请求头,来决定由哪个方法处理。这在 API 版本控制或根据特定条件路由时非常有用。
如何使用?
在 @RequestMapping
及其衍生注解中,使用 params
或 headers
属性来添加匹配条件。
代码示例
1 | package com.example.springbootdemo.controller; |
运行验证
使用 cURL 的 -H
选项来添加请求头,验证 headers
属性。
1 | curl -H "X-API-VERSION: v1.0" http://localhost:8080/precise |
1
Match with header 'X-API-VERSION'
如果请求不满足 params
或 headers
的匹配条件,客户端通常会收到一个 404 Not Found 或 400 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 | package com.example.springbootdemo.controller; |
运行验证
使用 cURL 访问 /users/search
路径,并附带查询参数。
1 | # 测试必需参数和默认值参数 |
1
正在搜索用户,关键词: admin, 页码: 1
1 | # 测试同时提供两个参数 |
1
正在搜索用户,关键词: admin, 页码: 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 | package com.example.springbootdemo.model; |
然后,在 UserController
中添加一个创建用户的方法。
文件路径: src/main/java/com/example/springbootdemo/controller/UserController.java
(修改)
1 | package com.example.springbootdemo.controller; |
重要信息: @RestController
是类级注解,让类成为返回数据的控制器;@RequestBody
是方法参数注解,能把请求体数据转成对象供方法使用,这两者是不同的,需要严格区分!
运行验证
我们使用 cURL 模拟一个 POST
请求,并通过 -H
指定 Content-Type
为 application/json
,使用 -d
传入 JSON 数据。
1 | curl -X POST \ |
1
成功创建用户: User(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 | package com.example.springbootdemo.controller; |
运行验证
我们像调用普通 GET 请求一样,在 URL 后面附上多个查询参数。
1 | curl "http://localhost:8080/users/filter?username=lisi&email=lisi@example.com" |
1
根据条件筛选用户: User(id=null, username=lisi, email=lisi@example.com)
核心区别: @RequestBody
用于处理一个 单一的、完整的请求体(通常是 JSON 或 XML)。而 POJO 直接接收参数的方式,则用于处理 零散的、多个的请求参数(通常是 URL 查询参数或表单)。一个方法中,@RequestBody
注解最多只能使用一次。