概述
2000 年,Roy Thomas Fielding 博士在他那篇著名的博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出了几种软件应用的架构风格,REST 作为其中的一种架构风格在这篇论文的第5章中进行了概括性的介绍。
REST 是“REpresentational State Transfer”的缩写,可以翻译成“表现状态转换”,但是在绝大多数场合中我们只说 REST 或者 RESTful。Fielding 在论文中将 REST 定位为“分布式超媒体应用(Distributed Hypermedia System)”的架构风格,它在文中提到一个名为“HATEOAS(Hypermedia as the engine of application state)”的概念。
我们利用一个面向最终用户的 Web 应用来对这个概念进行简单阐述:这里所谓的应用状态(Application State)表示 Web 应用的客户端的状态,简单起见可以理解为会话状态。资源在浏览器中以超媒体的形式呈现,通过点击超媒体中的链接可以获取其它相关的资源或者对当前资源进行相应的处理,获取的资源或者针对资源处理的响应同样以超媒体的形式再次呈现在浏览器上。由此可见,超媒体成为了驱动客户端会话状态的转换的引擎。
借助于超媒体这种特殊的资源呈现方式,应用状态的转换体现为浏览器中呈现资源的转换。如果将超媒体进一步抽象成一般意义上的资源呈现(Representation )方式,那么应用状态变成了可被呈现的状态(REpresentational State)。应用状态之间的转换就成了可被呈现的状态装换(REpresentational State Transfer),这就是 REST。
REST 是一种很笼统的概念,它代表一种架构风格。
怎么理解 RESTful
版本号
在 RESTful API 中,API 接口应该尽量兼容之前的版本。但是,在实际业务开发场景中,可能随着业务需求的不断迭代,现有的 API 接口无法支持旧版本的适配,此时如果强制升级服务端的 API 接口将导致客户端旧有功能出现故障。实际上,Web 端是部署在服务器,因此它可以很容易为了适配服务端的新的 API 接口进行版本升级,然而像 Android 端、IOS 端、PC 端等其他客户端是运行在用户的机器上,因此当前产品很难做到适配新的服务端的 API 接口,从而出现功能故障,这种情况下,用户必须升级产品到最新的版本才能正常使用。
为了解决这个版本不兼容问题,在设计 RESTful API 的一种实用的做法是使用版本号。一般情况下,我们会在 url 中保留版本号,并同时兼容多个版本。
【GET】 /v1/users/{user_id} // 版本 v1 的查询用户列表的 API 接口【GET】 /v2/users/{user_id} // 版本 v2 的查询用户列表的 API 接口
现在,我们可以不改变版本 v1 的查询用户列表的 API 接口的情况下,新增版本 v2 的查询用户列表的 API 接口以满足新的业务需求,此时,客户端的产品的新功能将请求新的服务端的 API 接口地址。虽然服务端会同时兼容多个版本,但是同时维护太多版本对于服务端而言是个不小的负担,因为服务端要维护多套代码。这种情况下,常见的做法不是维护所有的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,当绝大多数用户升级到较新的版本后,废弃一些使用量较少的服务端的老版本API 接口版本,并要求使用产品的非常旧的版本的用户强制升级。
注意的是,“不改变版本 v1 的查询用户列表的 API 接口”主要指的是对于客户端的调用者而言它看起来是没有改变。而实际上,如果业务变化太大,服务端的开发人员需要对旧版本的 API 接口使用适配器模式将请求适配到新的API 接口上。
资源路径
RESTful API 的设计以资源为核心,每一个 URI 代表一种资源。因此,URI 不能包含动词,只能是名词。注意的是,形容词也是可以使用的,但是尽量少用。一般来说,不论资源是单个还是多个,API 的名词要以复数进行命名。此外,命名名词的时候,要使用小写、数字及下划线来区分多个单词。这样的设计是为了与 json 对象及属性的命名方案保持一致。例如,一个查询系统标签的接口可以进行如下设计。
【GET】 /v1/tags/{tag_id}
同时,资源的路径应该从根到子依次如下
/{resources}/{resource_id}/{sub_resources}/{sub_resource_id}/{sub_resource_property}
我们来看一个“添加用户的角色”的设计,其中“用户”是主资源,“角色”是子资源。
【POST】 /v1/users/{user_id}/roles/{role_id} // 添加用户的角色
有的时候,当一个资源变化难以使用标准的 RESTful API 来命名,可以考虑使用一些特殊的 actions 命名。
/{resources}/{resource_id}/actions/{action}
举个例子,“密码修改”这个接口的命名很难完全使用名词来构建路径,此时可以引入 action 命名。
【PUT】 /v1/users/{user_id}/password/actions/modify // 密码修改
RESTFul 的本质
RESTful 的本质是基于 HTTP 协议对资源的增删改查操作做出定义。
理解 HTTP 协议非常简单,HTTP 是通过网络 Socket 发送一段字符串,这个字符串由键值对组成的 Header 部分和纯文本的 Body 部分组成。Url、Cookie、Method 都在 Header 中。
典型的 RESTful API 场景:
| 功能 | URL | HTTP Method | 幂等性 |
|---|---|---|---|
| 获取一组数据列表 | /path/records | GET | 幂等 |
| 根据 ID 获取某笔数据 | /path/records/{recordID} | GET | 幂等 |
| 新建数据 | /path/records | POST | 非幂等 |
| 完整更新数据 | /path/records/{recordID} | PUT | 幂等 |
| 部分更新数据 | /path/records/{recordID} | PATCH | 非幂等 |
| 删除数据 | /path/records/{recordID} | DELETE | 幂等 |
| 跨域访问预请求 | /path/records/{recordID} | OPTION |
虽然 HTTP 协议定义了其他的 Method,但是就普通场景来说,用好上面的几项已经足够了
RESTful 的注意事项
- URL 只是表达被操作的资源位置,因此不应该使用动词,且注意单复数区分
- 除了 POST 和 DELETE 之外,其他的操作需要幂等的,例如对数据多次更新应该返回同样的内容
- 设计风格没有对错之分,RESTful 一种设计风格,与此对应的还有 RPC 甚至自定义的风格
- RESTful 和语言、传输格式无关
- 无状态,HTTP 设计本来就是没有状态的,之所以看起来有状态因为我们浏览器使用了 Cookies,每次请求都会把 Session ID(可以看做身份标识)传递到 Headers 中。
- RESTful 没有定义 Body 中内容传输的格式,有另外的规范来描述怎么设计 Body 的数据结构。
JSON API
因为 RESTful 风格仅仅规定了 URL 和 HTTP Method 的使用,并没有定义 Body 中数据格式的。我们怎么定义请求或者返回对象的结构,以及该如何针对不同的情况返回不同的 HTTP 状态码?
具体可参考 JSON API 官网英文版 或 JSON API 中文版MIME 类型
JSON API 数据格式已经被 IANA 机构接受了注册,因此必须使用application/vnd.api+json类型。客户端请求头中 Content-Type 应该为application/vnd.api+json,并且在 Accept 中也必须包含application/vnd.api+json。如果指定错误服务器应该返回415或406状态码。JSON 文档结构
在顶级节点使用data、errors、meta,来描述数据、错误信息、元信息,注意data和errors应该互斥,不能再一个文档中同时存在,meta在项目实际上用的很少,只有特别情况才需要用到,比如返回服务器的一些信息。{"links": {"self": "http://example.com/articles","next": "http://example.com/articles?page[offset]=2","last": "http://example.com/articles?page[offset]=10"},"data": [{"type": "articles","id": "1","attributes": {"title": "JSON API paints my bikeshed!"},"relationships": {},"links": {"self": "http://example.com/articles/1"}}],"included": [],"meta": {"version": "1.0.0","copyright": "Copyright 2015 Example Corp."}}
data 属性
一个典型的 data 的对象格式,我们的有效信息一般都放在 attributes 中。
{"type": "articles","id": "1","attributes": {"title": "JSON API paints my bikeshed!"},"relationships": {},"links": {"self": "http://example.com/articles/1"}}
- id: 显而易见为唯一标识,可以为数字也可以为hash字符串,取决于后端实现
- type: 描述数据的类型,可以对应为数据模型的类名
- attributes: 代表资源的具体数据
- relationships、links: 为可选属性,用来放置关联数据和资源地址等数据
errors 属性
这里的 errors 和 data 有一点不同,一般来说返回值中 errors 作为列表存在,因为针对每个资源可能出现多个错误信息。最典型的例子为,我们请求的对象中某些字段不符合验证要求,这里需要返回验证信息,但是 HTTP 状态码会使用一个通用的 401,然后把具体的验证信息在 errors 给出来。{"errors": [{"code": 10011,"title": "Name can't be null"},{"code": 10011,"title": "Content can't be null","detail": ""}]}
在 title 字段中给出错误信息,如果我们在本地或者开发环境想打出更多的调试堆栈信息,我们可以增加一个 detail 字段让调试更加方便。需要注意的一点是,我们应该在生产环境屏蔽部分敏感信息,detail 字段最好在生产环境不可见。
常用返回码
200 OK
200 是一个最常用的状态码用来表示请求成功,例如 GET 请求到某一个资源,或者更新、删除某资源。需要注意的是使用 POST 创建资源应该返回 201 表示数据被创建。
201 Created
如果客户端发起一个 POST 请求,在 RESTful 部分我们提到,POST 为创建资源,如果服务器处理成功应该返回一个创建成功的标志,在 HTTP 协议中,201 为新建成功的状态。文档规定,服务器必须在 data 中返回 id 和 type。
HTTP/1.1 201 CreatedLocation: http://example.com/photos/550e8400-e29b-41d4-a716-446655440000Content-Type: application/vnd.api+json{"data": {"type": "photos","id": "550e8400-e29b-41d4-a716-446655440000","attributes": {"title": "Ember Hamster","src": "http://example.com/images/productivity.png"},"links": {"self": "http://example.com/photos/550e8400-e29b-41d4-a716-446655440000"}}}
在 HTTP 协议中,2xx 的状态码都表示成功,还有 202、204 等用的较少,就不做过多介绍了,4xx 返回客户端错误,会重点介绍。
401 Unauthorized
如果服务器在检查用户输入的时候,需要传入的参数不能满足条件,服务器可以给出 401 错误,标记客户端错误,需要客户端自查。
415 Unsupported Media Type
当服务器媒体类型 Content-Type 和 Accept 指定错误的时候,应该返回 415。
403 Forbidden
当客户端访问未授权的资源时,服务器应该返回 403 要求用户授权信息。
404 Not Found
500 Internal Server Error
当服务器发生任何内部错误时,应当返回 500,并给出 errors 字段,必要的时候需要返回错误的 code,便于查错。一般来说,500 错误是为了区分 4xx 错误,包括任何服务器内部技术或者业务异常都应该返回 500。
HATEOAS
HATEOAS(Hypermedia As The Engine Of Application State,超媒体应用程序状态引擎)是一种思想,怎么在项目中使用是需要灵活运用的,在实际项目中要使用 HATEOAS 也要付出额外的工作量(包括开发和前后端联调);
在 RESTful 和 JSON API 部分我们都贯穿了 HATEOAS 思想。比如在某个系统中产品和订单是一对多的关系,那我们给产品的返回值可以定义为:
{"data": {"type": "products","id": "550e8400-e29b-41d4-a716-446655440000","attributes": {"title": "Example for relationships and links"},"relationships": {"orders": {"links": {"self": "/path/products/550e8400-e29b-41d4-a716-446655440000/orders"}}},"links": {"self": "/path/products/550e8400-e29b-41d4-a716-446655440000"}}}
实际上 HATEOAS 算作是 JSON API 定义了的一部分,HATEOAS 思想是 RESTful 是利用 HTTP 协议来进行增删改查,那我们怎么在没有文档的情况下找到这些资源的地址呢,一种可行的办法就是在 API 的返回体里面加入导航信息,也就是 links。这样就像 HTML 中的 A 标签实现了超文本文档一样,实现了 超媒体应用程序状态追踪。
从返回中我们能得到 links 中 product 的的资源地址,同时也能得到 orders 的地址,这样我们不需要客户端自己拼装地址,就能够得到请求 orders 的地址。如果我们严格按照 HATEOAS 开发,客户端只需要在配置文件中定义一个入口地址就能够完成所有操作,在资源地址发生变化的时候也能自动适配。
