关于RESTful设计风格的思考
软件开发从单机软件到互联网化,从互联网单体应用到系统拆分、前后端分离,再到现在的微服务。软件架构越来越庞大复杂,相关联的领域划分更细致、职责更专一,这样各部分之间的交互就很频繁。RESTful作为一种架构风格、一种接口设计规范,简单、标准、易扩展让其得到了越来越多的应用。
什么是RESTful?
REST 全称 Representational State Transfer,翻译为资源表现层状态转化,资源指服务中的实体或信息;表现层指信息的展现形式,如json、xml、html等;状态转化指接口访问过程中数据和状态的变化,作为http无状态服务,业务的实现必然要涉及到数据和状态的变化。
RESTful的本质是一种软件架构风格,核心在于面向资源设计接口,即将所有的接口描述为对资源的操作,目的在于降低开发的复杂性,提高系统的可伸缩性。设计的准则主要有以下三点:
- 网络上所有的事物都可以被抽象为资源
- 每个资源都有唯一的资源标识,对资源的操作不会改变这些标识
- 所有的操作都是无状态的
采用URI标识资源
RESTful Web API采用面向资源的架构,所以在设计之初首先需要考虑的是有哪些资源可供操作。资源是一个很宽泛的概念,任何寄宿于Web可供操作的事物均可视为资源。资源可以体现为经过持久化处理保存到磁盘上的某个文件或者数据库中某个表的某条记录,也可以是Web应用接受到请求后采用某种算法计算得出的结果。资源可以体现为一个具体的物理对象,它也可以是一个抽象的流程。
一个资源必须具有一个或者多个标识,既然我们设计的Web API,那么很自然地应该采用URI来作为资源的标识。作为资源标识的URI最好具有可读性,因为具有可读性的URI更容易被使用,使用者一看就知道被标识的是何种资源,比如如下一些URI就具有很好的可读性。
http://www.artech.com/employees/c001(编号C001的员工)
http://www.artech.com/sales/2013/12/31(2013年12月31日的销售额)
http://www.artech.com/orders/2013/q4(2013年第4季度签订的订单)
除了必要的标志性和可选的可读性之外,标识资源的URI应该具有可寻址性。也就是说,URI不仅仅指明了被标识资源所在的位置,而且通过这个URI可以直接获取目标资源。URI除了可以标识某个独立的资源外(比如http://www.artech.com/employees/c001
),还可以标识一组资源的集合或者资源的容器(比如http://www.artech.com/orders/2013/q4
)。当然,一组同类资源的集合或者存放一组同类资源的容器本身也可以视为另一种类型的复合型(Composite)资源,所以URI总是标识某个资源这种说法是没有问题的。
使用HTTP方便表示对资源的动作
由于RESTful Web API采用了同一的接口,所以其成员体现为针对同一资源的操作。对于Web来说,针对资源的操作通过HTTP方法来体现。我们应该将两者统一起来,是Web API分别针对CRUD的操作只能接受具有对应HTTP方法的请求。
接下来介绍7个常用的HTTP方法。首先GET、HEAD和OPTIONS这三个HTTP方法旨在发送请求以或者所需的信息。对于GET,相应所有人对它已经非常熟悉了,它用于获取所需的资源,服务器一般讲对应的资源置于响应的主体部分返回给客户端。
HEAD和OPTIONS相对少见。从资源操作的语义来讲,一个针对某个目标资源发送的HEAD请求一般不是为了获取目标资源本身的内容,而是得到描述目标资源的元数据信息。服务器一般将对应资源的元数据置于响应的报头集合返回给客户端,这样的响应一般不具有主体部分。OPTIONS请求旨在发送一种探测请求以确定针对某个目标地址的请求必须具有怎样的约束(比如应该采用怎样的HTTP方法以及自定义的请求报头),然后根据其约束发送真正的请求。比如针对跨域资源的预检(Preflight)请求采用的HTTP方法就是OPTIONS。
至于其它4中HTTP方法(POST、PUT、PATCH和DELETE),它们旨在针对目标资源作添加、修改和删除操作。对于DELETE,它的语义很明确,就是删除一个已经存在的资源。我们着重推荐其它三个旨在完成资源的添加和修改的HTTP方法作一个简单的介绍。
通过发送POST和PUT请求均可以添加一个新的资源,但是两者的不同之处在于:对于前者,请求着一般不能确定标识添加资源最终采用的URI,即服务端最终为成功添加的资源指定URI;对于后者,最终标识添加资源的URI是可以由请求者控制的。也正是因为这个原因,如果发送PUT请求,我们一般直接将标识添加资源的URI作为请求的URI;对于POST请求来说,其URI一般是标识添加资源存放容器的URI。
比如我们分别发送PUT和POST请求以添加一个员工,标识员工的URI由其员工ID来决定。如果员工ID由客户端来指定,我们可以发送PUT请求;如果员工ID由服务端生成,我们一般发送POST请求。具体的请求与下面提供的代码片断类似,可以看出它们的URI也是不一样的。
PUT http://www.artech.com/employees/300357 HTTP/1.1
<employee>
<id>300357</id>
<name>张三</name>
<gender>男<gender>
<birthdate>1981-08-24</birthdate>
<department>3041</department>
</employee>
POST http://www.artech.com/employees HTTP/1.1
<employee>
<name>张三</name>
<gender>男<gender>
<birthdate>1981-08-24</birthdate>
<department>3041</department>
</employee>
除了进行资源的添加,PUT请求还能用于资源的修改。由于请求包含提交资源的标识(可以放在URI中,也可以置于保存在主体部分的资源内容中),所以服务端能够定位到对应的资源予以修改。对于POST和PUT,也存在一种一刀切的说法:POST用于添加,PUT用于修改。我个人比较认可的是:如果PUT提供的资源不存在,则做添加操作,否则做修改。
对于发送PUT请求以修改某个存在的资源,服务器一般会将提供资源将原有资源整体覆盖掉。如果需要进行局部修改,我们推荐请求采用PATCH方法,因为从语义上讲Patch就是打补丁的意思。
无状态性
RESTful只维护资源的状态,而不需要维护客户端的状态。对于它来说,每次请求都是全新的,它只需要针对本次请求作相应的操作,不需要将本次请求的相关信息记录下来以便用于后续来自相同客户端请求的处理。
对于上面我们介绍的RESTful的这些个特性,它们都是要求我们为了满足这些特征做点什么,唯有这个无状态却是要求我们不要做什么,因为HTTP本身就是无状态的。举个例子,一个网页通过调用Web API分页获取符合查询条件的记录。一般情况下,页面导航均具有上一页和下一页链接用于呈现当前页的前一页和后一页的记录。那么现在有两种实现方式返回上下页的记录。
- Web API不仅仅会定义根据具体页码的数据查询定义相关的操作,还会针对上一页和下一页这样的请求定义单独的操作。它自身会根据客户端的Session ID对每次数据返回的页面在本地进行保存,以便能够知道上一页和下一页具体是哪一页。
- Web API只会定义根据具体页码的数据查询定义相关的操作,当前返回数据的页码由客户端来维护。
第一种貌似很智能,其实就是一种画蛇添足的作法,因为它破坏了Web API的无状态性。设计无状态的Web API不仅仅使Web API自身显得简单而精炼,还因减除了针对客户端的亲和度使我们可以有效地实施负载均衡,因为只有这样集群中的每一台服务器对于每个客户端才是等效的。
RESTful遇到的困境
RESTful的设计风格可以极大提高接口API的易用性,目前越来越多的web服务采用RESTful风格的API对外提供服务,例如:ElasticSearch、MongoDB、Docker API等等。作为一名后台开发工程师,我在工作中也是以RESTful风格指导自己设计对外提供的接口API,在此过程中也踩过一些坑。
一个特别棘手的问题是RESTful风格的API在框架层面不好做监控。通常情况下监控为了不侵入业务代码,框架层面记录每个HTTP请求的URI、方法、返回状态码等信息,然后上报至一个统一的监控中心处理分析数据。以查询雇员信息的API为例,GET /employees/c001
和GET /employees/c002
分别代表查询c001和c002的信息,业务层面上使用了同一份查询逻辑,但是监控数据却显示在两个不同的URI下。
目前我们的解决方案是在业务层面为每个API配置一条监控的URI,这样GET /employees/c001
和GET /employees/c002
这两种请求的监控都会现在查询雇员信息API下,这样可以更容易反应服务执行状态。但是导致的结果就是工作量的增加,本来在框架层面可以统一处理的逻辑却要侵入至各个业务逻辑。
参考资料
[1] 浅谈RESTful接口设计和开发 https://www.jianshu.com/p/d674b6153326
[2] 我所理解的RESTful Web API [设计篇] https://www.cnblogs.com/artech/p/restful-web-api-02.html