引言
在现代 Web 开发中,RESTful API 已成为应用程序之间通信的标准方式。
REST(Representational State Transfer,表述性状态转移)是一种软件架构风格。REST 定义了一组约束条件和原则,用于创建可扩展、松耦合的 Web 服务。
REST 不是必须遵守的标准,只是一种设计风格。
是否采用 REST 取决于业务场景,但遵循 REST 风格通常能带来更好的可扩展性、可维护性和可读性。
概念解释
Representational (表述)
REST 中的 R(Representational) 指的是"资源的表述"。
资源本身是抽象概念(例如数据库里的用户记录),客户端从来不会直接摸到资源,而是拿到它的 表述------也就是一个 JSON、XML 或 HTML 文档。
比如 GET /users/123 返回:
json
{
"id": 123,
"name": "张三",
"email": "zhangsan@example.com"
}
这个 JSON 就是用户 123 当前状态的一份"表述"。
同样,创建用户时,客户端向 POST /users 发送一份 JSON 表述,服务端根据这份表述去创建一个新资源。
"状态转移"又是什么意思?
客户端通过服务端返回的资源表述,以及表述中可能包含的超链接,来知道接下来可以做什么,从而驱动应用状态发生变化。
举个例子:
客户端调用 GET /users/123 拿到用户信息,这段信息里可能包含一个 orders 链接字段:
json
{
"id": 123,
"name": "张三",
"links": {
"orders": "/users/123/orders"
}
}
客户端看到这个链接,就知道访问 /users/123/orders 可以查看该用户的订单。
用户从"浏览用户详情"转移到"浏览用户订单"这个新的界面状态,就靠返回数据里的链接来驱动 ------ 这就是 "表述性状态转移" 的字面由来:状态转移由资源的表述来引导。
(实际项目中这个"超链接"部分用得不多,但概念如此;简单的 CRUD 接口即使不包含链接,只要遵守了 URL 名词化 + 正确 HTTP 方法,大家也习惯叫它 RESTful。)
RESTful API
RESTful API 是遵循 REST 架构风格的 Web API 设计风格。它基于 HTTP 协议,通过 URL 定位资源,用 HTTP 方法(GET、POST 等)描述操作,实现客户端与服务器之间的交互。
一句话就是:
URL 中只表示资源,操作由 HTTP 动词来表达。
示例:RESTful 风格的用户接口:
java
GET /users # 获取用户列表
GET /users/1 # 获取 id=1 的用户
POST /users # 创建用户
PUT /users/1 # 更新 id=1 的用户
DELETE /users/1 # 删除 id=1 的用户
RESTful API 的特点
- 无状态:每个请求包含处理所需的所有信息
- 统一接口:使用标准 HTTP 方法进行操作
- 资源导向:所有内容都被抽象为资源
资源和 URI
在 REST 中,所有事物都被抽象为资源,URI 的设计遵循一定的规范:
- 使用名词而非动词表示资源
- 使用复数形式命名集合,用路径表示资源层级关系
- 避免文件扩展名,用 Accept 请求头指定返回格式
- 查询参数用于过滤、分页、排序
示例:
java
GET /users # 获取用户集合
GET /users/123 # 获取ID为123的用户,配合 Accept: application/json
GET /users/123/orders # 用户123的订单集合
GET /users/123/orders/20 # 用户123的20号订单集合
GET /users?status=active&page=1&size=10&sort=createTime,desc
HTTP 方法
| 方法 | 用途 | 幂等性 | 请求体 |
|---|---|---|---|
| GET | 获取资源 | 是 | 无 |
| POST | 创建资源 | 否 | 有 |
| PUT | 整体更新 | 是 | 有 |
| PATCH | 局部更新 | 视实现而定 | 有 |
| DELETE | 删除资源 | 是 | 通常无 |
说明:
- GET 用于查询资源,不应修改服务器状态
- POST 常用于创建资源,通常不是幂等的
- PUT 通常用于对资源进行整体替换,因此一般要求提交完整资源表示
- PATCH 用于部分更新,只提交需要修改的字段。它常常被设计为幂等,但并不天然保证幂等
- DELETE 用于删除资源,多次执行删除同一资源,对最终结果的影响通常是一致的,因此一般认为是幂等的
HTTP 状态码
响应的状态码不能都返回 200,应该有清晰的状态码表示响应的状态。
| 状态码 | 含义 | 说明 | 使用场景 |
|---|---|---|---|
| 200 | OK | 请求成功 | GET 请求成功返回数据 |
| 201 | Created | 资源创建成功 | POST 创建资源成功 |
| 204 | No Content | 删除成功 | DELETE 成功、更新成功但不返回数据 |
| 400 | Bad Request | 请求有误 | 请求参数错误 |
| 401 | Unauthorized | 未认证 | 未登录或 token 过期 |
| 403 | Forbidden | 无权限访问 | 已登录但无权限 |
| 404 | Not Found | 资源不存在 | 资源找不到 |
| 500 | Internal Server Error | 服务器内部错误 | 代码抛异常了 |
SpringMVC 实现 RESTful API
SpringMVC 提供了丰富的注解,方便实现 RESTful 接口。以用户相关接口为例:
CRUD
java
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
public List<User> list() {
return userService.list();
}
@GetMapping("/{id}")
public User get(@PathVariable Long id) {
return userService.getById(id);
}
@PostMapping
public User create(@RequestBody @Valid UserCreateRequest request) {
return userService.create(request);
}
@PutMapping("/{id}")
public User update(@PathVariable Long id,
@RequestBody @Valid UserUpdateRequest request) {
return userService.update(id, request);
}
@PatchMapping("/{id}")
public User patch(@PathVariable Long id,
@RequestBody Map<String, Object> fields) {
return userService.patch(id, fields);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
登录注册
登录注册通常不属于标准 CRUD,更适合单独设计认证接口:
java
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/register")
public Result register(@RequestBody @Valid RegisterRequest request) {
return authService.register(request);
}
@PostMapping("/login")
public Result login(@RequestBody @Valid LoginRequest request) {
return authService.login(request);
}
@PostMapping("/logout")
public Result logout() {
return authService.logout();
}
@PostMapping("/refresh")
public Result refresh(@RequestBody RefreshRequest request) {
return authService.refreshToken(request);
}
}
说明:
- 注册:创建用户资源
- 登录:生成 token 或 session
- 刷新 token:更新认证凭证
- 登出:使 token 失效
严格从 REST 角度看,这类接口不一定完全符合纯粹的资源建模方式,但在实际项目中,这种设计方式更常见,也更易于理解和实现。
统一返回格式
实际项目中会统一返回结构,方便前端处理:
java
public class Result<T> {
private Integer code;
private String message;
private T data;
}
示例:
java
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "Tom"
}
}
不过这里需要注意一点:
HTTP 状态码和业务状态码最好分层表达。
也就是说:
- HTTP 状态码用于表达请求在协议层面是否成功
Result.code用于表达业务层面的状态或错误码
例如:
- HTTP 返回
404,表示资源不存在 - 响应体中再返回业务码,如
USER_NOT_FOUND
这样更清晰,也更利于前后端联调和问题排查。
参数校验和异常处理
可以使用 Bean Validation 对请求参数进行校验:
java
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能小于 0")
private Integer age;
}
为了统一处理异常,通常还会使用全局异常处理器统一返回错误信息:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception e) {
return Result.fail(500, e.getMessage());
}
}
如果要进一步完善,项目中一般还会分别处理:
- 参数校验异常
- 业务异常
- 资源不存在异常
- 权限相关异常
这样返回给前端的信息会更准确
补充
URL 和 URI 的区别
URI(Uniform Resource Identifier)是统一资源标识符,用于标识一个资源。
URL(Uniform Resource Locator)是 URI 的一种,它不仅标识资源,还说明如何定位该资源,也就是告诉你应该通过什么协议、访问哪个主机、走哪条路径去找到它。例如:
text
https://example.com/users/1
除了 URL 之外,URI 还包括 URN(Uniform Resource Name):
- URL:告诉资源"在哪里、怎么访问"
- URN:只给资源一个唯一名称,不说明资源具体位置
接口的幂等性
幂等性指的是:
同一个请求执行一次和执行多次,对服务器资源状态产生的最终影响是一致的。
例如:
- 多次执行
GET /users/1,查询结果不会因为请求次数而改变资源状态 - 多次执行
DELETE /users/1,第一次可能删除成功,后续可能资源已经不存在,但最终资源都是"已删除"状态
需要注意的是,幂等性强调的是对资源状态的影响一致,并不意味着每次响应内容必须完全相同。
总结与思考
RESTful API 的核心思想可以概括为:
- 用 URL 表示资源
- 用 HTTP 方法表示操作
- 用状态码表达请求结果
- 保持接口设计统一、简洁、清晰
在 SpringMVC 中,可以通过 @RestController、@GetMapping、@PostMapping、@RequestBody、@PathVariable 等注解快速实现 RESTful API。
不过需要注意的是,REST 不是一套死板的规范。在实际项目中,接口设计还是要结合业务场景、团队规范以及前后端协作方式灵活处理。
优势
- 简单直观,易于理解
- 前后端职责清晰,耦合度较低
- 基于无状态设计,便于横向扩展
- 接口风格统一,维护成本较低
局限
- 对复杂业务动作的建模有时不够直接
- 对复杂查询场景表达能力有限
常见问题
1、PUT 和 PATCH 有什么区别?实际项目中怎么用的?
回答:PUT 通常用于对资源进行整体更新或替换,PATCH 用于部分更新,只需要传递变更的字段。
在实际项目中,我们使用 PATCH 更多一些,因为大多数场景只是修改某几个字段,没必要每次都传整个对象。不过如果业务上要求前端提交完整资源表示,那么使用 PUT 会更合适。
2、RESTfuI 接口怎么做版本控制?
回答常见的版本控制方式有三种:
-
URL 路径版本号 :如
/api/v1/users、/api/v2/users优点是简单直观、便于调试,也是最常见的方式。 -
Header 版本控制 :如
Accept: application/vnd.myapi.v1+json优点是 URL 更干净,但不够直观,调试也相对麻烦。 -
查询参数版本控制 :如
/users?version=1一般不太推荐,因为查询参数更适合用于过滤、分页、排序等语义。
如果是实际项目落地,大多数团队会优先选择第一种,也就是在 URL 中携带版本号。
3、如果一个操作不是简单的增删改查,比如"审批通过",RESTfuI 怎么设计?
回答:
通常有两种设计思路:
方式一:把操作建模成资源
例如:
http
POST /orders/1/approvals
表示为订单 1 创建一条审批记录。
这种方式更符合 REST 的资源导向思想,适用于审批本身就是一个独立业务对象的场景。
方式二:把操作视为状态变更
例如:
http
PATCH /orders/1
请求体:
json
{
"status": "approved"
}
这种方式更简单直接,适合"审批通过"本质上只是修改订单状态的场景。
实际项目中要看业务复杂度。如果审批过程本身有记录、流转、意见等信息,通常更适合建模成独立资源;如果只是简单改状态,用 PATCH 会更高效。
4、RESTful 接口返回的错误信息应该怎么设计?
首先,HTTP 状态码要使用正确:
400表示客户端请求有误401表示未认证403表示无权限404表示资源不存在500表示服务端内部错误
其次,响应体中建议包含:
- 业务错误码:便于前端识别错误类型
- 错误信息:便于展示或排查问题
- 详细描述:可选,便于开发调试
- traceId:可选,便于定位日志
例如:
json
{
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"description": "userId=123",
"traceId": "a1b2c3d4"
}