我所理解的RESTfulWebAPI[设计篇]
《》Web服务已经成为了异质系统之间的互联与集成的主要⼿段,在过去⼀段不短的时间⾥,Web服务⼏乎清⼀⽔地采⽤SOAP来构建。构建REST风格的Web服务是最近两三年风⾏的潮流,所以很多⼈以为REST是⼀个事物。⽽事实却是:REST⾃其诞⽣之⽇起到现在(2014年)已经有14年了,它为什么叫这么⼀个“奇怪”的名字呢?
⽬录
⼀、为什么叫这个“奇怪”的名字?⼆、采⽤URI标识资源
⼆、采⽤URI标识资源
三、使⽤“链接”关联相关的资源
四、使⽤统⼀的接⼝
五、使⽤标准的HTTP⽅法
六、⽀持多种资源表⽰⽅式
七、⽆状态性
⼀、为什么叫这个“奇怪”的名字?
2000年,Roy Thomas Fielding博⼠在他那篇著名的博⼠论⽂《Architectural Styles and the Design of Network-based Software Architectures》中提出了⼏种软件应⽤的架构风格,REST作为其中的⼀种架构风格在这篇论⽂的第5章中进⾏了概括性的介绍。我个⼈建议本书的读者都能读读这篇论⽂,原⽂和中⽂译⽂都可以从⽹络上到。REST是“REpresentational State Transfer”的缩写,可以翻译成“表现状态转换”,但是在绝⼤多数场合中我们只说REST或者RESTful。为什么会起这么⼀个奇怪的名字呢?
我们可以从上述这篇论⽂中到答案。Fielding在论⽂中将REST定位为“分布式超媒体应⽤(Distributed Hypermedia System)”的架构风格,它在⽂中提到⼀个名
为“HATEOAS(Hypermedia as the engine of application state)”的概念。
我们利⽤⼀个⾯向最终⽤户的Web应⽤来对这个概念进⾏简单阐述:这⾥所谓的应⽤状态(Application State)表⽰Web应⽤的客户端的状态,简单起见可以理解为会话状态。资源在浏览器中以超媒体的形式呈现,通过点击超媒体中的链接可以获取其它相关的资源或者对当前资源进⾏相应的处理,获取的资源或者针对资源处理的响应同样以超媒体的形式再次呈现在浏览器上。由此可见,超媒体成为了驱动客户端会话状态的转换的引擎。
借助于超媒体这种特殊的资源呈现⽅式,应⽤状态的转换体现为浏览器中呈现资源的转换。如果将超媒体进⼀步抽象成⼀般意义上的资源呈现(Representation )⽅式,那么应⽤状态变成了可被呈现的状态(REpresentational State)。应⽤状态之间的转换就成了可被呈现的状态装换(REpresentational State Transfer),这就是REST。
REST在我看来是⼀种很笼统的概念,它代表⼀种架构风格。对于多个Web应⽤采⽤的架构,我们只能说其中某⼀个⽐其它的更具有REST风格,⽽不能简单粗暴地说:“它采⽤了REST架构⽽其它的没有”。为了将REST真正地落地,Lenoard Rechardson & Sam Ruby在《RESTful Web Services》⼀书中提出了⼀种名为“⾯向资源的架构(ROA: Resource Oriented Architecture)”。该书中介绍了⼀些采⽤ROA架构的Web服务应该具备的基本特征,它们可以指导我们如果构架具体的RESTful Web API。⼆、采⽤URI标识资源
SOAP Web API采⽤RPC风格,它采⽤⾯向功能的架构,所以我们在设计SOAP Web API的时候⾸相考虑的是应⾼提供怎样的功能(或者操作)。RESTful Web API采⽤⾯向资源的架构,所以在设计之初⾸先需要考虑的是有哪些资源可供操作。
资源是⼀个很宽泛的概念,任何寄宿于Web可供操作的“事物”均可视为资源。资源可以体现为经过持久化处理保存到磁盘上的某个⽂件或者数据库中某个表的某条记录,也可以是Web应⽤接受到请求后采⽤某种算法计算得出的结果。资源可以体现为⼀个具体的物理对象,它也可以是⼀个抽象的流程。
⼀个资源必须具有⼀个或者多个标识,既然我们设计的Web API,那么很⾃然地应该采⽤URI来作为资源的标识。作为资源标识的URI最好具有“可读性”,因为具有可读性的URI更容易被使⽤,使⽤者⼀看就知道被标识的是何种资源,⽐如如下⼀些URI就具有很好的可读性。
www.artech/employees/c001(编号C001的员⼯)
www.artech/sales/2013/12/31(2013年12⽉31⽇的销售额)
www.artech/orders/2013/q4(2013年第4季度签订的订单)
除了必要的标志性和可选的可读性之外,标识资源的URI应该具有“可寻址性(Addressability)”。也就是说,URI不仅仅指明了被标识资源所在的位置,⽽且通过这个URI可以直接获取⽬标资源。通过前⾯的介绍我们知道URI具有URL和URN两种主要的表现形式,只要前者具有可寻址性,所以我们最好采⽤⼀个URL作为资源的标识。
URI除了可以标识某个独⽴的资源外(⽐如“www.artech/employees/c001”),还可以标识⼀组资源的集合或者资源的容器(⽐
如“www.artech/orders/2013/q4”)。当然,⼀组同类资源的集合或者存放⼀组同类资源的容器本⾝也可以视为另⼀种类型的复合型(Composite)资源,所以“URI 总是标识某个资源”这种说法是没
有问题的。
三、使⽤“链接”关联相关的资源
在绝⼤多数情况下,资源并不会孤⽴地存在,必然与其它资源具有某种关联。既然我们推荐资源采⽤具有可寻址性的URL来标识,那么我们就可以利⽤它来将相关的资源关联起来。⽐如我们采⽤XML来表⽰⼀部电影的信息,那么我们采⽤如下的形式利⽤URL将相关的资源(导演、领衔主演、主演、编剧以及海报)关联在⼀起。实际上这可以视为⼀份超⽂本/超媒体⽂档。当⽤户得到这样⼀份⽂档的时候,可以利⽤⾃⾝的内容获得某部影⽚基本的信息,还可以利⽤相关的“链接”得到其它相关内容的详细信息。
1:<movie>
2:<name>魔⿁代⾔⼈</name>
3:<genre>剧情|悬疑|惊悚</genre>
4:<directors>
5:<add ref="www.artech/directors/taylor-hackford">泰勒.海克福德</add>
6:</directors>
7:<starring>
8:<add ref = "www.artech/actors/al-pacino">阿尔.帕西诺</add>
9:<add ref = "www.artech/actors/keanu-reeves ">基诺.李维斯</add>
10:</starring>
11:<supportingActors>
12:<add ref = "www.artech/actors/charlize-theron ">查理兹.塞隆</add>
13:<add ref = "www.artech/actors/jeffrey-jones ">杰弗瑞.琼斯</add>
14:<add ref = "www.artech/actors/connie-nielsen">康尼.尼尔森</add>
15:</supportingActors>
16:<scriptWriters>
17:<add ref = "www.artech/scriptwriters/jonathan-lemkin">乔纳森•莱姆⾦</add>
19:<add ref = "www.artech/scriptwriters/tony-gilroy">托尼•吉尔罗伊</add>
20:</scriptWriters>
21:<language>英语</language>
22:<poster ref = "www.artech/images/the-devil-s-advocate"/>
23:<story>...</story>
24:</movie>
Fielding在他的论⽂中将REST定位为“分布式超媒体应⽤”的架构风格,⽽超媒体的核⼼就是利⽤“链接”相关的信息结成⼀个⾮线性的⽹,所以从⼀点也可以看出REST和“使⽤链接关联相关的资源”这个特性使吻合的。
四、使⽤统⼀的接⼝
由于REST是⾯向资源的,所以⼀个Web API旨在实现针对单⼀资源的操作。我们在前⾯已经说个,针对资源的基本操作唯CRUD⽽已,这是使我们可以为Web API定义标准接⼝成可能。所谓的标准接⼝就是针对不同资源的Web API定义⼀致性的操作来操作它们,其接⼝可以采⽤类似于下⾯的模式。
1:public class ResourceService
2: {
3:public IEnumerable<Resource>[] Get();
4:public void Create(Resource resource);
5:public void Update(Resource resource);
6:public void Delete(string id);
7: }
能否采⽤统⼀接⼝是RESTful Web API和采⽤RPC风格的SOAP Web服务⼜⼀区别。如果采⽤RPC风格的话,我们在设计Web API的时候⾸先考虑的是具体哪些功能需要被提供,所以这样的Web API是⼀组相关功能的集合⽽已。
以⼀个具体的场景为例。现在我们需要设计⼀个Web API来管理⽤于授权的⾓⾊,它只需要提供针对⾓⾊本⾝的CRUD的功能以及建⽴/解除与⽤户名之间的映射关系。如果我们将其定义成针对SOAP的Web服务,其服务接⼝具有类似于如下的结构。
1:public class RoleService
2: {
3:public IEnumerable<string> GetAllRoles();
4:public void CreateRole(string roleName);
5:public void DeleteRole(string roleName);
6:
7:public void AddRolesInUser(string userName, string[] roleNames);
8:public void RemoveRolesFromUser(string userName, string[] roleNames);
9: }
如下我们需要将其定义成⼀个纯粹的RESTful的Web API,只有前⾯三个⽅法在针对⾓⾊的CRUD操作范畴之内,但是后⾯两个⽅法却可以视为针对“⾓⾊委派(Role Assignment)”对象的添加和删除操作。所以这⾥实际上涉及到了两种资源,即⾓⾊和⾓⾊委派。为了使Web API具有统⼀的接⼝,我们需要定
restful接口详解
义如下两个Web API。
1:public class RolesService
2: {
3:public IEnumerable<string> Get();
4:public void Create(string roleName);
5:public void Delete(string roleName);
6: }
7:
8:public class RoleAssignmentsService
9: {
10:public void Create(RoleAssignment roleName);
11:public void Delete(RoleAssignment roleName);
五、使⽤标准的HTTP⽅法
由于RESTful Web API采⽤了同⼀的接⼝,所以其成员体现为针对同⼀资源的操作。对于Web来说,针对资源的操作通过HTTP⽅法来体现。我们应该将两者统⼀起来,是Web API分别针对CRUD的操作只能接受具有对应HTTP⽅法的请求。
我们甚⾄可以直接使⽤HTTP⽅法名作为Web API接⼝的⽅法名称,那么这样的Web API接⼝就具有类似于如下的定义。对于ASP.NET Web API来说,由于它提供了Action ⽅法名称和HTTP⽅法的⾃动映射,所以如果我们采⽤这样的命名规则,就⽆需再为具体的Action⽅法设定针对HTTP⽅法的约束了。
1:public class ResourceService
2: {
3:public IEnumerable<Resource>[] Get();
4:public void Post(Resource resource);
5:public void Put(Resource resource);
6:public void Patch (Resource resource);
7:public void Delete(string id);
8:
9:public void Head(string id);
10:public void Options();
11: }
上⾯代码⽚断提供的7个⽅法涉及到了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也是不⼀样的。
1: PUT www.artech/employees/300357 HTTP/1.1
2: ...
3:
4: <employee>
5:<id>300357</id>
6:  <name>张三</name>
7:  <gender>男<gender>
8:  <birthdate>1981-08-24</birthdate>
9:  <department>3041</department>
10: </employee>
1: POST www.artech/employees HTTP/1.1
2: ...
3:
4: <employee>
5:  <name>张三</name>
6:  <gender>男<gender>
7:  <birthdate>1981-08-24</birthdate>
8:  <department>3041</department>
9: </employee>
POST和PUT请求⼀般将所加资源的内容置于请求的主体。但是对于PUT请求来说,如果添加资源的内容完全可以由其URI来提供,这样的请求可以不需要主体。⽐如我们通过请求添加⼀个⽤于控制权限的⾓⾊,标识添加⾓⾊的URI由其⾓⾊名称来决定,并且不需要指定除⾓⾊名称的其它信息,那么我们只要发送如下⼀个不含主体的PUT请
1: PUT www.artech/roles/admin HTTP/1.1
2:
3: ...
除了进⾏资源的添加,PUT请求还能⽤于资源的修改。由于请求包含提交资源的标识(可以放在URI中,也可以置于保存在主体部分的资源内容中),所以服务端能够定位到对应的资源予以修改。对于POST和PUT,也存在⼀种⼀⼑切的说法:POST⽤于添加,PUT⽤于修改。我个⼈⽐较认可的是:如果PUT提供的资源不存在,则做添加操作,否则做修改。
对于发送PUT请求以修改某个存在的资源,服务器⼀般会将提供资源将原有资源整体“覆盖”掉。如果需要进⾏“局部”修改,我们推荐请求采⽤PATCH⽅法,因为从语义上
讲“Patch”就是打补丁的意思。
安全性与幂等性
关于HTTP请求采⽤的这些个⽅法,具有两个基本的特性,即“安全性”和“幂等性”。对于上述7种HTTP⽅法,GET、HEAD和OPTIONS均被认为是安全的⽅法,因为它们旨在实现对数据的获取,并不具有“边界效应(Side Effect)”。⾄于其它4个HTTP⽅法,由于它们会导致服务端资源的变化,所以被认为是不安全的⽅法。
幂等性(Idempotent)是⼀个数学上的概念,在这⾥表⽰发送⼀次和多次请求引起的边界效应是⼀致的。在⽹速不够快的情况下,客户端发送⼀个请求后不能⽴即得到响应,由于不能确定是否请求是否被成功提交,所以它有可能会再次发送另⼀个相同的请求,幂等性决定了第⼆个请求是否有效。
上述3种安全的HTTP⽅法(GET、HEAD和OPTIONS)均是幂等⽅法。由于DELETE和PATCH请求操作的是现有的某个资源,所以它们是幂等⽅法。对于PUT请求,只有在对应资源不存在的情况下服务器才会进⾏添加操作,否则只作修改操作,所以它也是幂等⽅法。⾄于最后⼀种POST,由于它总是进⾏添加操作,如果服务器接收到两次相同的POST操作,将导致两个相同的资源被创建,所以这是⼀个⾮幂等的⽅法。
当我们在设计Web API的时候,应该尽量根据请求HTTP⽅法的幂等型来决定处理的逻辑。由于PUT是⼀个幂等⽅法,所以携带相同资源的PUT请求不应该引起资源的状态变化,如果我们在资源上附加⼀个⾃增长的计数器表⽰被修改的次数,这实际上就破坏了幂等型。
不过就我个⼈的观点来说,在有的场合下针对幂等型要求可以不需要那么严格。举个例⼦,我对于我们开发的发部分应⽤来说,数据表基本上都有⼀个名
为LastUpdatedTime的字段表⽰记录最后⼀次被修改的时间,因为这是为了数据安全审核(Auditing)的需要。在这种情况下,如果接收到⼀个基于数据修改的PUT请求,我们总是会⽤提交数据去覆盖现有
的数据,并将当前服务端时间(客户端时间不可靠)作为字段LastUpdatedTime的值,这实际上也破坏了幂等性。
可能有⼈说我们可以在真正修改数据之前检查提交的数据是否与现有数据⼀致,但是在涉及多个表链接的时候这个“预检”操作会带来性能损失,⽽且针对每个字段的逐⼀⽐较也是⼀个很繁琐的事情,所以我们⼀般不作这样的预检操作。
六、⽀持多种资源表⽰⽅式
资源和资源的表⽰(Representaion)是两个不同的概念,资源本⾝是⼀个抽象的概念,是看不见摸不着的,⽽看得见摸得着的是资源的表现。⽐如⼀个表⽰⼀个财年销售情况的资源,它既可以表⽰为⼀个列表、⼀个表格或者是⼀个图表。如果采⽤图表,⼜可以使⽤柱状图、K线图和饼图等,这⼀切都是针对同⼀个资源的不同表⽰。
我们说“调⽤Web API获取资源”,这句话其实是不正确的,因为我们获取的不是资源本⾝,仅仅是资源的某⼀种表⽰⽽已。对于Web来说,⽬前具有两种主流的数据结
构,XML和JSON,它们也是资源的两种主要的呈现⽅式。在多语⾔环境下,还应该考虑描述资源采⽤的语⾔。
我们在设计Web API的时候,应该⽀持不同的资源表⽰,我们不能假定请求提供的资源⼀定表⽰成XML,也不能总是以JSON格式返回获取的资源,正确的做法是:根据请求携带的信息识别提交和希望返回的资源表⽰。对于请求提交的资源,我们⼀般利⽤请求的Content-Type报头携带的媒体类型来判断其采⽤的表⽰类型。对于响应资源表⽰类型的识别,可以采⽤如下两种⽅式。
让请求URI包含资源表⽰类型,这种⽅式使⽤的最多的是针对多语⾔的资源,我们⼀般讲表⽰语⾔(也可以包含地区)的代码作为URI的⼀部分,⽐
如“www.artech/en/orders/2013”表⽰将2013年的订单以英⽂的形式返回。
采⽤“内容协商(Content Negotiation)”根据请求相关报头来判断它所希望的资源表⽰类型,⽐如“Accept”和“Accept-language”报头可以体现请求可以接受的响应媒体类型和语⾔。
对于上述两种资源表⽰识别机制,我们很多⼈会喜欢后者,因为第⼀种不够“智能”。实际上前者具有⼀个后者不具有的特性:“浏览器兼容型”。对于Web API开发来说,浏览器应该成为⼀种最为常⽤的测试⼯具。在不借助任何插件的情况下,我们利⽤浏览器访问我们在地址栏中输⼊的URI时对⽣成的请求内容不能作任何⼲预的,如果与资源表⽰相关的信息(⽐如语⾔、媒体类型)被直接包含到请求的URI中,那么所有的情况都可以利⽤浏览器直接测试。
有⼈从另⼀⽅⾯对“URI携带资源表⽰类型”作了这样的质疑:由于URI是资源的标识,那么这导致了相同的资源具有多个标识。其实这是没有问题的,URI是资源的唯⼀标识,但不是其“唯⼀的唯⼀标识“,相同的资源可以具有多个标识。
七、⽆状态性
RESTful只要维护资源的状态,⽽不需要维护客户端的状态。对于它来说,每次请求都是全新的,它只需要针对本次请求作相应的操作,不需要将本次请求的相关信息记录下来以便⽤于后续来⾃相同客户端请求的处理。
对于上⾯我们介绍的RESTful的这些个特性,它们都是要求我们为了满⾜这些特征做点什么,唯有这个⽆状态却是要求我们不要做什么,因为HTTP本⾝就是⽆状态的。举个例⼦,⼀个⽹页通过调⽤Web API分页获取符合查询条件的记录。⼀般情况下,页⾯导航均具有“上⼀页”和“下⼀页”链接⽤于呈现当前页的前⼀页和后⼀页的记录。那么现在有两种实现⽅式返回上下页的记录。
Web API不仅仅会定义根据具体页码的数据查询定义相关的操作,还会针对“上⼀页”和“下⼀页”这样的请求定义单独的操作。它⾃⾝会根据客户端的Session ID对每次数据返回的页⾯在本地进⾏保存,以便能够知道上⼀页和下⼀页具体是哪⼀页。
Web API只会定义根据具体页码的数据查询定义相关的操作,当前返回数据的页码由客户端来维护。
第⼀种貌似很“智能”,其实就是⼀种画蛇添⾜的作法,因为它破坏了Web API的⽆状态性。设计⽆状态的Web API不仅仅使Web API⾃⾝显得简单⽽精炼,还因减除了针对客户端的“亲和度(Affinty)”使我们可以有效地实施负载均衡,因为只有这样集中的每⼀台服务器对于每个客户端才是等效的。
⼤部分计算机书籍都将Side Effect翻译成“副作⽤”,⽽我们⼀般将“副(负)作⽤”理解为负⾯的作⽤,其实计算机领域Side Effect表⽰的作⽤⽆所谓正负,所以我们觉得还是还原其字⾯的含义“边界效⽤”。除此之外,对于GET、HEAD和OPTIONS请求来说,如果服务端需要对它们作⽇志、缓存甚⾄计数操作,严格来说这也算是⼀种Side Effect,但是请求的发送者不对此负责。
这⾥的“兼容”不是指⽀持由浏览器发送的请求,因为通过执⾏JavaScript脚本可以让作为宿主的浏览器发送任何我们希望的请求,这⾥的兼容体现在尽可能地⽀持浏览器访问我们在地址栏中输⼊的URI默认发送的HTTP-GET请求。
参考资料:
[1] 《HTTP: The Definitive Guide》, By By David Gourley, Brian Totty, Marjorie Sayer, Anshu Aggarwal, Sailu Reddy
[2] 《RESTful Web Services》, RESTful Web Services
[3] 《A Brief Introduction to REST》,
[4] 《TCP/IP Illustrated (Volumn 1: The Protocol)》, by W. Richard Stevens

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。