REST(Representational State Transfer)并非一种协议,而是一组基于 HTTP 协议的架构约束与设计原则。遵循这些原则构建的 API,能够天然具备无状态、可缓存、分层系统等优势。然而,真正生产环境中可靠、易用、易维护的 RESTful API,还需要在规范细节和工程实践上下一番功夫。本节将围绕 URL 设计、HTTP 方法语义、状态码、版本管理、错误处理、分页过滤、安全等维度,给出可直接落地的设计规范与 Spring 生态中的最佳实践。
12.7.1 资源导向的 URL 设计
RESTful API 的核心是资源,每个 URL 应该代表一个资源或资源集合,而非动作。
1. 使用名词,避免动词
# 推荐
GET /users # 获取用户列表
GET /users/{id} # 获取指定用户
POST /users # 创建用户
PUT /users/{id} # 完整更新用户
PATCH /users/{id} # 部分更新用户
DELETE /users/{id} # 删除用户
# 不推荐
GET /getUserList
POST /createUser
POST /deleteUser
2. 资源嵌套层次不宜过深
表示关联关系时,采用嵌套资源,但深度一般不超过两层。
# 推荐
GET /users/{userId}/orders # 获取某用户所有订单
GET /users/{userId}/orders/{orderId} # 获取某用户指定订单
# 可替代方案:若嵌套过深,可使用顶级资源加查询参数
GET /orders?userId=123&status=paid
3. 集合用复数,文档用单数标识
统一使用复数名词表示集合,用路径参数中的 ID 标识具体资源。
GET /products # 产品集合
GET /products/{id} # 单个产品
4. URL 中不使用文件扩展名
API 的表述格式应由 Accept 和 Content-Type 头协商,而非 URL 中的 .json、.xml。
# 推荐
GET /users/123
Accept: application/json
# 不推荐
GET /users/123.json
12.7.2 正确使用 HTTP 方法
HTTP 方法的语义必须与操作匹配,这是 RESTful API 最具契约性的部分。
| 方法 | 语义 | 幂等性 | 安全性 | 用途 |
|------|------|--------|--------|------|
| GET | 获取资源表述 | 是 | 是 | 查询操作 |
| POST | 创建资源或触发操作 | 否 | 否 | 新增、复杂查询 |
| PUT | 完整替换资源 | 是 | 否 | 全量更新 |
| PATCH | 部分更新资源 | 否 | 否 | 增量更新 |
| DELETE | 删除资源 | 是 | 否 | 删除 |
使用示例:
POST /users # 创建用户,返回 201 Created 及 Location 头
PUT /users/{id} # 全量替换用户资源,需传递完整对象
PATCH /users/{id} # 仅更新部分字段,如 {"email":"new@example.com"}
DELETE /users/{id} # 删除用户,返回 204 No Content
不以动词冒充资源: 若确实有非 CRUD 的“动作”,应将其作为资源的子资源或复合名词处理。
# 推荐:将“激活”视为用户的一个子资源
POST /users/{id}/activation
# 或者使用命令式资源
POST /password-resets
12.7.3 善用 HTTP 状态码
状态码是 RESTful API 表达结果的直接语言,而不应把业务错误全部塞进响应体中。
常用状态码分类:
- 2xx 成功
200 OK:请求成功(GET、PUT、PATCH 常用)201 Created:资源创建成功(配合 Location 头返回新资源 URI)204 No Content:成功但无响应体(DELETE 成功、更新成功但无需返回内容)
- 3xx 重定向
301 Moved Permanently:资源 URI 永久变更304 Not Modified:资源未改动,可使用缓存
- 4xx 客户端错误
400 Bad Request:请求参数错误、格式不对401 Unauthorized:未认证或认证失效403 Forbidden:认证通过但无权限404 Not Found:资源不存在或无权访问409 Conflict:资源冲突(如重复提交、版本冲突)422 Unprocessable Entity:语义错误(常用于参数校验失败)
- 5xx 服务端错误
500 Internal Server Error:未预期异常503 Service Unavailable:服务临时不可用
实践要求:
- 不将业务错误码混入 HTTP 状态码,例如用
200包裹错误信息(如{"code":500,"message":"error"})。应让 HTTP 状态码直接表达结果。 - 在响应体中可提供更详细的业务错误码和描述,便于客户端精细化处理。
// 400 Bad Request 时的响应体示例
{
"error": "validation_failed",
"message": "请求参数校验不通过",
"details": [
{ "field": "email", "message": "邮箱格式不正确" }
]
}
12.7.4 API 版本控制
随着业务演进,API 难免需要发布不兼容的变更。选择合适的版本控制策略可以避免客户端被意外破坏。
常见方案:
- URI 路径版本(最常用)
GET /v1/users
GET /v2/users
清晰直观,便于路由和文档化管理。
- 请求头版本
GET /users
Accept: application/vnd.myapp.v2+json
URI 保持干净,但对客户端调试略不友好。
- 查询参数版本
GET /users?version=2
简便但不推荐作为长期策略。
推荐实践:
- 新项目优先采用 URI 路径版本,简单且易于网关层控制。
- 同一大版本内应保持向后兼容,新字段采用“只增不减”原则。
- 废弃的 API 需要提供 Sunset 头 或在响应中添加
Deprecation头,明确告知客户端迁移时间线。
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
12.7.5 标准化错误响应
一个友好且统一的错误格式能显著降低客户端集成的成本。
推荐结构:
{
"error": "invalid_token",
"error_description": "访问令牌已过期",
"error_uri": "https://api.example.com/docs/errors#invalid_token",
"timestamp": "2025-01-15T10:23:45Z",
"trace_id": "abc123"
}
Spring 中的实践:
通过 @ControllerAdvice 结合 ResponseEntityExceptionHandler 统一处理异常,将各类业务异常映射为统一的响应结构。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
ErrorResponse error = new ErrorResponse(
"validation_failed",
"请求参数校验失败",
fieldErrors.stream()
.map(e -> new FieldErrorDetail(e.getField(), e.getDefaultMessage()))
.collect(Collectors.toList())
);
return ResponseEntity.badRequest().body(error);
}
}
12.7.6 分页、过滤、排序与字段选择
当资源集合的数据量较大时,API 必须提供灵活的数据检索能力。
1. 分页
采用基于偏移量或游标的方式。
- 偏移量分页:
GET /users?page=0&size=20
响应中返回分页元数据:
{
"data": [...],
"page": {
"size": 20,
"number": 0,
"totalElements": 200,
"totalPages": 10
}
}
- 游标分页(推荐用于实时数据流):
GET /users?cursor=eyJsYXN0SWQiOjEyM30&limit=20
避免因数据插入导致的重复或遗漏。
2. 过滤
GET /users?status=active&role=admin
复杂过滤条件可使用标准化的查询语法,如 filter 参数,但需注意避免过度设计。
3. 排序
GET /users?sort=createdAt,desc&sort=lastName,asc
4. 字段选择
允许客户端指定返回的字段,减少带宽消耗。
GET /users?fields=id,email,profile.name
Spring 中可使用 Jackson 或自定义注解配合响应过滤实现。
12.7.7 HATEOAS:让 API 自描述
成熟的 RESTful API 应向客户端提供可操作的状态转移链接,即 HATEOAS(Hypermedia as the Engine of Application State)。这样客户端无需硬编码 URI,而是根据响应中的链接驱动后续操作。
Spring HATEOAS 实践:
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/{id}")
public EntityModel<Order> getOrder(@PathVariable Long id) {
Order order = orderService.findById(id);
return EntityModel.of(order,
linkTo(methodOn(OrderController.class).getOrder(id)).withSelfRel(),
linkTo(methodOn(OrderController.class).cancel(id)).withRel("cancel"),
linkTo(methodOn(PaymentController.class).pay(id)).withRel("payment")
);
}
}
响应示例:
{
"id": 1001,
"status": "pending",
"_links": {
"self": { "href": "/orders/1001" },
"cancel": { "href": "/orders/1001/cancel" },
"payment": { "href": "/orders/1001/payment" }
}
}
即使不完全实现 HATEOAS,至少应在响应中包含 self 链接,方便日志追踪和调试。
12.7.8 安全与幂等性
1. 强制 HTTPS
生产环境禁止使用明文 HTTP,所有 API 调用应通过 HTTPS 加密。
2. 身份认证与授权
- 公开 API 使用 OAuth2 + JWT 或 API Key。
- 内部服务间调用使用 mTLS 或服务网格。
- 凭证绝不放在 URL 中,应使用
Authorization头:Authorization: Bearer <token>
3. 幂等性保障
对于非幂等的 POST 请求,需提供幂等键(Idempotency-Key)机制,防止网络重试导致重复创建资源。
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
服务端以该键作为唯一标识缓存请求结果,重复请求直接返回已处理的结果。
4. 跨域(CORS)配置
若 API 面临浏览器前端调用,需配置 CORS 策略,明确允许的来源、方法和头信息。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET","POST","PUT","DELETE")
.allowCredentials(true);
}
}
12.7.9 文档化与开发体验
没有文档的 API 等于没有 API。利用 Spring REST Docs 或 OpenAPI(原 Swagger)自动生成准确、可读性强的文档。
- Spring REST Docs:基于测试生成,确保文档与代码实际行为一致。
- Springdoc-openapi:基于注解自动生成 OpenAPI 规范,可在运行时通过 Swagger UI 浏览。
最佳实践:
- 每个 API 必须提供请求/响应示例。
- 标注认证要求和可能的错误码。
- 提供变更日志和废弃时间表。
- 使用代码生成工具(如 OpenAPI Generator)从规范生成客户端 SDK,保持多端一致。
12.7.10 性能与缓存
充分利用 HTTP 缓存机制来减少服务端压力和响应延迟。
- ETag / If-None-Match:用于资源变更检查,返回
304 Not Modified。 - Last-Modified / If-Modified-Since:基于时间戳的缓存验证。
- Cache-Control:对于不常变化的资源(如字典数据),设置
max-age和public。
在 Spring 中可使用 WebContentInterceptor 或手动设置响应头实现缓存策略。
此外,合理使用 Gzip 压缩、分页限制、速率限制(Rate Limiting)等手段,保证 API 的稳定性和吞吐能力。
小结
设计优雅的 RESTful API 并非单纯套用规则,而是在一致性、可维护性和用户体验之间持续权衡。以资源为中心,利用 HTTP 完整语义,保持版本与错误处理的规范,并不断迭代文档,你的 API 就能从“能用”走向“好用”,成为团队内外部真正值得信赖的基础设施。