不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

一切尽在不言中 RESTful API设计指南(中)

一切尽在不言中(RESTful API设计指南 中篇)

回顾

上篇课程我们介绍了RESTful三层标准,并且我我以王国维的人生三层境界与RESTful的三层标准进行了一一对比。这意味着我们设计的RESTful接口也是有境界高低之分的。那么如何设计出高境界的RESTful接口就是下篇要重点介绍内容。

端点设计规范

我习惯性把RESTful接口叫做端点设计,因为在使用springboot框架的时候,各种资源被命名未endpoint。接下来我指的端点设计就是等价于RESTful接口设计,希望大家可以理解。

端点URL组成部分说明

我们知道一个端点完整的URL路径是由协议、端口、主机等好多部分组成。这是属于端点的路径设计,也是大家比较熟悉的。但是URL地址路径大家虽然经常性的使用,对于每个组成部分是不是都十分熟悉了?端点URL大概包含下面这些组成部分:

我举一个例子,大家看看能不能让上面的组成部分对号入座。比如:http://localhost:8080/saas-sale/webapi/orders?orderid=1 请大家心理默默映射下各组成部分对应关系。完成后再继续往下看......

参考答案:scheme是http,host是it.hzqiuxm.com,port是默认的80,path是saas-sale/webapi/orders,queryString是orderid=1

问题不止于此,接下来的问题才是重点,请问上述URL中资源指的是哪部分?要回答这个问题就必须弄清楚saas-sale/webapi/orders/order拆开后分别对应什么路径?在JAVA开发中拆开后saas-sale一般代表ContextPath;webapi一般代表ServletPath;orders/order一般代表PathInfo.所以代表资源部分的应该是orders.

端点URL设计的准则

下面介绍一些端点设计的准则,用来指导大家设计出更易理解的端点

  • URL命名必须是小写
  • URL中资源命名必须是名词
  • URL中资源命名应该用复数形式
  • URL必须是易读的
  • 一定不能暴露服务器架构(方法、目录结构)
  • 标准越高越好(之前介绍的3层)

总结起来八个字:小写、名词、复数、易读

所以结合HTTP常见操作,就构成了我们操作指令的命名规则:
- 客户端发出的数据操作指令:动词 + 宾语 (动宾结构),例子:GET(动词) /articles(宾语)

宾语就是API的URL部分,必须是名词,如果在URL里带上get/add/del/create等动词都是错误的。例如: getAllBooks,createCar
名词采用单数还是复数没有特别的规定,但是项目团队内部必须保持一致(一般用复数)
避免多级URL,如果资源存在多级分类,除了第一级,其它级别用参数字符串表达;例如:获取某个作者的某一类博客

特别说明命名:核心本质是让人理解,而不时翻译成晦涩的英文(否则为什么不推荐用拼音首字母是吧?)

URL实际案例分析

下面是一些实际项目中存在的URL,大家可以看看它们是否违反了端点设计规范,分别违反了哪几条?

端口中过滤器的设计

在大部分的查询需求中都会有分页、筛选、排序等需求。我们称这类需求为过滤器。过滤器是最有效的方式去处理那些获取资源集合的请求。所以只要出现GET的请求,就应该通过URL来过滤信息。
以下有一些过滤器的例子,你可以参考下:

  • ?limit=10: 减少返回给客户端的结果数量(用于分页)
  • ?offset=10: 发送一堆信息给客户端(用于分页)
  • ?animal_type_id=1: 使用条件匹配来过滤记录
  • ?sortby=name&order=asc: 对结果按特定属性进行排序

有些过滤器可能会与端点URL的效果重复。例如我之前提到的GET /zoo/ZID/animals。它也同样可以通过GET /animals?zoo_id=ZID来实现。独立的端点会让客户端更好过一些,因为他们的需求往往超出你的预期。本文中提到这种冗余差异可能对第三方开发者并不可见。

无论怎么说,当你准备过滤或排序数据时,你必须明确的将那些客户端可以过滤或排序的列放到白名单中,因为我们不想将任何的数据库错误和所有列都发送给客户端。

响应状态码使用

RESTful设计思想提倡我们尽量去复用HTTP的一些规范和准则,所以关于响应状态码使用HTTP的标准就可以了:

  • 1xx:相关信息
  • 2xx:操作成功:GET:200; POST:201; PUT 200; PATCH 200; DELETE 204; 202 收到请求,等待处理(异步请求时使用)
  • 3xx:重定向
  • 4xx:客户端错误 :400 错误请求;401 未认证;403 无权限;404 资源不存在; 405 HTTP方法权限不足; 410 资源已转移;415 要求的返回格式不支持;422 附件无法处理;429 请求次数太多超过限制
  • 5xx:服务器错误:500 服务器处理时发生异常;503 服务器无法处理请求,维护中

