Web系统设计 --- HTTP + RESTful
RESTful(Representational State Transfer)是一种基于HTTP协议设计网络应用程序接口(API)的架构风格,由Roy Fielding在2000年的博士论文中提出。其核心思想是以资源为中心,通过统一的接口对资源进行操作,实现客户端与服务器的解耦也就是RESTful是基于http定义了一组接口格式规范,用来规范所有http请求。
·
Web系统设计 --- HTTP + RESTful
什么是RESTful
- RESTful(Representational State Transfer)是一种基于HTTP协议设计网络应用程序接口(API)的架构风格,由Roy Fielding在2000年的博士论文中提出。其核心思想是以资源为中心,通过统一的接口对资源进行操作,实现客户端与服务器的解耦
- 也就是RESTful是基于http定义了一组接口格式规范,用来规范所有http请求
RESTful 设计原则
核心原则
- 资源导向(Resource-Based)
- 所有事物抽象为资源(如用户、订单、文章),每个资源有唯一的标识符(URI,如 /users/123)。
- 资源通过JSON与客户端交互。
- 统一接口(Uniform Interface)
- 定义了HTTP方法明确操作类型:
GET:获取资源
POST:创建资源
PUT:更新资源(全量替换)
PATCH:更新资源(部分修改)
DELETE:删除资源- HTTP状态码标识结果
如200 OK、404 Not Found等。
- 无状态(Stateless)
- 每个请求必须包含所有必要信息,服务器不保存客户端状态(如Session),提升扩展性和可靠性。
- 可缓存(Cacheable)
- 响应需明确是否可缓存(如通过Cache-Control头),减少重复请求
- 客户端无需关注服务器架构细节(如负载均衡、代理层)
- URI命名
- 使用名词复数表示资源(如/users而非/getUser)。
- 避免动词,用HTTP方法表达操作(如DELETE /users/123)。
- 层级关系用/分隔(如/users/123/orders)。
- URI中体现版本:/api/v1/users
RESTful 接口类型
GET
- 用途:获取资源(查询数据)。
- 特性:
安全:不会修改服务器资源。
幂等:多次请求结果相同。
可缓存:响应可被浏览器或代理缓存。- 响应状态码:
200 OK(成功)
404 Not Found(资源不存在)
304 Not Modified(缓存未更新)
GET /users/123 # 获取ID为123的用户信息
GET /users?role=admin # 过滤角色为admin的用户
POST
- 用途:创建新资源(或提交非幂等操作)。
- 特性:
非安全:会修改服务器资源。
非幂等:多次调用可能产生不同结果(如重复提交订单)。- 响应状态码:
201 Created(成功创建,响应头包含 Location: /users/456)
400 Bad Request(请求数据不合法)
409 Conflict(资源冲突,如重复创建)
POST /users
Body: { "name": "Alice", "age": 30 } # 创建新用户
PUT
- 用途:全量更新资源(替换整个资源)。
- 特性:
幂等:多次调用结果一致(如重复更新同一数据,最终状态相同)。
若资源不存在,可创建(需由服务端决定是否支持)。- 响应状态码:
200 OK 或 204 No Content(更新成功)
201 Created(资源被创建)
404 Not Found(资源不存在且服务端不支持创建)
PUT /users/123
Body: { "name": "Bob", "age": 25 } # 替换用户123的所有字段
DELETE
- 用途:删除资源。
- 特性:
幂等:删除后再次调用无效果。- 响应状态码:
204 No Content(删除成功,无返回内容)
404 Not Found(资源不存在)
403 Forbidden(无权删除)
DELETE /users/123 # 删除ID为123的用户
PATCH
- 用途:部分更新资源(仅修改指定字段)。
- 特性:
非幂等:连续相同请求可能导致不同结果(如递增计数器)。
需明确传递修改的字段。- 响应状态码:
200 OK 或 204 No Content
400 Bad Request(修改格式错误)
404 Not Found
- 目前Patch一般有两个标准,分别是
- JSON Patch
- JSON Merge Patch
JSON Patch (RFC 6902)
- 请求体:一个由操作对象组成的 JSON 数组。
- 操作对象:每个对象必须包含一个 op 字段指定操作类型,一个 path 字段指定目标位置,以及可能需要的 value
- 常用操作:
- add: 添加新值或替换现有值。
- remove: 移除一个值。
- replace: 替换一个值(相当于 remove + add)。
- move: 从一个位置移动值到另一个位置。
- copy: 从一个位置复制值到另一个位置。
- test: 测试目标位置的值是否等于给定值(用于乐观锁验证)。
PATCH /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "/email", "value": "new.email@example.com" },
{ "op": "add", "path": "/address/city", "value": "Berlin" },
{ "op": "remove", "path": "/nickname" },
{ "op": "test", "path": "/version", "value": 7 } // 确保版本号是7,否则操作失败
]
- 优点:
- 功能强大:支持添加、删除、替换、移动、复制和测试。
- 操作原子性:所有操作在一个请求中顺序执行,如果任何操作失败(例如 test 失败),整个 Patch 都不会被应用。
- 精确控制:
可以操作数组中的特定元素(例如 { "op": "replace", "path": "/tags/3", "value": "Berlin" }, 表示更改第四个标签也就是第四个个元素)。
- 缺点:
- 学习成本:语法相对复杂,需要客户端构造操作数组。
- 可读性稍差:不如 Merge Patch 直观。
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPatch("{id}")]
public IActionResult Patch(int id, [FromBody] JsonPatchDocument<Product> patchDoc)
{
if (patchDoc == null)
{
return BadRequest("Patch document is null.");
}
// 查找要更新的产品
var product = _products.FirstOrDefault(p => p.Id == id);
if (product == null)
{
return NotFound();
}
// 在应用 Patch 之前记录原始状态,以便错误处理或审计
var originalProduct = JsonConvert.SerializeObject(product);
// 应用 Patch 操作到找到的产品实体
patchDoc.ApplyTo(product, ModelState);
// 检查 ModelState 是否有效
// ApplyTo 方法会自动验证 Patch 操作本身(如路径是否存在)
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// (可选)进行模型层面的验证(如数据注解)
if (!TryValidateModel(product))
{
return BadRequest(ModelState);
}
// 在实际应用中,这里应保存到数据库
// _context.SaveChanges();
// 返回更新后的实体
return Ok(product);
}
}
Json Patch 乐观锁
- 乐观锁是一种并发控制策略,它假设多个事务在大多数情况下不会互相干扰。因此,它不会在整个事务过程中锁定资源,而是在提交更新时检查数据是否被其他事务修改过。如果没有,则提交成功;如果已被修改,则提交失败并通常要求客户端重试。test 操作完美地实现了这一“检查”步骤。
- 典型工作流程:
- 客户端A和B 获取资源 /users/123,
- 客户端A注意到 version 字段的值为 5。
- 客户端B 也获取了同一个资源,同样看到 version: 5。
json
GET /users/123
{"id": 123, "name": "Alice", "email": "alice@example.com", "version": 5}
- 客户端A 想要修改邮箱。它构造一个 JSON Patch 请求,在修改之前先测试 version 是否仍然是 5。
http
PATCH /users/123 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "test", "path": "/version", "value": 5 },
{ "op": "replace", "path": "/email", "value": "alice.new@example.com" },
{ "op": "replace", "path": "/version", "value": 6 }
]
- 假设在此期间没有其他修改:服务器接收到请求。
- 首先执行 test:检查 /version 的值是否为 5。是,测试通过。
- 然后执行 replace:将 email 改为新地址。
- 最后执行 replace:将 version 递增为 6。
- 返回成功 (200 OK)。
- 现在客户端B想修改名字。它也用旧的版本号 5 构造了一个 Patch 请求。
http
PATCH /users/123 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "test", "path": "/version", "value": 5 },
{ "op": "replace", "path": "/name", "value": "Bob" },
{ "op": "replace", "path": "/version", "value": 6 }
]
- 并发冲突发生! 服务器接收到客户端B的请求:
- 首先执行 test:检查 /version 的值是否为 5。但现在的值已经是 6(由客户端A更新)。测试失败!
- 服务器立即停止处理这个 Patch 数组,后续的 replace 操作都不会执行。
- 数据库中的资源没有任何改变。
- 服务器返回 412 Precondition Failed 错误给客户端B。
- 客户端B 收到 412 错误后,其工作流程应该是:
- 重新 GET /users/123 获取最新的资源状态(包括 name: “Alice” 和 version: 6)。
- 基于新数据重新进行它想要的修改(例如合并名字的更改)。
- 重新构造并发送一个新的 PATCH 请求,这次测试 version 是否为 6。
json
[
{ "op": "test", "path": "/version", "value": 6 },
{ "op": "replace", "path": "/name", "value": "Bob" },
{ "op": "replace", "path": "/version", "value": 7 }
]
JSON Merge Patch (RFC 7396)
- 请求体:一个包含需要更新的字段及其新值的 JSON 对象。
- 规则:
- 提供的字段会被更新或添加。
- 如果请求体中某个字段被显式设置为 null,则该字段会被删除。
- 未在请求体中出现的字段保持不变
PATCH /api/users/123 HTTP/1.1
Host: example.com
Content-Type: application/merge-patch+json
{
"email": "new.email@example.com",
"address": {
"city": "Berlin"
},
"nickname": null // 删除 nickname 字段
}
- 优点:
- 简单直观:客户端只需发送想要修改的字段,语法简单。
- 易于理解:请求体看起来就像是资源的一个片段。
- 缺点:
- 功能有限:无法操作数组中的特定元素。要更新数组,通常必须发送整个新数组。
- 无法区分 null 和删除:设置 null 即表示删除,如果你的业务逻辑中 null 是一个有效值,这会带来问题。
- 没有原子性保证:不像 JSON Patch 有 test 操作来支持并发控制
//Merge Patch 没有标准实现,
//需要手动处理或使用第三方库。这里展示手动合并的方式。
[HttpPatch("merge-patch/{id}")]
public async Task<IActionResult> PatchUserMergePatch(int id, [FromBody] User partialUpdate)
{
// 1. 获取现有用户
var existingUser = await _userRepository.GetByIdAsync(id);
if (existingUser == null)
{
return NotFound();
}
// 2. 手动合并逻辑
// 更新提供的字段,忽略 null 字段(或者根据业务逻辑,将 null 视为删除)
if (partialUpdate.Email != null)
{
existingUser.Email = partialUpdate.Email;
}
// 注意:对于值类型(int, bool等),无法区分“未提供”和“提供了默认值”
// ... 处理其他字段
// 3. 验证和保存
if (!TryValidateModel(existingUser))
{
return BadRequest(ModelState);
}
await _userRepository.UpdateAsync(existingUser);
return Ok(existingUser);
}
RESTful 状态码
1xx(信息响应)
100Continue
客户端应继续发送请求的剩余部分(用于大文件上传前的确认)
2xx(成功)
200 OK: 请求成功,
返回响应数据(如 GET 或 PUT 请求成功)。201 Created
资源创建成功(如 POST 请求后返回新资源的URI,响应头包含 Location: /users/123)。202 Accepted
请求已接受但未处理完成(常用于异步任务)。204 No Content
请求成功,但无返回内容(如 DELETE 成功或 PUT 更新后无需返回数据)。
3xx(重定向)
301 Moved Permanently
资源永久迁移到新URI(需更新书签)。302 Found
资源临时重定向到新URI(浏览器默认行为是后续请求仍用原URI)。304 Not Modified
资源未修改,客户端可使用缓存(配合 If-Modified-Since 头使用)。
4xx(客户端错误)
400 Bad Request
请求格式错误(如参数缺失或JSON语法错误)。401 Unauthorized
未认证(需提供有效身份凭证,如JWT失效)。403 Forbidden
已认证但无权限访问资源(如普通用户访问管理员接口)。404 Not Found
资源不存在(URI无效或资源已删除)。405 Method Not Allowed
HTTP方法不支持(如对只允许 GET 的URI发送 POST)。409 Conflict
资源状态冲突(如重复创建同名用户)。429 Too Many Requests
请求频率超出限制(常见于API限流)。
5xx(服务器错误)
500 Internal Server Error
服务器内部错误(通用兜底错误,需排查日志)。502 Bad Gateway
网关或代理服务器收到无效响应(如后端服务崩溃)。503 Service Unavailable
服务暂时不可用(如维护或过载,可配合 Retry-After 头提示重试时间)。504 Gateway Timeout
网关或代理服务器超时(后端服务未及时响应)
RESTful Uri设计原则
对于Api的uri命名要遵守以下设计
- 使用名词复数表示资源(如/users而非/getUser)。
- 避免动词,用HTTP方法表达操作
- 如DELETE /users/123,而不是DELETE /delete-users/123)。
- 层级关系用/分隔
- 如/users/123/orders, 而不是 /users/orders/123
- URI中体现版本:/api/v1/users
- 推荐使用小驼峰,比如/pet/petId
- uri应该大小写敏感,并且推荐使用全小写
- 以上只是建议,具体根据实际情况而定
Example: https://petstore.swagger.io/
- 以下Api来住swagger官方例子,可以看到其实并没有严格遵守规范,比如Api里没有版本信息,命名里有动词等等
Api传参:QueryString 和 UriPath
使用 URI Path(路径参数)的场景
- URI Path 用于标识 资源的主键 或 资源间的层级关系,
通常对应服务端的 核心数据模型。
适用场景:
- 唯一资源标识
通过路径参数直接定位唯一资源(如用户ID、订单ID)。
GET /users/{id} # 获取指定ID的用户
DELETE /orders/{orderId} # 删除指定ID的订单
- 资源层级关系
表示资源之间的从属关系(如用户的所有订单)。
示例:
GET /users/{userId}/orders # 获取用户的订单列表
特点:
- 必要性:路径参数通常是 必填 的(缺少会导致404)。
- 语义明确:路径体现资源的唯一性和结构(如 /users/123 明确指向用户123)。
- 缓存友好:路径参数不同的 URL 会被视为不同资源,适合缓存(如 CDN 缓存)
使用 Query String(查询参数)的场景
- Query String 用于 过滤、排序、分页 或 附加操作,
通常是 可选 的非核心参数。
适用场景:
- 过滤数据
筛选符合特定条件的资源列表。
示例:
GET /users?role=admin&status=active # 获取角色为admin且状态为活跃的用户
- 分页与排序
控制返回数据的范围和顺序。
示例:
GET /articles?page=2&limit=20&sort=-created_at # 获取第二页文章,按创建时间倒序
- 附加操作
触发特定功能(如统计、导出格式)。
示例:
GET /reports/sales?format=csv # 导出CSV格式的销售报表
GET /products?fields=name,price # 仅返回名称和价格字段(字段选择)
- 非资源标识参数
不影响资源定位,仅修饰响应结果。
示例:
GET /search?q=restful&lang=en # 搜索关键词"restful",语言为英文
特点:
- 可选性:
缺少查询参数时,通常返回默认结果(如不分页)。- 灵活性:参数组合自由,适合动态需求(如多种过滤条件)。
- 幂等性:同一 Query String 的 GET 请求是幂等的(结果可缓存)
RESTful和HTTP的区别
- RESTful并不等于HTTP,它只是基于HTTP的一些特性设定了一组设计规范
- 类似的还有GraphQL,也是基于HTTP的特性设定了一组规范
注意事项
- 以上只是标准的设计规范,并没有技术上的限制,开发者完全可以POST当PUT用,PUT当POST用,甚至POST当GET用等等,只是这样不规范,会导致程序可维护性降低
- 现实开发中需要根据实际情况调整,最好和以前的开发风格保持一致
- 大部分Api的设计都没有严格的遵守以上规则,比如Api的uri里没有版本信息,返回的状态码用的不准确等,使用动词等等
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐




所有评论(0)