一、RESTful API 概述
1. 什么是 REST?
REST(Representational State Transfer):表述性状态转移,是一种软件架构风格,用于设计网络应用的 API。
2. REST 的核心原则
| 原则 | 说明 |
|---|---|
| 无状态 | 每个请求包含所有信息 |
| 统一接口 | 使用标准 HTTP 方法 |
| 资源导向 | 一切皆是资源 |
| 可缓存 | 响应可被缓存 |
| 分层系统 | 客户端不知道是连接到服务器还是代理 |
3. RESTful API 的特点
| 特点 | 说明 |
|---|---|
| 使用 HTTP 动词 | GET、POST、PUT、DELETE |
| 使用 URI 标识资源 | /users/{id} |
| 使用标准状态码 | 200、201、404、500 |
| 使用 JSON 格式 | 请求和响应数据 |
二、HTTP 方法与资源操作
1. HTTP 方法对应 CRUD
| HTTP 方法 | CRUD 操作 | 说明 | 是否幂等 |
|---|---|---|---|
| GET | Read(查询) | 获取资源 | ✅ 幂等 |
| POST | Create(创建) | 创建新资源 | ❌ 不幂等 |
| PUT | Update(更新) | 更新整个资源 | ✅ 幂等 |
| PATCH | Update(部分更新) | 更新部分资源 | ❌ 不幂等 |
| DELETE | Delete(删除) | 删除资源 | ✅ 幂等 |
2. HTTP 方法详解
GET(查询)
用途:获取资源
示例:
GET /api/users # 获取所有用户
GET /api/users/1 # 获取 ID 为 1 的用户
GET /api/users?name=张 # 查询姓名包含"张"的用户
特点:
- 幂等:多次请求结果相同
- 可缓存:响应可被缓存
- 安全:不修改服务器数据
POST(创建)
用途:创建新资源
示例:
POST /api/users
Content-Type: application/json
{
"name": "张三",
"email": "zhangsan@example.com",
"password": "123456"
}
响应:
HTTP/1.1 201 Created
Location: /api/users/1
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}
特点:
- 不幂等:每次请求创建新资源
- 需要返回 201 状态码
- 返回新资源的 URI
PUT(更新)
用途:更新整个资源
示例:
PUT /api/users/1
Content-Type: application/json
{
"id": 1,
"name": "张三(已更新)",
"email": "zhangsan@example.com",
"phone": "13800138000"
}
特点:
- 幂等:多次请求结果相同
- 更新整个资源(所有字段)
- 资源不存在时可以创建(可选)
PATCH(部分更新)
用途:更新资源的部分字段
示例:
PATCH /api/users/1
Content-Type: application/json
{
"phone": "13900139000"
}
特点:
- 不幂等:每次请求可能产生不同结果
- 只更新提供的字段
- 更适合局部更新
DELETE(删除)
用途:删除资源
示例:
DELETE /api/users/1
响应:
HTTP/1.1 204 No Content
特点:
- 幂等:多次删除结果相同
- 返回 204 状态码
- 响应体为空
三、HTTP 状态码
1. 2xx 成功
| 状态码 | 说明 | 使用场景 |
|---|---|---|
| 200 OK | 请求成功 | GET、PUT、PATCH、DELETE 成功 |
| 201 Created | 资源创建成功 | POST 创建资源成功 |
| 204 No Content | 请求成功,无内容返回 | DELETE 删除成功 |
2. 3xx 重定向
| 状态码 | 说明 | 使用场景 |
|---|---|---|
| 301 Moved Permanently | 永久重定向 | 资源永久移动到新位置 |
| 302 Found | 临时重定向 | 资源临时移动到新位置 |
3. 4xx 客户端错误
| 状态码 | 说明 | 使用场景 |
|---|---|---|
| 400 Bad Request | 请求参数错误 | 数据验证失败 |
| 401 Unauthorized | 未授权 | 未登录或 Token 无效 |
| 403 Forbidden | 禁止访问 | 权限不足 |
| 404 Not Found | 资源不存在 | 查询的资源不存在 |
| 409 Conflict | 资源冲突 | 邮箱已被使用 |
| 422 Unprocessable Entity | 无法处理的实体 | 业务规则验证失败 |
4. 5xx 服务器错误
| 状态码 | 说明 | 使用场景 |
|---|---|---|
| 500 Internal Server Error | 服务器内部错误 | 服务器异常 |
| 503 Service Unavailable | 服务不可用 | 服务器过载或维护 |
四、URI 设计规范
1. URI 设计原则
| 原则 | 说明 |
|---|---|
| 使用名词 | URI 应该使用名词,而不是动词 |
| 使用复数 | 资源名称使用复数形式 |
| 层次结构 | 使用 / 表示层级关系 |
| 使用小写 | URI 使用小写字母 |
| 使用连字符 | 多个单词使用连字符连接 |
2. URI 设计示例
| 不推荐 | 推荐 | 说明 |
|---|---|---|
/getUser | /users | 使用名词,不用动词 |
/user | /users | 使用复数 |
/Users | /users | 使用小写 |
/getUserById | /users/{id} | 资源导向 |
/getUserOrders | /users/{id}/orders | 层级结构 |
3. 复杂查询
分页查询:
GET /api/users?page=1&size=10
排序查询:
GET /api/users?sort=id,desc
过滤查询:
GET /api/users?name=张&status=active
组合查询:
GET /api/users?page=1&size=10&sort=id,desc&name=张&status=active
五、Spring Boot 实现 RESTful API
1. 创建 Controller
UserController.java:
package com.example.myapp.controller;
import com.example.myapp.dto.CreateUserDTO;
import com.example.myapp.dto.UpdateUserDTO;
import com.example.myapp.dto.UserDTO;
import com.example.myapp.model.User;
import com.example.myapp.service.UserService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// GET /api/users - 获取所有用户(分页)
@GetMapping
public ResponseEntity<Page<UserDTO>> findAll(Pageable pageable) {
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
// GET /api/users/{id} - 获取单个用户
@GetMapping("/{id}")
public ResponseEntity<UserDTO> findById(@PathVariable Long id) {
UserDTO user = userService.findById(id);
return ResponseEntity.ok(user);
}
// POST /api/users - 创建用户
@PostMapping
public ResponseEntity<UserDTO> create(@Valid @RequestBody CreateUserDTO userDTO) {
UserDTO createdUser = userService.create(userDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
// PUT /api/users/{id} - 更新用户
@PutMapping("/{id}")
public ResponseEntity<UserDTO> update(
@PathVariable Long id,
@Valid @RequestBody UpdateUserDTO userDTO
) {
UserDTO updatedUser = userService.update(id, userDTO);
return ResponseEntity.ok(updatedUser);
}
// PATCH /api/users/{id} - 部分更新用户
@PatchMapping("/{id}")
public ResponseEntity<UserDTO> partialUpdate(
@PathVariable Long id,
@RequestBody UpdateUserDTO userDTO
) {
UserDTO updatedUser = userService.partialUpdate(id, userDTO);
return ResponseEntity.ok(updatedUser);
}
// DELETE /api/users/{id} - 删除用户
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
// GET /api/users/search?name=xxx - 搜索用户
@GetMapping("/search")
public ResponseEntity<Page<UserDTO>> search(
@RequestParam String name,
Pageable pageable
) {
Page<UserDTO> users = userService.searchByName(name, pageable);
return ResponseEntity.ok(users);
}
}
2. DTO 设计
UserDTO(响应 DTO)
package com.example.myapp.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class UserDTO {
private Long id;
private String name;
private String email;
private String phone;
// 从 Entity 转换为 DTO
public static UserDTO fromEntity(User user) {
return new UserDTO(
user.getId(),
user.getName(),
user.getEmail(),
user.getPhone()
);
}
}
CreateUserDTO(创建用户 DTO)
package com.example.myapp.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class CreateUserDTO {
@NotBlank(message = "姓名不能为空")
@Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
private String name;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 100, message = "密码长度必须在 6-100 个字符之间")
private String password;
@Size(max = 20, message = "电话号码长度不能超过 20 个字符")
private String phone;
}
UpdateUserDTO(更新用户 DTO)
package com.example.myapp.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class UpdateUserDTO {
@Size(min = 2, max = 50, message = "姓名长度必须在 2-50 个字符之间")
private String name;
@Email(message = "邮箱格式不正确")
private String email;
@Size(max = 20, message = "电话号码长度不能超过 20 个字符")
private String phone;
}
六、分页与排序
1. Spring Data 分页
Controller
@GetMapping
public ResponseEntity<Page<UserDTO>> findAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir
) {
Sort sort = sortDir.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
响应示例
{
"content": [
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com",
"phone": "13800138000"
},
{
"id": 2,
"name": "李四",
"email": "lisi@example.com",
"phone": "13900139000"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
}
},
"totalPages": 1,
"totalElements": 2,
"last": true,
"numberOfElements": 2,
"first": true,
"empty": false
}
2. 前端分页示例
React 示例
import { useState, useEffect } from 'react';
import axios from 'axios';
function UserList() {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(0);
const [size, setSize] = useState(10);
const [totalPages, setTotalPages] = useState(0);
const fetchUsers = async () => {
const response = await axios.get('/api/users', {
params: { page, size, sortBy: 'id', sortDir: 'desc' }
});
setUsers(response.data.content);
setTotalPages(response.data.totalPages);
};
useEffect(() => {
fetchUsers();
}, [page]);
return (
<div>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>电话</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.phone}</td>
</tr>
))}
</tbody>
</table>
<div>
<button
disabled={page === 0}
onClick={() => setPage(page - 1)}
>
上一页
</button>
<span>第 {page + 1} / {totalPages} 页</span>
<button
disabled={page >= totalPages - 1}
onClick={() => setPage(page + 1)}
>
下一页
</button>
</div>
</div>
);
}
七、API 版本控制
1. URI 版本控制
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
// V1 版本
}
@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {
// V2 版本
}
示例:
GET /api/v1/users
GET /api/v2/users
2. 请求头版本控制
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
@RequestMapping(headers = "X-API-Version=1")
public ResponseEntity<List<UserDTO>> findAllV1() {
// V1 版本
}
@GetMapping
@RequestMapping(headers = "X-API-Version=2")
public ResponseEntity<Page<UserDTO>> findAllV2(Pageable pageable) {
// V2 版本(支持分页)
}
}
请求:
GET /api/users
X-API-Version: 2
3. 查询参数版本控制
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public ResponseEntity<?> findAll(
@RequestParam(required = false, defaultValue = "1") String version,
Pageable pageable
) {
if ("1".equals(version)) {
// V1 版本
} else if ("2".equals(version)) {
// V2 版本
}
}
}
请求:
GET /api/users?version=2
八、最佳实践
1. 统一响应格式
统一响应类
ApiResponse.java:
package com.example.myapp.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}
使用统一响应
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<UserDTO>> findById(@PathVariable Long id) {
UserDTO user = userService.findById(id);
return ResponseEntity.ok(ApiResponse.success(user));
}
}
响应:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}
}
2. API 文档
Swagger 配置
添加依赖:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
访问 Swagger UI:
http://localhost:8080/swagger-ui.html
3. 安全性
HTTPS
生产环境必须使用 HTTPS。
认证与授权
使用 JWT Token 认证,确保 API 安全。
限流
防止恶意攻击,限制 API 调用频率。
九、总结
| 概念 | 说明 |
|---|---|
| REST | 表述性状态转移 |
| RESTful API | 遵循 REST 风格的 API |
| HTTP 方法 | GET、POST、PUT、PATCH、DELETE |
| HTTP 状态码 | 2xx、3xx、4xx、5xx |
| URI 设计 | 使用名词、复数、小写 |
| 分页排序 | Pageable 接口 |
| 版本控制 | URI、请求头、查询参数 |