这里特别需要说明的几个响应码是才做成功的时候有4个:200(查询成功),201(添加成功), 202(异步请求成功,等待处理),204(删除成功)。我们大多数初级开发人员会习惯都用只用200来代表成功,这个需要特别注意。

端点设计拾遗

设计规范测验

下面是上面端口设计规范检测的答案,在这个基础上我们来尝试优化改进下这些端口

参考答案:
根据不同的业务场景,大家设计的端点可以会有点不一样,但是原则上应该都需要符合设计原则

PS:上图中关于短信验证码的两种设计,在业务场景上会有什么区别?
POST /users/verify_code 说明短信发送是在用户模块
POST /verify/sms_code 说明短信发送是一个独立的服务

连通性设计

连通性实现方式

接下来是端口设计的另一个重头戏:连通性设计。很多开发人员对这一点比较陌生,可能你是第一次听说这个概念。其实REST的一个重要特性就是连通性,其核心思想就是把一个个把资源链接起来,比如我完成了下订单的操作,返回的报文中应该告诉我下完订单后的支付访问URL是什么。它的实现方式一般有两种:WebLink的方式和HATEOAS的方式。

  • Web Link定义在IETFRFC 5988(Web Linking),是通过在HTTP头中定义链接信息,以描述当前页面与链接页面之间的关系。Web Link是一种过渡型链接(Transitional Links)。JAX-RS 2.0引入了javax.ws.rs.core.Link类,用来处理Web Link的表述。因为是过渡型的,目前大多数应用不再使用这种方式。

  • HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用程序状态引擎)。HATEOAS的形式是包含链接信息的超媒体文档,HATEOAS的核心是“引擎”,该引擎的目的是通过请求的响应实体将超媒体信息返回给客户端,超媒体信息可以告诉用户,如果接下来选择去往某个链接(或者链接列表中的某个链接),应用的状态就会如超媒体描述的那样发生转变。HATEOAS是一种结构型链接(Structural Links)。

很多人在设计RESTful架构时,使用很多时间来寻找漂亮的URI,而忽略了超媒体。所以,应该多花一些时间来给资源的表述提供链接(蓦然回首,寻找那人在灯火阑珊处),而不是仅仅专注于"资源的CRUD"(为伊消得人憔悴)。

连通性设计例子介绍

根据名称查询某商品图片信息,根据前端展现要求,有大中小三种图片格式,我们应该如何设计连通性URL呢?

请求的URL没有什么特殊性,连通性主要是体现在响应上,假设请求URL为:


GET https://api.example.com/products/?name=“something”

那么一个典型的连通性响应应该是这样的:

{ "name": “something",

"picture":

  { "large": "https://somecdn.com/pictures/1200x1200.png",    

           "medium": "https://somecdn.com/pictures/100x100.png",     

           "small": "https://somecdn.com/pictures/10x10.png"      

  }

}

在响应中包含了链接地址,因此使用该API的客户端就能够自由选择要下载怎样的信息。这些链接告知了客户端有哪些选择,并且它们的地址在哪里。因此在这里我们无需同时返回三个不同版本的用户档案图片,我们所做的只是告诉客户端有三种可用的图片尺寸可以选择,并且告诉客户端能够在哪里找到这些图片。

这样一来,客户端就能够根据不同的场景,做出符合自身需要的选择。而且,如果客户端只需要一种格式的图片,那就无需下载全部三种版本的图片了。这样一来可谓一箭三雕:既减少了网络负载,又增进了客户端的灵活性,更增进了API的可探索性。

这就是超媒体的本质:经由资源之间的链接,我们改变整个应用的状态,即超媒体转换了分布式应用的状态。需要注意的是,服务器和消费者两者间交换的是资源状态的表述,而不是应用的状态,被转移的表述中包括了反应应用状态的链接。

weblink连通性设计例子

上面是github获取某个组织下的项目列表的请求,可以看到在响应头里边增加Link头告诉客户端怎么访问下一页和最后一页的记录。 而在响应体里边,用url来链接项目所有者和项目地址

超媒体引擎HATEOAS

假设我们有个请求:http://localhost:8080/greeting ,在没有连通性设计之前,只要返回具体的值就可以了,但是如果要加入连通性,那么根据这个请求,我们需要告诉客户端,它可以是否能够传入相关参数。
我们可以在响应中加上一个推导的URL响应 http://localhost:8080/greeting?name=User,那么该请求最终的响应可能是这样的:

