3-1 RESTful API 设计
概念解析
REST 六大原则
| 原则 | 说明 |
|---|---|
| 客户端-服务器分离 | 客户端与服务端独立演化 |
| 无状态 | 每个请求包含所有必要信息 |
| 可缓存 | 响应可标记为可缓存 |
| 分层系统 | 允许中间层 |
| 统一接口 | 资源通过 URI 标识 |
| 按需代码 | 可返回可执行代码(可选) |
RESTful 动词
| 方法 | 语义 | 幂等 | 安全 |
|---|---|---|---|
| GET | 查询资源 | ✅ | ✅ |
| POST | 创建资源 | ❌ | ❌ |
| PUT | 更新资源(完整) | ✅ | ❌ |
| PATCH | 更新资源(部分) | ❌ | ❌ |
| DELETE | 删除资源 | ✅ | ❌ |
HTTP 状态码
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | OK | 成功响应 |
| 201 | Created | 资源创建成功 |
| 204 | No Content | 删除成功,无返回 |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 资源冲突 |
| 500 | Server Error | 服务器内部错误 |
代码示例
1. RESTful Controller
@RestController
@RequestMapping("/api/users")
@Api(tags = "用户管理")
public class UserController {
@Autowired
private UserService userService;
// GET /api/users
@GetMapping
public Result<List<User>> list(
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
Page<User> result = userService.query(name, page, size);
return Result.success(result.getContent(), result.getTotalElements());
}
// GET /api/users/{id}
@GetMapping("/{id}")
public Result<User> get(@PathVariable Long id) {
return Result.success(userService.getById(id));
}
// POST /api/users
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Result<User> create(@Valid @RequestBody UserDTO dto) {
User user = userService.create(dto);
return Result.success(user);
}
// PUT /api/users/{id}
@PutMapping("/{id}")
public Result<User> update(
@PathVariable Long id,
@Valid @RequestBody UserDTO dto) {
dto.setId(id);
return Result.success(userService.update(dto));
}
// DELETE /api/users/{id}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.success(null);
}
// PATCH /api/users/{id}/status
@PatchMapping("/{id}/status")
public Result<Void> updateStatus(
@PathVariable Long id,
@RequestParam Integer status) {
userService.updateStatus(id, status);
return Result.success(null);
}
}
2. 统一响应格式
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
private long timestamp;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data, System.currentTimeMillis());
}
public static <T> Result<T> success(T data, long total) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
Map<String, Object> pageData = new HashMap<>();
pageData.put("list", data);
pageData.put("total", total);
result.setData((T) pageData);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null, System.currentTimeMillis());
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}
}
3. API 版本管理
// 方式一:URL 路径版本
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { }
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
// v2 可以增加新字段,改变响应结构
}
// 方式二:Header 版本
@GetMapping(value = "/users", headers = "X-API-VERSION=1")
public Result<User> getV1(@PathVariable Long id) { }
@GetMapping(value = "/users", headers = "X-API-VERSION=2")
public Result<UserDTO> getV2(@PathVariable Long id) { }
// 方式三:参数版本
@GetMapping(value = "/users", params = "version=1")
public Result<User> getV1(@PathVariable Long id) { }
4. 分页查询
// 分页请求 DTO
@Data
public class PageRequest {
@Min(1)
private int page = 1;
@Min(1)
@Max(100)
private int size = 10;
}
// 使用 PageHelper(MyBatis 分页)
@GetMapping
public Result<PageInfo<User>> list(PageRequest request) {
PageHelper.startPage(request.getPage(), request.getSize());
List<User> users = userService.list();
PageInfo<User> pageInfo = new PageInfo<>(users);
return Result.success(pageInfo);
}
// 使用 JPA Specification
@GetMapping
public Result<Page<User>> list(UserQuery query) {
Specification<User> spec = (root, cq, cb) -> {
if (query.getName() != null) {
cq.where(cb.like(root.get("name"), "%" + query.getName() + "%"));
}
cq.orderBy(cb.desc(root.get("createTime")));
return cq;
};
return Result.success(userRepository.findAll(spec, PageRequest.of(query.getPage() - 1, query.getSize())));
}
常见坑点
⚠️ 坑 1:POST 和 PUT 混淆
| 场景 | 方法 | 说明 |
|---|---|---|
| 创建资源 | POST | 服务端生成 ID |
| 更新资源(完整) | PUT | 客户端提供完整资源 |
| 更新资源(部分) | PATCH | 只更新部分字段 |
⚠️ 坑 2:状态码使用错误
// ❌ 创建成功后返回 200
@PostMapping
public Result<User> create(@RequestBody UserDTO dto) {
return Result.success(userService.create(dto)); // 应返回 201
}
// ✅
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Result<User> create(@RequestBody UserDTO dto) {
return Result.success(userService.create(dto));
}
⚠️ 坑 3:RESTful 过度设计
// ❌ 过度 RESTful
GET /api/users/{id}/orders/{orderId}/items/{itemId}
// ✅ 资源嵌套不要太深(建议不超过 2 层)
GET /api/items/{itemId} // 通过 itemId 直接获取
面试题
Q1:GET 和 POST 的区别?
参考答案:
| 维度 | GET | POST |
|---|---|---|
| 参数位置 | URL query string | Request body |
| 长度限制 | 浏览器限制(约 2KB) | 无限制 |
| 缓存 | 可缓存 | 不可缓存 |
| 安全性 | 参数暴露在 URL | 参数在 body 中 |
| 幂等性 | 幂等 | 非幂等 |
| 编码 | URL 编码 | 多种编码 |
Q2:PUT 和 PATCH 的区别?
参考答案:
-
PUT:完整更新,幂等操作
PUT /users/1 { "name": "张三", "age": 25, "email": "zhangsan@example.com" // 必须全部字段 } -
PATCH:部分更新,非幂等
PATCH /users/1 { "age": 26 // 只更新 age }
Q3:如何保证 RESTful API 的安全性?
参考答案:
- 认证授权:JWT / OAuth2
- 参数校验:@Valid + BindingResult
- 防 XSS:输入过滤
- 防 SQL 注入:使用参数化查询
- 限流:接口限流防止滥用
- HTTPS:全程加密
- 敏感数据脱敏:日志中隐藏敏感信息