RESTful深入理解
背景
在使用retrofit时,许多博客提到该套件适合使用RESTful API的服务,
但对于RESTful仅仅有着粗浅的理解,
- 如何区分是否是RESTful架构
- 是否使用RESTful对于应用通信方式,具体来说是对retrofit有什么影响
历史
Roy Thomas Fielding (HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席)
在2000年博士论文中发表.
早年软件就是本地软件,
一直以来改变应用程序的互动风格对整体效果有着非常大的影响.
要求上可能有
- 性能好
- 功能强
当进入网络软件时代,
这种互动则需要满足进一步的需求.
- 适宜通信
含义
Representational State Transfer
表现层状态转化
在无状态的http协议下,
想让位于服务器的资源的状态发生变化,
不能对http协议动手,
而是应该对资源的表现层(资源具体呈现出来的形式)做适当修改
具体流程是
- 每个URI代表一个资源,就像rails中对resource的定义
- 客户端和服务器之前传递的是该资源的某种表现层
- 最终该资源的状态发生了变化
设计细节
URL
- 使用专用域名,或者在API比较简单时放在主域名下
- 常用两个动词和复数名词
- 有些客户端仅支持两个动词,为GET和POST(POST此时需要模拟PUT,PATCH,DELETE)
- 使用复数名词表示资源
- 避免多级别的URL,其他的放在参数中
- 有时会将版本号放入URL
- 如果将进一步的URL放入响应中,称为HATEOAS
传输格式
- 要求json格式的传输,不要用纯文本
状态码
- 不要在错误时返回200,返回的状态码要尽量精确
返回值结构设计
-
标准
HEADER里放状态码和简单的信息,
BODY里放最裸奔的数据.
优点是简单明了,小而美,接收端不用层层剥茧.直接获得并转换即可.
对于错误信息,也有一定程度的自定义可能性.1
2
3
4
5
6
7
8
9
10// 正确时
// HTTP/1.1 200 OK
[
{"id":1, "name":"Lora"},
{"id":2, "name":"Dave"},
]
// 错误时
// Status: 500 INTERNAL SERVER ERROR
{}需要注意有时一些人为了完整表达会把这些写成下面这样,
简直是丧心病狂.1
2
3
4
5
6
7
8{
"code": 200,
"message": "OK",
"data": [
{"id":1, "name":"Lora"},
{"id":2, "name":"Dave"},
]
}关于code和message:
- flask等简单的http服务器已经提供了自定义code和message的功能,
- 如果不写message,则是http的标准解释
- 自定义的code有长度限制,最大3位
-
关于错误的扩展
通常错误时想知道更进一步的信息.于是有些系统会这样做:
1
2
3
4
5
6
7
8// 正确时不变
// 错误时
// HTTP/1.1 500 INTERNAL SERVER ERROR
{
"errCode": 3000,
"errMsg": "unable to xxxx because xxx"
}在实现性上,只要请求的前端有解析的方法,无论是什么结构,都是可以的
rails中有时也会有仅仅使用message不使用code的情况
1
2
3
4
5
6
7
8
9
10// HTTP/1.1 500 INTERNAL SERVER ERROR
{
"price":[
"price must be integer",
"price must be large then 0"
]
"color":[
"unknown color code"
]
}前端使用的解析方法比如
1
2
3
4
5
6
7
8
9
10if (response.code() != 20x) {
// ...
} else {
foreach(<key,errors> in response.body()) {
foreach(error in errors) {
res[key][] = error;
}
}
return res;
}通常不想违背原始的状态码,即一定不会在200时返回有error的body.
这样做,系统会有一些偏离简洁,但想表达的信息已经可以表达了. -
较为统一的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 正确时
// HTTP/1.1 200 OK
{
"status": "OK",
"errCode": "",
"messages": []
"data": {
// ....
}
}
// 错误时
// HTTP/1.1 500 INTERNAL SERVER ERROR
{
"status": "ERROR",
"errCode": "3001",
"messages": ["unable to xxxxx because xxxx"],
"data": {}
}其实感觉status有点多余,本来能从HEADER里面得到是否成功的信息.
前端可能使用的解析方法是1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16if (response.code() == 20x) {
return parse(response.body.data);
} else {
log(response.body.errCode);
log(response.body.messages);
}
// 或者
// 提前定义好一个超类ResultData,里面分两个类Success<T>和Error<string, List<string>>
if (response.code() == 20x) {
result = ResultData.Success(response.body().data);
} else {
result = ResultData.Error(response.body().errCode, response.body().messages);
}
return result;
效果
- 自解释,易于理解
- URL风格一致,易于提供OpenAPI
- 可以使前后端的分离更加彻底
- 对资源的读操作是无副作用的,可以使用缓存,然后可以提高性能
批判
在一定场景下才有一定的优势.
有些复杂环境下不好套用REST的思想,无法表达复杂的操作.
有时为了达成目标,引入许多不必要的资源,导致数据更加复杂.
设计区分
主要从两个角度看API设计
- 操作
- 资源
如果以操作为中心设计API,
有时会发现在执行A操作之前需要先执行B操作,
过程比较杂乱不规则,
如果以资源为中心设计,则会变得比较简单.
难点
TODO 登陆
-
看做某种资源的状态的转变
简单的登陆比如说基于session,可以对session资源进行操作,
GET session/new表示获取登陆界面,
POST session来表示登陆,
DELETE session表示登出. -
TODO 发放令牌
令牌存在cookie中,用于API的验证?
多个查询条件
GET /objects
- ?limit=10
- ?offset=10
- ?page=2&per~page~=100;
- ?sortby=name&order=asc;
- ?xx~typeid~=1
批量操作
-
全部放入请求体中
1
2
3
4
5POST /api/resource/batch
Body: {
"method": "update",
"data": [{"id":1, "name":"aaa"}, {"id":2, "name":"bbb"}]
}有些网关会针对DELETE请求,会去除body,所以用PUT模拟PUT,PATCH,DELETE.
和其他的简单对比
SOAP
Simple Object Access Protocol
简单对象访问协议
SOAP主要使用xml格式,可以使用多种传输协议来传输,(HTTP, TCP, SMTP)
当它使用HTTP传输时,header的Content-type设置为 text/xml
.
SOAP包本身有点像HTML,
最外层是 <soap:Envelope>
,
内部包含可选的 <soap:Header>
,和必须的 <soap:Body>
如果想返回错误信息,则会在Body内部添加 <soap:Fault>
标签.
标签里进一步会分成 <faultcode>
, <faultstring>
, <faultactor/>
和 <detail />
比如处理前发送
1 |
|
处理时使用
1 |
|
得到的回复是
1 |
|
SOAP优点
- 除端对端以外,还能提供通过中介的验证,SOAP还有数据完整性和隐私性的实现
- 支持ACID事务
- 具备内置的成功/重试逻辑,可以保证消息的可靠
SOAP缺点
- 层层包装,效率不高
- 仅仅支持xml格式,不支持其他数据格式
- SOAP的读取无法被缓存
恰巧银行业务需要的就是SOAP
GraphQL
github一开始是使用REST的,
后来改成了GraphQL.
GraphQL靠两个概念 Schema
和 Resolver
分别实现了路由和参数校验,
Schema提供了相当于router的功能,
Resolver提供了相当于controller的功能.
因此GraphQL的概念变为 入口 (或者个人认为是条目?).
通过GraphQL查询语句对 GraphQL Schema
中的入口进行查询.
比如
1 | query { |
表示查询 users
,并返回 name
字段,
查询结果通常是
1 | { |
GraphQL的一个优点是
获取多个资源,只需要一个请求
在RESTful中需要分别查询
1 | /api/users |
在GraphQL中则只需要
1 | query GetUser { |
个人感觉GraphQL在查询的自解释性上要强于REST,
REST能做到的GraphQL能做到,
REST不能做到的GraphQL也能做到.