{

"content":"Hello, World!",

"_links":{

"self":{

"href":"http://localhost:8080/greeting?name=World"

    }

  }

}

我们接着看一个例子,该例子是创建订单后通过链接引导客户端如何去付款的一个连通性响应:

连通性带来的好处:

  • 1 GET请求包含允许执行的链接操作,避免硬编码
  • 2 资源信息体现了当前状态下允许操作,前端轻业务化

Spring HATEOAS介绍

Spring HATEOAS是Spring官方提供的HATEOAS一种实现方式,官方定义:Simplifies creating REST representations that follow the HATEOAS principle.(简化了创建遵循HATEOAS原则的REST表示)
它具体以下几个特点:

  • 使用link来作为模型类来表示资源模型
  • Link builder API创建指向Spring MVC控制器方法的链接
  • 支持像HAL这样的超媒体格式
  • 支持SpringBoot简化配置

它包含了两个核心概念:

  • Links 链接:Link值对象跟随该原子链接定义和包括一个rel与一个href属性,它包含一些众所周知的rels的常量,例如self,next等等
  • Resource资源:资源的每个表示几乎都包含一些链接(至少是self一个),spring Hateoas提供了统一的基类
    其实我之前举的一个greeting请求例子就是用Spring HATEOAS来实现的, 在springboot中使用也很简单,以Gradle为例,只要添加以下依赖就可以:
compile("org.springframework.boot:spring-boot-starter-hateoas")

具体示例和文档大家可以去官方网站上去了解:

  • https://docs.spring.io/spring-hateoas/docs/0.25.1.RELEASE/reference/html/
  • https://github.com/spring-projects/spring-hateoas-examples

小结

兼容性演化

  • 必须兼容老的API

无论你正在构建什么,无论你在入手前做了多少计划,你核心的应用总会发生变化,数据关系也会变化,资源上的属性也会被增加或删除。只要你的项目还活着,并且有大量的用户在用,这种情况总是会发生。

请谨记一点,API是服务器与客户端之间的一个公共契约。如果你对服务器上的API做了一个更改,并且这些更改无法向后兼容,那么你就打破了这个契约,客户端又会要求你重新支持它。为了避免这样的事情,你既要确保应用程序逐步的演变,又要让客户端满意。那么你必须在引入新版本API的同时保持旧版本API仍然可用。

注:如果你只是简单的增加一个新的特性到API上,如资源上的一个新属性或者增加一个新的端点,你不需要增加API的版本。因为这些并不会造成向后兼容性的问题,你只需要修改文档即可。

随着时间的推移,你可能声明不再支持某些旧版本的API。申明不支持一个特性并不意味着关闭或者破坏它。而是告诉客户端旧版本的API将在某个特定的时间被删除,并且建议他们使用新版本的API。

  • 语义化版本管理

一个好的RESTful API会在URL中包含版本信息。另一种比较常见的方案是在请求头里面保持版本信息。但是跟很多不同的第三方开发者一起工作后,我可以很明确的告诉你,在请求头里面包含版本信息远没有放在URL里面来的容易。

对于兼容性具体措施用一句话概括就是:严以律己宽以待人:服务端为请求属性提供默认值,客户端忽略额外的响应属性

面临挑战

  • 1 一个请求中获取多个资源:正常业务中往往会出现一个请求来获取多个服务的相关资源,比如订单查询时还需要获取厨房、送餐、支付等信息

    解决方案:GraphQL/Falcor高效数据获取

  • 2 把操作映射为HTTP动词的挑战:HTTP提供的动词很有限,可能无法准确表达出业务对资源的操作含义,比如商品的上架,下架,冻结

    解决方案:
    ①将每种操作定义为子资源(非GET)
    ②将动词指定为查询参数(GET)
    ③无业务场景的也可以放在请求体中

优缺点

优点
  • 简单
  • 可调试的工具多
  • http对防火墙友好
  • 无中间代理,简化架构

缺点
  • 只支持请求/响应的通信方式
  • 可能导致可用性降低
  • 单个请求获取多个资源有挑战性
  • 操作和HTTP动词映射挑战

结束语

到此为止,我们RESTful设计的理论部分都已经介绍完了,希望所介绍的内容对大家有用,在最后一篇中,我将设计一个虚拟的项目,提出一些需求让后大家一起来完成端口设计,看看如何利用这些理论作为指导去设计一个高境界的RESTful接口。

未经允许不得转载:菡萏如佳人 » 一切尽在不言中

欢迎加入极客江湖

进入江湖关于作者