RESTful
本章是 Spring MVC 教程中"请求映射"章节的设计思想基础。在讲解
@GetMapping、@PostMapping等注解的语法之前,必须先理解 RESTful 设计思想——它回答了"URL 应该怎么设计""HTTP 方法应该怎么用"等根本问题。RESTful 不是 Spring MVC 的专属概念,但 Spring MVC 的注解体系(如@PathVariable、@RequestBody)是为 RESTful 风格量身打造的。不理解 RESTful,注解只是死记硬背的符号。
定义与作用
RESTful 是一种软件架构风格,全称 REpresentational State Transfer(表述性状态转移),由 Roy Fielding 在 2000 年的博士论文中提出。它的核心思想是:用 URL 定位资源,用 HTTP 方法描述对资源的操作。
传统 Web 开发中,URL 是"动作 + 对象"的混合体,如 /userDelete?id=1、/userView?id=1。RESTful 则要求 URL 只表示资源本身(名词),操作意图通过 HTTP 方法(GET/POST/PUT/DELETE)表达。同一个 URL /users/1,用 GET 访问是"查询",用 DELETE 访问是"删除",用 PUT 访问是"更新"。
RESTful 不是协议、不是标准、不是框架,它是一种设计约束。遵循这些约束的系统,天然具备可读性、可缓存性、可扩展性。Spring MVC 的 @RestController、@PathVariable、@RequestBody 等特性,本质上都是为了方便开发者构建 RESTful 风格的接口。
生活类比:图书馆管理系统
想象你去图书馆借书:
- 传统 URL 风格:图书馆有 4 个窗口,分别写着"查询图书窗口"、
"借阅图书窗口"、"归还图书窗口"、"删除图书记录窗口"。每个窗口的办事流程完全不同,你要记住哪个窗口办什么事。 - RESTful 风格:图书馆只有一个"图书资源柜台"(
/books)。你走到柜台前,用不同的"办事方式"表达意图:- GET
/books/123→ "请把 123 号图书的信息给我看看" - POST
/books→ "我要新增一本图书,信息在申请表里" - PUT
/books/123→ "123 号图书的信息变了,请按这张表更新" - DELETE
/books/123→ "请把 123 号图书记录注销掉"
- GET
这个类比的关键在于:URL 只标识资源(图书),动作由 HTTP 方法表达。柜台工作人员(服务器)看到 GET 就知道是查询,看到 DELETE 就知道是删除,无需在 URL 里写动词。
核心原理
RESTful 的六大设计原则
| 原则 | 含义 | 在 Web 开发中的体现 |
|---|---|---|
| 资源识别 | 系统中的一切都是资源,每个资源有唯一标识 | URL 如 /users/1、/orders/2024-001 |
| 统一接口 | 对资源的操作通过统一的 HTTP 方法表达 | GET 查询、POST 创建、PUT 全量更新、DELETE 删除 |
| 无状态 | 服务器不保存客户端的上下文状态 | 每个请求携带完整信息(如 Token),服务器不依赖之前的请求 |
| 可缓存 | 响应可以被客户端或中间层缓存 | GET 响应可设 Cache-Control,减少重复请求 |
| 分层系统 | 客户端不需要知道是否直连服务器 | 负载均衡、CDN、反向代理对客户端透明 |
| 按需代码 | 服务器可向客户端下发可执行代码(可选) | JavaScript 脚本动态下发(较少使用) |
重点理解前三个原则:
资源识别要求开发者把业务对象抽象为资源。"用户"是资源、
"订单"是资源、"文章评论"也是资源。资源可以嵌套:/articles/42/comments/3表示"42 号文章的 3 号评论"。统一接口是 RESTful 最直观的特征。同样的 URL,不同的 HTTP 方法,含义完全不同。这要求前端开发者理解 HTTP 协议语义,不能把所有操作都用 POST 完成。
无状态意味着服务器不会在内存中保存"当前用户是谁"这类会话信息。每个请求必须自包含认证凭证(如 JWT Token 或 Session ID Cookie)。这让服务器可以水平扩展——请求发到任意一台机器都能正确处理。
RESTful URL 设计规范
| 规范 | 正确示例 | 错误示例 | 说明 |
|---|---|---|---|
| 用名词,不用动词 | GET /users | GET /getUsers | URL 标识资源,动作由 HTTP 方法表达 |
| 用复数形式 | GET /users/1 | GET /user/1 | 集合资源用复数,表示一类资源 |
层级关系用 / 分隔 | GET /users/1/orders | GET /userOrders?userId=1 | 体现资源嵌套关系 |
| 避免查询参数做主要标识 | GET /users/1 | GET /users?id=1 | 资源 ID 应放在路径中 |
| 查询参数仅用于过滤/分页 | GET /users?page=2&size=10 | — | ? 后的参数是查询条件,不是资源标识 |
| 统一使用小写 | /user-profiles | /UserProfiles | 避免大小写敏感问题 |
用连字符 - 分隔单词 | /user-profiles | /user_profiles | 下划线在 URL 中可能被浏览器隐藏 |
HTTP 方法与 CRUD 对应关系
| HTTP 方法 | CRUD 操作 | 幂等性 | 用途 | Spring MVC 注解 |
|---|---|---|---|---|
| GET | Read | 幂等 | 查询资源 | @GetMapping |
| POST | Create | 非幂等 | 创建资源 | @PostMapping |
| PUT | Update | 幂等 | 全量更新资源 | @PutMapping |
| PATCH | Update | 非幂等 | 部分更新资源 | @PatchMapping |
| DELETE | Delete | 幂等 | 删除资源 | @DeleteMapping |
幂等性说明:幂等操作执行一次和执行 N 次,结果相同。GET/PUT/DELETE 是幂等的——多次查询不会改数据,多次全量更新为同样内容结果不变,多次删除同一资源最终都是"已删除"。POST 是非幂等的——两次 POST 可能创建两条记录。
RESTful vs 传统 URL 设计对比
| 操作 | 传统 URL 风格 | RESTful 风格 | 对比分析 |
|---|---|---|---|
| 查询用户列表 | GET /user/list | GET /users | RESTful 用复数名词,无需 list 动词 |
| 查询单个用户 | GET /user/view?id=1 | GET /users/1 | RESTful 用路径参数标识资源,URL 更简洁 |
| 创建用户 | POST /user/add | POST /users | RESTful 用 POST 表达"创建",URL 无动词 |
| 更新用户 | POST /user/update?id=1 | PUT /users/1 | RESTful 用 PUT 表达"更新",语义更精确 |
| 删除用户 | GET /userDelete?id=1 | DELETE /users/1 | 传统风格用 GET 做删除,违背 HTTP 语义且不安全 |
| 查询用户的订单 | GET /orderList?userId=1 | GET /users/1/orders | RESTful 用层级路径表达资源关系 |
RESTful 架构风格图解
图解说明:
- 资源层:
/users、/orders等 URL 只标识资源,不表达动作 - 方法层:GET/POST/PUT/DELETE 表达要对资源做什么
- 状态层:服务器上的资源状态被 HTTP 方法改变,客户端通过响应获取资源的"表述"(Representation)
完整示例
场景
飞翔科技要开发一个员工管理系统 API,供内部 OA 系统和移动端 App 调用。CTO 大翔要求接口设计必须遵循 RESTful 规范,"让 URL 一看就懂,让 HTTP 方法用对地方"。架构师白歌负责制定 URL 设计规范,小崔负责 Controller 实现,黄俪负责前端对接,李眉负责 API 文档和测试。
白歌的 RESTful 设计规范
资源:员工(employees)、部门(departments)
GET /employees → 查询员工列表(支持 ?dept=技术部&page=1)
GET /employees/{id} → 查询指定员工
POST /employees → 创建新员工(请求体含员工信息)
PUT /employees/{id} → 全量更新员工信息
PATCH /employees/{id} → 部分更新(如只改手机号)
DELETE /employees/{id} → 删除员工
GET /departments/{id}/employees → 查询某部门下的所有员工
黄俪的反馈:"以前对接的接口全是 POST,现在看到 GET 就知道是查数据,看到 DELETE 就知道要小心。URL 里没动词,但 HTTP 方法本身就是动词,组合起来语义特别清晰。"
小崔的 Controller 实现
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
// GET /employees?page=1&size=10
@GetMapping
public Page<Employee> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return employeeService.findPage(page, size);
}
// GET /employees/1001
@GetMapping("/{id}")
public Employee getById(@PathVariable Long id) {
return employeeService.findById(id);
}
// POST /employees
@PostMapping
public ResponseEntity<Employee> create(@RequestBody @Valid EmployeeDTO dto) {
Employee employee = employeeService.create(dto);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(employee.getId())
.toUri();
return ResponseEntity.created(location).body(employee);
}
// PUT /employees/1001
@PutMapping("/{id}")
public Employee update(@PathVariable Long id, @RequestBody @Valid EmployeeDTO dto) {
return employeeService.update(id, dto);
}
// DELETE /employees/1001
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
employeeService.delete(id);
}
}
代码分析:
@RestController表明该类所有方法返回数据(JSON),而非视图名@PathVariable从 URL 路径中提取资源 ID,如/employees/1001中的1001@RequestBody从请求体中解析 JSON 数据,映射为 DTO 对象ResponseEntity.created(location)返回 201 状态码,并在响应头中携带新资源的 URL——这是 RESTful 的规范做法
李眉的 API 测试用例
# 查询员工列表
curl -X GET http://localhost:8080/employees?page=1&size=5
# 查询单个员工
curl -X GET http://localhost:8080/employees/1001
# 创建员工
curl -X POST http://localhost:8080/employees \
-H "Content-Type: application/json" \
-d '{"name":"张三","dept":"技术部","phone":"13800138000"}'
# 更新员工
curl -X PUT http://localhost:8080/employees/1001 \
-H "Content-Type: application/json" \
-d '{"name":"张三","dept":"架构部","phone":"13800138000"}'
# 删除员工
curl -X DELETE http://localhost:8080/employees/1001
李眉的测试报告:
- GET 请求无副作用,可重复执行,适合缓存
- POST 创建后返回 201 + Location 头,前端可直接跳转新资源页
- DELETE 返回 204 No Content,表示操作成功但无响应体——符合 RESTful 规范
易错场景与面试考点
误区一:把所有操作都用 POST
错误设计:
@RestController
public class UserController {
@PostMapping("/user/query") // ❌ 查询用 POST
public User query(@RequestBody QueryForm form) { }
@PostMapping("/user/delete") // ❌ 删除用 POST
public void delete(@RequestBody DeleteForm form) { }
@PostMapping("/user/update") // ❌ 更新用 POST
public void update(@RequestBody UpdateForm form) { }
}
纠正:这种做法被称为"HTTP 协议滥用"或"伪 RESTful"。虽然功能上能跑,但丧失了 RESTful 的核心优势:
- 缓存失效:POST 响应默认不可缓存,而 GET 响应可被浏览器、CDN 缓存
- 语义模糊:运维人员看日志时,无法从 HTTP 方法判断操作类型
- 工具支持缺失:浏览器直接输入 URL 只能发 GET,反向代理的安全规则通常按 HTTP 方法配置
误区二:URL 里混用动词和名词
错误设计:
GET /users/getUserInfo/1 ❌ 动词 getUserInfo 多余
POST /users/createUser ❌ 动词 create 多余,POST 已表达创建
POST /users/1/updatePhone ❌ 动词 update 多余,PUT 已表达更新
纠正:
GET /users/1 ✅ 查询用户信息
POST /users ✅ 创建用户
PUT /users/1 ✅ 更新用户(或 PATCH /users/1 部分更新)
误区三:在 URL 里放动作状态
错误设计:POST /users/1/enable 表示"启用用户",POST /users/1/disable 表示"禁用用户"。
纠正:"启用/禁用"是资源状态的变更,应该用 PUT/PATCH 更新资源的 status 字段:
PATCH /users/1
{ "status": "ENABLED" }
如果业务上"启用"是一个复杂操作(涉及发送邮件、初始化权限等),可以设计为子资源或动作资源:
POST /users/1/activation # 将"激活"视为一个独立资源创建
面试高频:RESTful 的幂等性是什么意思?哪些方法是幂等的?
标准回答:
幂等性是指同样的操作执行一次和执行多次,对系统状态的影响相同。
- GET:幂等。多次查询不会修改数据。
- PUT:幂等。将用户年龄改为 25,执行 1 次和 100 次结果相同。
- DELETE:幂等。删除 ID 为 1 的用户,第一次删除后用户不存在;再执行 99 次,用户仍然不存在——系统状态不变。
- POST:非幂等。两次 POST
/orders会创建两条订单记录。 - PATCH:通常非幂等。如
PATCH { "age": "+1" }执行两次会加 2 岁。
实际意义:幂等操作在网络超时后可以安全重试,不会导致重复副作用。例如 DELETE 请求超时后重试,不会误删其他数据。
小结
RESTful 是一种用 URL 标识资源、用 HTTP 方法表达操作的架构风格。它的六大原则(资源识别、统一接口、无状态、可缓存、分层系统、按需代码)指导开发者设计出语义清晰、可缓存、可扩展的 Web API。
Spring MVC 的 @RestController、@PathVariable、@RequestBody、ResponseEntity 等特性,本质上都是为 RESTful 风格服务的。理解 RESTful 后,这些注解不再是孤立的语法点,而是"如何用 Spring MVC 实现 RESTful 设计"的具体工具。
本章与全局的关系:本章回答了"URL 应该怎么设计""HTTP 方法应该怎么用"。下一章"@RequestMapping 与派生注解"将深入讲解 Spring MVC 如何用注解把 RESTful 设计映射为具体代码。