zoukankan      html  css  js  c++  java
  • RESTful API设计实践

    在当前程序设计语言层出不穷,技术架构五花八门的现在,系统间交互已经渗透到程序设计的方方面面。作为当前最成熟的API设计理论,RESTful API在当下得到了最广泛的应用。

    REST(Representational State Transfer,表属性状态转移)的概念源自于Roy Thomas Fielding博士在2000年发表的论文《Architectural Styles and the Design of Network-based Software Architectures》,而我们常说的RESTful则是指REST风格的API设计,即该API的设计应用了REST的思想。也就是说,基于REST的思想所设计出的API即可称之为RESTful API。但是一般情况下,我们对RESTful API的定义则更加狭隘,通常情况下,我们所属的RESTful API则是使用HTTP通信协议来传递JSON格式数据的API。

    本文中,我们主要讨论这种基于HTTP协议传递JSON格式数据的RESTful API设计的最佳实践。

     

    1.    资源

    在RESTful的世界中,一切皆为资源,就好像在面向对象的世界中,一切皆为对象一样。因此在设计RESTful API的第一步,就是对资源的抽象。这里需要我们将系统中的一切都抽象为资源,包括数据和行为。在定义了资源之后,再为资源附加上其所允许的操作,即完成了对RESTful API的定义。

    1.1   资源

    对数据的抽象相对比较直观,一般来讲,数据可以直接映射成一种资源,例如订单、存储阵列、虚拟机等。而对于行为则会显得稍微有点抽象,例如对文件的拷贝或者移动,也需要视作一种资源。在实际的API设计中,资源呈现为HTTP中的URI,通常使用资源的复数形式来定义这个URI。例如,“/hosts”表示主机这种资源,而“/hosts/1”则表示ID为“1”的主机实例。

    对于不同资源间所存在的关系,通过子资源的方式呈现。例如,“/hosts/1/switches”表示主机1所连接的交换机资源,而“/hosts/1/switches/1”表示主机1所连接的交换机1,即主机1与交换机1相连。等价地,“/switches/1/hosts/1”为交换机1与主机1相连,其所表示的含义是相同的,但是其所处的视角不同,分别以主机和交换机的视角来描述这种情况。

    而对于行为,例如主机上下电,可通过“/hosts/1/powers”来表示主机的电源情况。

    1.2   操作

    在抽象了资源之后,就需要定义对这些资源的操作,而对每种资源,最多只能应用6种操作,即创建、删除、全量修改、增量修改、查询单个资源、列出所有资源。这些操作一般通过HTTP的URI和请求方法共同表示。

    以主机资源为例:

    URI

    请求方法

    功能

    /hosts

    POST

    创建一个主机实例。

    /hosts/{id}

    PUT

    全量修改指定ID的主机信息。

    /hosts/{id}

    PATCH

    增量修改指定ID的主机信息。

    /hosts/{id}

    DELETE

    删除指定ID的主机实例。

    /hosts/{id}

    GET

    查询指定ID的主机信息。

    /hosts

    GET

    列出符合条件的主机信息,通过URL的查询参数来指定查询条件。

    以主机与交换机的关系为例:

    URI

    请求方法

    功能

    /hosts/{hostId}/switches/{switchId}

    POST

    将指定ID的主机与指定ID的交换机连接。

    /hosts/{hostId}/switches/{switchId}

    PUT

    全量修改指定ID主机与指定ID交换机的连接信息。

    /hosts/{hostId}/switches/{switchId}

    PATCH

    增量修改指定ID主机与指定ID交换机的连接信息。

    /hosts/{hostId}/switches/{switchId}

    DELETE

    断开指定ID的主机与指定ID的交换机的连接。

    /hosts/{hostId}/switches/{switchId}

    GET

    查询指定ID的主机与指定ID的交换机的连接信息。

    /hosts/{hostId}/switches

    GET

    列出指定主机所连接的交换机的连接信息,通过URL查询参数指定查询条件。

    以主机的电源为例:

    URI

    请求方法

    功能

    /hosts/{id}/powers

    POST

    为主机上电。

    /hosts/{id}/powers

    DELETE

    为主机下电。

    /hosts/{id}/powers

    GET

    查询主机的电源情况。

    在上例中,只描述了主机简单的电源情况。若需要描述更详细的电源情况,如主机上下电的历史信息、每次上下电的操作人等信息,可为上电和下电分别定义资源。

    1.2.1    全量修改与增量修改

    PUT和PATCH都可以表示修改,但分别表示全量修改与增量修改。其中的区别在于对未指定的数据的处理逻辑。

    例如,主机包含名称和注释属性,当传递的消息中仅包括主机的名称信息而不包含注释信息时,若为全量修改,则需要将注释属性设置到初始状态(无任何内容);而若为增量修改,则应仅修改名称,而不修改注释。

    另外需要注意的是,对于增量修改的API,参数中不包含指定属性和指定属性的值为null应表示不同的含义。不包含指定属性时时,其含义为不修改该属性;而若包含指定属性,且值为null,则其含义为将属性设置为null值(或初始值)。

    但是通常情况下,对参数的解析(反序列化)通常由框架进行,如Spring MVC,此时在业务逻辑中已无法区分参数中是未设置某个属性,还是将属性设置为null值,更普遍的做法是只区分null和空值(如空字符串)。但是在协议层面上,这种做法本质上是有歧义的。这里的协议层面是指直接发送一个HTTP报文。因此为了避免这种歧义,应至少在文档层面上对这种处理方式进行解释和说明。

    1.2.2    单个查询与批量查询

    再以查询主机为例,一般来讲,“/host/1”与“/hosts?id=1”都是查询ID为1的主机,但其返回结果存在差异,前者返回的是一个主机实例的信息,而后者则返回的是一个列表,其中仅包含一个主机实例的信息。虽然其关键数据一致,但返回的数据结构不同。

    通常情况,批量查询接口被称为“list”,即“列出资源信息”的含义。

     

    2.    异常

    2.1.    HTTP状态码

    在基于HTTP协议的RESTful API定义中,异常的定义应遵循HTTP的机制。在HTTP中,通过状态码来表示资源的状态。例如404表示资源不存在,410表示资源已经被删除等。常用的HTTP状态码有:

    状态码

    原因短语

    含义

    200

    OK

    请求成功。常用于GET请求。响应中应包含资源信息。

    201

    Created

    请求成功,且有一个资源根据请求的内容被创建。常用语POST请求。

    202

    Accepted

    服务端已接受该请求,但后续的处理过程可能会失败。常见于请求的异步处理。通常情况下需要返回一个凭证,用以持续跟踪请求的处理情况。常见于POST、PUT、PATCH、DELETE请求。

    204

    No Content

    响应中没有响应内容,常用于DELETE请求。

    205

    Reset Content

    客户端应重新查询资源内容。常用于PUT、PATCH请求。

    400

    Bad Request

    请求格式不正确,应用于参数的合法性校验,通常也用于参数的有效性校验。

    401

    Unauthorized

    未授权,需要进行权限认证后再进行请求。

    404

    Not Found

    所需要操作的资源不存在,常用于PUT、PATCH、DELETE、GET请求。这里需要注意的是,一般来讲,即使是需要删除某个资源,此时资源不存在,也应使用该响应码。因为客户端可能需要区分资源是否是因自己的请求而被删除。

    406

    Not Acceptable

    无法提供合适的内容。通常情况下表示服务端已准备好资源,但是无法将这些内容转换为客户端所需的格式(请求的Accept头)。

    409

    Conflict

    被请求的资源因状态冲突而无法提供服务。例如资源处于不可修改状态,而客户端提交了PUT或PATCH请求。

    410

    Gone

    表示资源已被删除。与404的区别在于,404表示当前服务端没有该资源,该资源可能从未被创建。而410则更加明确该资源曾经存在过。一般情况下这种情况会被归为404一同表示。

    500

    Internal Server Error

    服务端发生未知错误。一般情况下,这个错误码表示服务进入了未处理的异常分支,通常仅用于表示代码BUG。

    HTTP中还有很多其他的状态码,但一般不需要业务代码来感知。例如503所表示的服务不可用(Service Unavailable)通常用于服务应用的启动、升级等场景,但这个错误码通常情况下应被底层的应用程序框架或Web容器所处理。

    2.2.    异常信息

    一般来讲,在响应的HTTP状态码为4XX或5XX时,报文内容应承载异常信息,而非用户数据。而在状态码为2XX时,响应报文中应仅承载用户数据,而不体现异常信息的数据结构。

    这与高级语言中的异常体系的思路是一致的。在高级语言中,如Java、C#等,当发生异常时,会抛出一个Exception,待调用方程序捕获处理,而不会在方法的返回值中表示是否发生了异常。

     

    3.    示例

    假定我们有一个应用的部署系统,需要提供应用的创建、修改、删除、查询、部署能力,那么这些API的交互过程应按以下方式完成。

    3.1.    创建应用

    当我们需要创建一个应用时,我们应当向服务端发送以下信息:

    POST /apps HTTP/1.1

    Content-Type: application/json;charset=UTF-8

    Content-Length: 168

    Accept: application/json;charset=UTF-8

    Accept-Language: en-US

    Accept-Encoding: gzip, deflate

     

    {

        "name": "my-app",

        "archive": "http://cloud.com/archives/my-app.iso",

        "spec": {

            "cpus": 1,

            "memory": "100MB",

            "disk": "2GB"

        }

    }

    这个请求的首部中,我们指定了HTTP报文内容是一个JSON格式的文本,以UTF-8字符集进行编码,内容长度为168个字节。同时我们需要服务端给我们的响应也是一个JSON格式的文本,以UTF-8字符集编码,期望的语言为英语,可使用gzip或deflate算法进行压缩。

    而请求内容中表示,我们所创建的应用名为“my-app”,归档件所在地址为“http://cloud.com/archives/my-app.iso”,应用所需的运行环境规格为1个CPU,100MB内存和2GB的磁盘空间。

    服务端可能给我们返回以下内容的信息:

    HTTP/1.1 201 Created

    Content-Type: application/json;charset=UTF-8

    Content-Length: 392

    Content-Encoding: gzip

    Date: Wed, 20 Jan 2021 23:41:56 GMT

     

    {

        "app": {

            "id": "01d59735-25e6-e227-23b8-926efbf34c82",

            "name": "my-app",

            "archive": "http://cloud.com/archives/my-app.iso",

            "spec": {

                "cpus": 1,

                "memory": "100MB",

                "disk": "2GB"

            },

            "state": "available",

            "lastModifier": "Jishi Liang",

            "lastModificationTime": "2021/1/20 23:41:56"

        }

    }

    这个请求的首部中,指定了响应报文的内容是一个JSON格式的文本,以UTF-8字符集编码,长度为392个字节。使用gzip算法进行压缩,并在2021/1/20 23:41:56返回该响应。

    响应的内容中为已经创建成功的应用的信息,除请求中所提交的信息外,还额外附加了应用的唯一标识、状态、创建人、创建时间等信息。

    这里需要注意的是,响应的HTTP状态码为201(Created)。而且,一般来讲,响应中的数据会多于请求所提交的数据,并且这些数据通常是不可被修改的。

    3.2.    部署应用

    请求信息:

    POST /apps/01d59735-25e6-e227-23b8-926efbf34c82/deployments HTTP/1.1

    Content-Type: application/json;charset=UTF-8

    Content-Length: 0

    Accept: application/json;charset=UTF-8

    Accept-Language: en-US

    Accept-Encoding: gzip, deflate

     

    请求中表示,我们需要为应用“01d59735-25e6-e227-23b8-926efbf34c82“创建一个部署服务。这里的”部署“是一个资源,该资源会对应用进行部署。

    HTTP/1.1 202 Accepted

    Content-Type: application/json;charset=UTF-8

    Content-Length: 52

    Content-Encoding: gzip

    Date: Wed, 20 Jan 2021 23:42:21 GMT

     

    {

        "id": "ea8bc7e1-07e4-8ca7-fa72-283a46efb122"

    }

    这个响应表示,服务端已接受了请求,并提供了部署的唯一标识,以供客户端后续跟进。通常情况下这个更多会返回一个异步任务的唯一标识。

    3.3.    查询部署信息

    请求信息:

    GET /apps/01d59735-25e6-e227-23b8-926efbf34c82/deployments/ea8bc7e1-07e4-8ca7-fa72-283a46efb122 HTTP/1.1

    Content-Type: application/json;charset=UTF-8

    Content-Length: 0

    Accept: application/json;charset=UTF-8

    Accept-Language: en-US

    Accept-Encoding: gzip, deflate

     

    这个请求中,我们通过首行的URI,告知服务端,我们需要查询应用ID为 “01d59735-25e6-e227-23b8-926efbf34c82”且部署ID为“ea8bc7e1-07e4-8ca7-fa72-283a46efb122”的部署信息。

    响应信息:

    HTTP/1.1 200 OK

    Content-Type: application/json;charset=UTF-8

    Content-Length: 260

    Content-Encoding: gzip

    Date: Wed, 20 Jan 2021 23:43:38 GMT

     

    {

        "deployment": {

            "id": "ea8bc7e1-07e4-8ca7-fa72-283a46efb122",

            "app": {

                "id": "01d59735-25e6-e227-23b8-926efbf34c82",

                "host": "192.168.13.68",

                "port": 9019

            },

            "state": "deploying"

        }

    }

    这个响应向我们展示了当前的部署状态,应用部署在主机“192.168.13.68”上,且使用9019端口,当前依旧在部署中。

    3.4.    修改应用

    请求信息:

    PATCH /apps/01d59735-25e6-e227-23b8-926efbf34c82 HTTP/1.1

    Content-Type: application/json;charset=UTF-8

    Content-Length: 45

    Accept: application/json;charset=UTF-8

    Accept-Language: en-US

    Accept-Encoding: gzip, deflate

     

    {

        "spec": {

            "disk": "4GB"

        }

    }

    这个请求中,我们通过PATCH请求对应用信息进行增量更新,将将所需运行环境的规格中的磁盘空间更新至4GB。所需修改的应用的唯一标识在首行中的URI中呈现。

    响应信息:

    HTTP/1.1 205 Reset Content

    Content-Type: application/json;charset=UTF-8

    Content-Length: 0

    Content-Encoding: gzip

    Date: Wed, 20 Jan 2021 23:53:32 GMT

     

    这个响应告诉我们,信息已经被修改,我们需要重新查询来更新客户端所持有的数据。

    3.5.    列出应用

    请求信息:

    GET /apps?offset=0&limit=100 HTTP/1.1

    Content-Type: application/json;charset=UTF-8

    Content-Length: 0

    Accept: application/json;charset=UTF-8

    Accept-Language: en-US

    Accept-Encoding: gzip, deflate

     

    这个请求中,我们通过首行中URI的查询参数,指定我们需要查询所有应用中的第1~100条应用信息。

    响应信息:

    HTTP/1.1 200 OK

    Content-Type: application/json;charset=UTF-8

    Content-Length: 446

    Content-Encoding: gzip

    Date: Wed, 20 Jan 2021 23:41:56 GMT

     

    {

        "apps": [{

            "id": "01d59735-25e6-e227-23b8-926efbf34c82",

            "name": "my-app",

            "archive": "http://cloud.com/archives/my-app.iso",

            "spec": {

                "cpus": 1,

                "memory": "100MB",

                "disk": "4GB"

            },

            "state": "available",

            "lastModifier": "Jishi Liang",

            "lastModificationTime": "2021/1/20 23:53:32"

        }],

        "offset": 0,

        "limit": 100,

        "total": 1

    }

    响应中apps表示查询到的应用信息列表,offset和limit分别表示查询到的结果集在总结果集中的偏移量和限定长度,total表示总结果集中仅包含1个应用记录。

    3.6.    删除应用

    请求信息:

    DELETE /apps/01d59735-25e6-e227-23b8-926efbf34c82 HTTP/1.1

    Content-Type: application/json;charset=UTF-8

    Content-Length: 0

    Accept: application/json;charset=UTF-8

    Accept-Language: en-US

    Accept-Encoding: gzip, deflate

     

    这个请求中,我们通过URI中资源的唯一标识,来指定我们需要删除这个应用。

    响应信息:

    HTTP/1.1 204 No Content

    Content-Type: application/json;charset=UTF-8

    Content-Length: 0

    Content-Encoding: gzip

    Date: Wed, 20 Jan 2021 23:41:56 GMT

     

    这个响应告诉我们,这个资源已经被删除,但是服务端没有需要返回给我们的数据。

  • 相关阅读:
    不定方程(Exgcd)
    [模板]乘法逆元
    STL-Deque(双端队列)与单调队列的实现
    最优得分 score
    摆书 book
    [模板]树链剖分
    [模板]Splay
    NOIP2013 货车运输
    Java的类类型和类的动态加载
    Java:搜索特定后缀名的文件
  • 原文地址:https://www.cnblogs.com/talefox/p/14305941.html
Copyright © 2011-2022 走看看