CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
REST 资源设计
这一课你将能做到
设计出符合 REST 规范的 URI,避免动词、层级过深等常见错误
实现 API 版本控制,掌握 URI 版本、Header 版本、参数版本三种方案
设计分页、排序、过滤的查询接口,并保证幂等性
为什么要学
URI 是 API 的门面。/api/getLesson?id=1 和 /api/lessons/1 哪个更专业?答案显而易见。版本策略决定你的 API 能不能平滑升级,分页设计影响性能和用户体验。面试必问:怎么设计幂等接口?API 版本怎么管理?
先建立图像
资源层级设计:
┌────────────────────────────────────────┐
│ /api/tracks/java-web │ ← 学习路径(集合)
│ /api/tracks/java-web/lessons │ ← 路径下的课程(子集合)
│ /api/lessons/123 │ ← 具体课程(单个资源)
│ /api/lessons/123/submissions │ ← 课程的提交记录(子集合)
│ /api/submissions/456 │ ← 具体提交(单个资源)
└────────────────────────────────────────┘
分页响应结构:
{
"code": 200,
"data": {
"content": [...], ← 当前页数据
"page": 0, ← 当前页码(0-based)
"size": 20, ← 每页大小
"totalElements": 156, ← 总条数
"totalPages": 8 ← 总页数
}
}
跟着做
步骤 1:URI 设计规范
好的 URI:
GET /api/lessons # 课程列表
GET /api/lessons/123 # 单个课程
POST /api/lessons # 创建课程
PUT /api/lessons/123 # 全量更新
PATCH /api/lessons/123 # 部分更新
DELETE /api/lessons/123 # 删除
GET /api/lessons/123/submissions # 某课程的提交列表
POST /api/lessons/123/submissions # 提交作业
坏的 URI:
GET /api/getLesson?id=123 # 动词 get
POST /api/createLesson # 动词 create
GET /api/lesson/list # 单数 + 动词
GET /api/lessons/123/submissions/456/comments/789/likes # 层级太深
原则:URI 里只有名词,用 HTTP 方法表达动作;层级不超过 3 层。
步骤 2:版本控制
三种主流方案:
方案 A:URI 版本(最直观)
GET /api/v1/lessons
GET /api/v2/lessons
@RestController
@RequestMapping("/api/v1/lessons")
public class LessonV1Controller { ... }
@RestController
@RequestMapping("/api/v2/lessons")
public class LessonV2Controller { ... }
优点:简单、可缓存、浏览器直接访问。缺点:URI 污染。
方案 B:Header 版本(最优雅)
GET /api/lessons
Accept: application/vnd.codeforge.v1+json
@GetMapping(produces = "application/vnd.codeforge.v1+json")
public ApiResponse<List<LessonV1>> listV1() { ... }
@GetMapping(produces = "application/vnd.codeforge.v2+json")
public ApiResponse<List<LessonV2>> listV2() { ... }
优点:URI 干净。缺点:测试不方便(需要改 header),缓存复杂。
GET /api/lessons?version=1
优点:实现简单。缺点:不 RESTful,缓存不友好。
推荐:对外公开 API 用 URI 版本(v1/v2),内部服务用 Header 版本。
步骤 3:分页实现 @GetMapping
public ApiResponse<Page<Lesson>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id,desc") String sort) {
// sort 格式:字段,方向
String[] sortParams = sort.split(",");
Sort.Direction direction = sortParams.length > 1 && "desc".equalsIgnoreCase(sortParams[1])
? Sort.Direction.DESC : Sort.Direction.ASC;
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortParams[0]));
Page<Lesson> lessons = lessonRepository.findAll(pageable);
return ApiResponse.success(lessons);
}
{
"code": 200,
"data": {
"content": [...],
"page": 0,
"size": 20,
"totalElements": 156,
"totalPages": 8,
"first": true,
"last": false
}
}
步骤 4:过滤与搜索 @GetMapping("/search")
public ApiResponse<Page<Lesson>> search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String trackId,
@RequestParam(required = false) String difficulty,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
LessonQuery query = LessonQuery.builder()
.keyword(keyword)
.trackId(trackId)
.difficulty(difficulty)
.build();
Page<Lesson> result = lessonService.search(query, PageRequest.of(page, size));
return ApiResponse.success(result);
}
步骤 5:幂等性设计 问题:用户双击「提交」按钮,或网络超时重试,导致重复创建。
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "lesson_id"}))
public class Submission { ... }
curl -X POST http://localhost:8080/api/submissions \
-H "Content-Type: application/json" \
-H "Idempotency-Key: a1b2c3d4-e5f6-7890" \
-d '{"lessonId":123,"code":"..."}'
@PostMapping
public ApiResponse<Submission> create(
@RequestBody @Valid SubmissionRequest request,
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey) {
if (idempotencyKey != null) {
Optional<Submission> existing = submissionRepository
.findByIdempotencyKey(idempotencyKey);
if (existing.isPresent()) {
return ApiResponse.success(existing.get());
}
}
Submission submission = submissionService.create(request, idempotencyKey);
return ApiResponse.success(submission);
}
方案三:PUT 代替 POST(客户端生成 ID)
curl -X PUT http://localhost:8080/api/submissions/a1b2c3d4 \
-H "Content-Type: application/json" \
-d '{"lessonId":123,"code":"..."}'
讲清楚要点
URI 是资源的地址,不是操作的命令 :把 URI 想成「文件路径」,HTTP 方法是「操作类型」。/api/lessons/123 是「第 123 号课程」这个资源。
版本控制是 API 的生命线 :一旦发布就不能随便改字段、删字段。新增字段可以,改类型、删字段必须升版本。
分页必须返回总数 :前端需要知道「共 156 条,分 8 页」才能渲染分页组件。只返当前页数据是不够的。
过滤参数放 Query String :?trackId=java-web&difficulty=easy 比 /api/lessons/track/java-web/difficulty/easy 更灵活,参数可选、可组合。
幂等性不是可选的 :网络不可靠,用户会重试,消息队列会重发。写操作必须考虑「执行一次和执行十次结果一样」。
你可能会懵的地方
PUT 和 PATCH 在版本升级时怎么处理?
v1 用 PUT 全量更新,v2 可以引入 PATCH 支持部分更新。但注意:如果 v1 的客户端不传某个字段,v2 的 PATCH 不能把这个字段清空。版本升级时要保持向后兼容。
分页用 page 还是 cursor?
page 适合「跳页」场景(第 1 页、第 5 页);cursor 适合「无限滚动」(微信朋友圈)。cursor 性能更好(不需要 OFFSET),但不能跳页。大数据量推荐 cursor。
批量操作怎么设计?
批量创建:POST /api/lessons/batch,body 传数组。批量删除:DELETE /api/lessons?ids=1,2,3 或 POST /api/lessons/batch-delete。批量操作要限制数量(如最多 100 条),防止超时。
HATEOAS 是什么?需要实现吗?
HATEOAS(Hypermedia as the Engine of Application State)是 REST 的最高成熟度:响应里带相关资源的链接。比如返回课程时带 "links": {"submissions": "/api/lessons/123/submissions"}。实际项目中很少全量实现,太啰嗦。
Spring Data REST 能用吗?
Spring Data REST 自动生成 Repository 的 REST API,适合原型和内部系统。但可控性差,复杂业务建议手写 Controller。
错法对照 错法 问题 正解 /api/lessons/getByTrackId?trackId=1URI 里出现动词 /api/lessons?trackId=1升级 API 直接改 v1 的返回结构 老客户端崩溃 新增 v2,v1 保持不动 分页只返 List<Lesson> 前端不知道总页数 返回 Page<Lesson> 或自定义分页对象 批量删除用 GET 危险,可能被缓存或预取 POST /api/lessons/batch-delete 过滤条件用路径参数 参数可选性受限,组合爆炸 Query String
卡点急救
分页性能差 :OFFSET 100000 LIMIT 20 会扫描前 10 万条。大表用 cursor 分页(WHERE id > lastId LIMIT 20)。
版本冲突 :两个客户端同时更新同一资源,后提交的覆盖先提交的。用乐观锁(@Version)或 ETag。
幂等键失效 :Idempotency-Key 存 Redis 要设过期时间(如 24 小时),防止无限膨胀。
面试常问
REST 成熟度模型(Richardson Maturity Model)?
Level 0:POX(Plain Old XML),用 POST 发 XML,URI 无意义。Level 1:Resources,URI 标识资源,但仍用 POST 做所有操作。Level 2:HTTP Verbs,用 GET/POST/PUT/DELETE,状态码正确。Level 3:Hypermedia(HATEOAS),响应带链接。大多数公司做到 Level 2。
怎么保证接口幂等?
数据库唯一约束、幂等键(Idempotency-Key)、乐观锁、状态机(已支付不能重复支付)。能结合场景讲 2-3 种方案。
API 版本策略怎么选?
URI 版本简单直观,适合对外 API;Header 版本干净,适合内部微服务;参数版本灵活但缓存不友好。能说清优缺点和适用场景。
自测三问
设计「用户收藏课程」的 API,URI 和方法怎么写?考虑取消收藏。
如果课程表有 1000 万条数据,分页查询 page=50000&size=20 会有什么问题?怎么优化?
为什么 PUT 是幂等的,POST 不是?各举一个业务场景。
小结一句话 REST 资源设计的核心是「名词 + 动词分离」:URI 定位资源,HTTP 方法表达操作;版本控制和幂等性是生产环境 API 的必备设计。
延伸