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,返回的状态码要尽量精确

返回值结构设计

  1. 标准

    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位
  2. 关于错误的扩展

    通常错误时想知道更进一步的信息.于是有些系统会这样做:

    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
    10
    if (response.code() != 20x) {
    // ...
    } else {
    foreach(<key,errors> in response.body()) {
    foreach(error in errors) {
    res[key][] = error;
    }
    }
    return res;
    }

    通常不想违背原始的状态码,即一定不会在200时返回有error的body.
    这样做,系统会有一些偏离简洁,但想表达的信息已经可以表达了.

  3. 较为统一的版本

    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
    16
    if (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 登陆

  1. 看做某种资源的状态的转变

    简单的登陆比如说基于session,可以对session资源进行操作,
    GET session/new表示获取登陆界面,
    POST session来表示登陆,
    DELETE session表示登出.

  2. TODO 发放令牌

    令牌存在cookie中,用于API的验证?

多个查询条件

GET /objects

  • ?limit=10
  • ?offset=10
  • ?page=2&per~page~=100;
  • ?sortby=name&order=asc;
  • ?xx~typeid~=1

批量操作

  1. 全部放入请求体中

    1
    2
    3
    4
    5
    POST /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
2
3
4
5
6
7
8
9
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:add xmlns:ns2="http://lenve.server/">
<a>3</a>
<b>4</b>
</ns2:add>
</S:Body>
</S:Envelope>

处理时使用

1
2
3
4
5
6
@WebService
public interface IMyServer {

@WebResult(name="addResult")
public int add(@WebParam(name="a")int a,@WebParam(name="b")int b);
}

得到的回复是

1
2
3
4
5
6
7
8
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:addResponse xmlns:ns2="http://lenve.server/">
<addResult>7</addResult>
</ns2:addResponse>
</S:Body>
</S:Envelope>

SOAP优点

  • 除端对端以外,还能提供通过中介的验证,SOAP还有数据完整性和隐私性的实现
  • 支持ACID事务
  • 具备内置的成功/重试逻辑,可以保证消息的可靠

SOAP缺点

  • 层层包装,效率不高
  • 仅仅支持xml格式,不支持其他数据格式
  • SOAP的读取无法被缓存

恰巧银行业务需要的就是SOAP

GraphQL

github一开始是使用REST的,
后来改成了GraphQL.

GraphQL靠两个概念 SchemaResolver 分别实现了路由和参数校验,
Schema提供了相当于router的功能,
Resolver提供了相当于controller的功能.
因此GraphQL的概念变为 入口 (或者个人认为是条目?).
通过GraphQL查询语句对 GraphQL Schema 中的入口进行查询.
比如

1
2
3
4
5
query {
users {
name
}
}

表示查询 users,并返回 name 字段,
查询结果通常是

1
2
3
4
5
6
7
8
9
10
11
12
{
"data": {
"users": [
{
"name": "Jack"
},
{
"name": "Joe"
}
]
}
}

GraphQL的一个优点是
获取多个资源,只需要一个请求

在RESTful中需要分别查询

1
2
/api/users
/aui/user?name=Jack

在GraphQL中则只需要

1
2
3
4
5
6
7
8
9
10
query GetUser {
users {
name
}
user(name: "Jack") {
name
gender
tags
}
}

个人感觉GraphQL在查询的自解释性上要强于REST,
REST能做到的GraphQL能做到,
REST不能做到的GraphQL也能做到.

参考

  1. 阮一峰的博客常常通俗易懂
  2. 批判
  3. 一个有关的个人博客
  4. GraphQL的有关介绍