Restful Objects简介
作者 Dan Haywood, Richard Pawson 译者 蔡坚安 发布于 2012年8月21日
Restful Objects和其他RESTful标准有什么区别呢——比如说和Java平台的JAX-RS (JSR311)相比较或者和.NET平台的微软Web API相比较?答案是其他RESTful框架主要用来抽象纯粹的网络问题,它们并不保证所定义的和各种领域对象类型交互的资源结构的统一性。并且它们没有或者很少支持RESTful体系中被认为最重要的原则:“将超文本作为应用程序状态的引擎”,或者说HATEOAS。平白地说这意味着访问系统的所有功能是可行的,只要跟随着来自资源主站的超媒体控制(链接)就行了。和Restful Objects意图最相近的框架或许就是OData。但两者间还是存在很大差别的,Odata主要专注于CRUD(Create, Read, Update, Delete)功能,而Restful Objects提供了访问对象所有行为(方法)的能力。
实现了Restful Objects规范的新框架不仅使得在领域对象模型上编写符合HATEOAS(超媒体即应用状态引擎)标准的API更加容易——事实上它们消除了这些工作量!你可以用简单的词法编写领域对象模型——按传统的Java或C#对象编写——然后在几分钟之内在其上创建完整的RESTful API。
另外,因为这些框架提供的资源和表述(representations)符合公共规范,这意味着所编写的客户端如果能和运行其中一种框架的服务器交互,那么就可以和运行另一种框架的服务器交互。我们希望在接下来的几个月里能看到更多符合Restful Objects规范的服务端实现(我们已经知道至少有一个第三方已计划这么做)。同时,我们也已看到使用Restful Objects API的标准客户端框架逐渐涌现。这些我们后面再谈。
可应用性和优势
Restful Objects规范最有可能的受众是采用领域驱动的开发者,已经实现或者正在实现领域对象模型的开发者,以及那些想在该模型上提供RESTful API之类特性的人员。我们自己的领域驱动开发(DDD)经验包括了非常大范围的领域对象模型设计 [1, 4, 5],包括了被封装为领域实体上的方法的大多数行为。我们希望能使用Restful Objects来更深入地挖掘领域模型资产。
除了能巨大地减少工作量,使用实现了Restful Objects规范的框架还有另外几项优势:
- 测试。所有业务逻辑都只在领域类型中表示,因此可以在内存中快速而经济地将它们进行单元测试。相对的,如果RESTful API是根据特定用例自定义编写的,则只有通过跨服务器的(缓慢的)集成测试才能有效地将其进行验证。
- 文档。Restful Objects是由领域对象模型所驱动的,因此可以使用已有的成熟技术(例如UML或者简单的Javadoc/Sandcastle)将其文档化。这比创建一些新的标记以显示地文档化手工编写的RESTful API更加可取。
在继续深入之前,我们想说Restful Objects是和领域对象模型的语法不相关的。所以,不管你是认为领域对象应该表示具有业务逻辑的实体,还是认为应该有单独的“资源模型”以表示用例实体,在这两种情况下Restful Objects规范都同样适用。为了简单明了,在本文中我们会优先使用和前者(领域对象作为实体)相关的例子,但在本文的后面我们还会继续讨论这个主题。
资源
在Restful Objects规范中每个领域对象就是一项资源;例如下面的URI指向Id为31的客户:
~/objects/customers/31
其中 ~ 是服务器地址(比如http://myserver.com
),customers
定义了对象的类型,而31
则是该类型的“实体标识”。该规范并没有定义标识的格式,任意的唯一字符串都是可以的。为什么需要/objects
呢?这是为了和其他顶级资源区分开了,包括services
(例如repositories和factories之类具有方法但没有属性的单件对象)以及domain-types
(类型元数据);这些顶级资源都具有各自的子资源结构。
每个对象资源都具有一些子资源以表示该对象的成员。所以:
~/objects/customers/31/properties/dateOfBirth
指向一个特定客户的生日,而:
~/objects/customers/31/collections/orders
指向和该客户相关联的订单的集合。下面的表格显示了和各种HTTP方法相对应的对象、属性以及集合资源。如果不能赋予某个HTTP方法有效的意义,则指定为405错误。
对象 | 对象 | 对象 | |
GET | 对象概述 | 属性详细及值 | 集合详细及内容 |
PUT | 更新或清除多个属性值 | 更新或清除值 | 添加对象(如果集合具有Set语义) |
DELETE | 删除对象 | 清除值 | 从集合中移除对象 |
POST | n/a - 405 | n/a - 405 | 添加对象(如果集合具有List语义) |
以上的分析并非什么创新的东西——很多预定的RESTful API都具有类似的特性。但Restful Objects规范也算开辟了一片新天地,在对象的“action”上应用了同样的方式——“action”是指可以通过RESTful接口访问的对象上的方法。扩展些说,在其他设计(例如[2])中这些问题已有解决方案,通常是将对action的调用映射到HTTP POST上。我们的目标使我们将提供对象的action信息(这需要参数集合)和实际调用action区分开来。因此,URL:
~/objects/customers/31/actions/lastOrder
描述了获取Id为31的客户的最后一条订单的action,而:
~/objects/customers/31/actions/lastOrder/invoke
描述了调用该action的资源。
同时,我们意识到,要求所有action都通过POST进行调用并不是使用HTTP的正确方式:比如幂等action或者仅仅用于获取其他对象的查询action(有时被描述为“没有副作用的(side-effect free)”)还要POST吗?下面的表格描述了我们如何应对这些问题:
对象Action资源 | 对象Action调用资源 | |
GET | action的详细信息(比如调用的参数和HTTP方法)。 | 调用——如果action被认为是只读的(没有副作用) |
PUT | n/a - 405 | 调用——如果action被认为是幂等的,但不是只读的。 |
DELETE | n/a - 405 | n/a - 405 |
POST | n/a - 405 | 调用被认为是非幂等且非只读的action |
在一些领域模型中,大多数行为(业务逻辑)是通过服务的形式实现的,其中领域实体仅仅是作为数据结构在服务的action上双向传递。Restful Objects规范也适用于这种模式;唯一的小区别是URL被用来标识服务实体而不是领域对象实体。与此同时,该规范也适用于在“行为复杂(behaviourally-rich)”的领域对象上以方法实现大多数行为的模式;这种模式下,服务仅仅是作为提供对象访问(查找已存在的对象或者是创建新对象)的次要角色。
表述(Representation)
每个资源返回一个表述;Restful Objects规范将这些表述定义为JSON(JavaScript Object Notation)格式。对于使用PUT或者POST方法访问的资源,在请求体中也必须提供表述,以描述要怎样修改资源。
规范定义了若干个主要表述:
object
(代表任意领域对象或服务)list
(访问其他对象的链接)property
collection
action
action result
(一般包含一个object
或者list
,或者只是反馈信息)
同时,规范还描述了几个次要表述,比如home
和user
等。每个表述都具有一个正式媒体类型,其包含在HTTPContent-Type
头部中。对于领域对象而言,其具有以下格式:
application/json;profile="urn:org.restfulobjects:repr-types/object"; x-ro-domain-type="http://~/domain-types/customer"
正式地,这是具有两个可选参数profile
和x-ro-domain-type
的application/json
类型。这两个参数为客户端提供了额外语义,可以想象成将其中一个层叠在另一个之上。在最底层,application/json
仅仅告诉客户端负载为JSON格式。对于某些客户端而言,比如浏览器的开发者插件,这就是所需的所有信息了。在上面一层,profile
参数指明该表述是领域对象形式的。最后,x-ro-domain-type
参数指明对象的类型:在本例中为Customer
。客户端可以使用这个参数来自定义用户界面,或者校验返回的表述是否为所期望的。另外要注意的是,该规范使用.x-ro-
前缀以便在没有现行标准或者草案标准可以利用时将命名空间冲突最小化。这种格式必然产生了一些自定义参数和查询字符串;然而Restful Objects没有专门自定义HTTP头部。
链接
与HATEOAS原则一致,每个表述包含了到其他资源的链接,而每个链接具有rel
参数以定义关系的性质。该规范重用了几个IANA定义的rel
值(例如:self、up、describedby
),另外还加上了几个自定义的rel值。请看下面的例子:
urn:org.RESTfulobjects:rels/details;action="placeOrder"
定义了一个在对象表述中指向该对象的action资源的链接。前缀:
urn:org.restfulobjects:rels/details
对一般客户端而言用其已够用了,而额外的参数action——在本例中其指明了该链接指向placeOrder
action——也许对定制的客户端有用。
综上所述,下面的表格描述了通过资源集合完成用户目标的典型流程。
描述 | 方法 | URL | 主体 | 返回的 |
跳到主页 | GET |
| - | 主页 |
链接到提供的服务列表 | GET |
| - | 列表(服务的链接集合) |
链接到产品库存服务 | GET |
| - | 服务 |
链接到‘Find By Name’ action | GET |
| - | action(在界面上以对话框呈现) |
调用(只读)action并传递参数"cycle" | GET |
| - | action结果,包含匹配Product对象的链接列表 |
链接到集合中的一个Product对象 | GET |
| - | 对象(代表一个产品) |
调用对象上的(不带参数)‘AddToBasket’action | POST |
| - | - |
调用BasketService上的‘ViewBasket...’action | GET |
| - | action结果:包含指向Item对象的链接的列表 |
修改刚新增的Item上的Quantity属性 | PUT |
| 属性名及其值3 | - |
从购物篮中删除(之前添加的)一个Item | DELETE |
| - | - |
服务端实现
前面我们提到两个实现了Restful Objects规范的独立开源框架。其中,Restful Objects for .NET完整实现了规范,但其目前还处于beta版本,因为它使用了Microsoft Web API框架(ASP.NET MVC4的一部分——在写本文时MVC4正处于‘RC’阶段)。第二个框架Restful Objects for Isis运行于Java平台之上;该框架已经可以使用,但其目前仅实现了Restful Objects规范的前期草案,在正式发布前还需要继续开发和测试。我们一直在积极地参与这两个框架的开发。
使用这两个框架,你能够根据领域对象模型分别编写POCOs和POJOs代码,然后创建完整的符合Restful Objects规范的RESTful API,而不用编写任何其他深入的代码。这个录制的在线视频(使用.NET框架)演示了上述工作如何在仅仅几分钟内就可以完成。
这是可能的,因为这两个框架都是建立在实现了naked objects模式——根据领域对象模型利用反射自动创建面向对象的用户界面,并(默认)提供用户活动的公共方法——的现行框架之上的。新的Restful Objects框架以相似的方式反射领域对象模型,但以RESTful API的形式呈现对象的功能,而不是以用户界面的形式。两个新的框架都将反射、对象持久以及其他横向关注点(cross-cutting concerns)的职责委托给了已有框架(分别为Naked Objects for .NET以及Apache Isis)。
上述的新框架能够识别一些简单的领域对象代码规范以及标示法(在.NET中为‘attributes’)。例如:对象上的任何公有方法都默认会在Restful Objects API中以action提供出来,但允许通过将方法标示为Hidden
以重写。如果某个对象定义了公有方法foo([params])
和另一个公有方法validateFoo([params])
,则后者会被认为是用来在前者执行前为传递给前者的参数提供验证逻辑的。
这两个框架还提供了细粒度的基于用户身份和/或角色的授权机制。对于给定的领域类型,如果用户没有被授权查看某个给定的属性、集合或者活动(action),则在相应的表述中指向该对象成员的链接就永远不会呈现给该客户。当用户试图通过直接构造指向该资源的URL以进行访问时,他们将接收到404错误;而如果用户拥有查看该对象成员的权限,但没有编辑的权限,则当试图进行编辑时就会接收到403错误。
Restful Objects for .NET框架的源代码可以从Codeplex网站下载或者以NuGet的形式安装;Restful Objects for Isis框架的源代码可以以源代码的形式下载或者使用Maven原型安装。
对于已经使用过Naked Objects for .NET或者Naked Objects for Apache Isis的开发者,使用新的框架意味着他们能够在几分钟之内(形象地说)在他们已有的领域模型上创建RESTful API。但我们的意图是将新框架推荐给没有naked objects模式知识同时也对其不感兴趣的开发者,事实上我们已经看到这些开发者逐渐感兴趣了。
客户端
目前我们大部分开发工作都聚焦在服务器端——根据领域对象模型产生Restful Objects API的相关代码。不过与此同时,我们也在使用这些API的客户端应用程序上做了一些工作。
在其中一个例子中,客户端是一个规范的web应用程序(使用ASP.NET MVC编写),该应用中的控制器方法调用了在另一台服务器上实现的Restful Objects API。我们希望在将来开发一个小的客户端应用程序库,用来调用Restful Objects服务端以及将返回的JSON表述转换为能够由C#或者Java操纵的对象。
在另外一个例子中,我们编写的客户端是一个‘单页面应用程序’,其只由一个页面组成,仅包含了若干行静态HTML代码,主要由使用了JQuery的众多JavaScript函数支撑。JavaScript负责调用服务器上的Restful Objects API,然后将返回的结果以HTML在浏览器中呈现。再次,我们希望在将来能编写一个小的JavaScript应用程序库,专门用来消费(consuming)Restful Objects API,计划该库主要使用JQuery编写——也或许由其他者接棒。
Restful Objects规范的一般性产生了另一个可能性:存在能在不必修改的情况下通过RESTful API和任意领域模型交互的通用客户端。我们已获知有三个不同的开源通用客户端正处于开发当中,它们在Restful Objects网站上被罗列出来了。它们都是单页面应用程序,使用已有的程序库和JavaScript编写。这些通用客户端中的JavaScript代码也能被作为创建一个或多个自定义客户端的基础。这三个通用客户端在用户交互风格上差别很大。下面展示了其中之一的截图——由Adam Howard开发的Restful Objects工作空间(AROW):
(点击图片以放大浏览)
以牙还牙
通过Restful API暴露领域实体的想法受到了很多争议。有些评论者认为这是一个糟糕的想法,认为它产生了安全漏洞;其他一些人甚至认为不可能通过该方式创建真正的Restful API。现在让我们来详细审视下这些争论。
Rickard Öberg声称将领域实体作为资源暴露出来“不可能是HATEOAS的,因为不存在在资源之间创建链接以暴露应用程序状态的合理方式”[3]。其实显然不是这样的:Restful Objects将实体作为资源暴露出来是完全HATEOAS的,同时对链接来说也是相当合理的。
Jim Webber声称即使这是可能实现的,也还是一个糟糕的创意,因为这导致了客户端和服务端的紧耦合,然而在Restful体系中,客户端和服务端是应该能够独立改进的[6]。他以及其他人争论说应该只暴露视图模型和/或表示用例的对象,这两者都应该是版本化的,它们能使客户端不受领域模型变化的影响。
我们认为这些论点即使不是完全错误的,也是不分清红皂白的。我们认为真实的情况和他们所说的有很多微妙的差别:在一些场景下这些论点是有效的,但在大多数场景下则并不是。我们需要考虑到以下两个因素:
第一个因素是:客户端和服务端是否都在同一方的控制之下。对于可公共访问的Restful API——比如Twitter或者Amazon的Restful API——也就意味着服务端和客户端分别属于不同的独立组织,在这种情况下我们同意暴露领域实体不是好的方式。但是,在这种情况下,Restful Objects也能够完美地跟视图模型和/或用例对象协同,这些模式在规范中有详细的描述。
反过来,事实上在很多潜在的Restful API用例中,客户端和服务端的变化发展是由同一方控制的——比如主要在内部使用的企业应用程序。在这些情况下,暴露领域实体不仅安全,而且还是个好创意。这些属于‘主权’系统(此概念由Alan Cooper提出)一类的应用程序,相对于面向公众的(或者‘短暂交互的’)应用来说,特别需要赋予用户访问更大范围数据和功能的权利。
第二个因素是:客户端是‘定制的’(专门用于和特定的领域模型协同工作)还是‘通用的’(能在不必修改的前提下和任意领域模型协同工作)。目前,不管是在内部网络中还是在开放的因特网中,几乎所有消费Restful API的客户端都是定制的——所以我们需要将它们同领域模型的变化隔离开来。但如果存在能够自动响应领域模型变化的通用客户端,则不需要这样做了。目前,很少有人关注通用客户端的可能性,因为没有综合的标准来指导怎样创建这样的应用程序。我们认为,事实上通用客户端的概念相比定制客户端的概念更加符合REST的精神(以及REST的语义)。要知道,网页浏览器就是在一定层级上访问restful接口的通用客户端,其工作于文档层级之上。而Restful Objects使得工作于更高一层抽象(即领域对象模型)的通用客户端的概念成为可能。
把这两个因素描绘成2x2的分析表,如下面所示,你可以看到如果你需要应对内部网络应用程序和/或通用客户端,则暴露通过RESTful API领域实体是安全和有效的。只有当你需要应对公开的因特网应用程序且是定制的客户端时才需要关心以下建议:领域实体应该用视图模型和/或用例对象掩藏起来,以将客户端和服务端的变化隔离开来。
部署环境: | 内部网 | 因特网 |
通用 | 可以暴露领域实体 | 可以暴露领域实体 |
定制 | 可以暴露领域实体 | 只能暴露版本化的视图模型和/或用例对象 |
再强调一遍:Restful Objects在该表格中的任意位置都同样适用。我们认为,目前右下角受到最多关注的事实更多地反映了构建定制的Restful API的复杂性,而不是反映了固有的局限性。另一个常见的谬论——认为Restful API只能在小的严格定义的状态转换表之上创建——进一步证明了很多人的思维狭隘地集中在右下角的单元格中。
Öberg进一步争论,因为授权问题,以RESTful API暴露领域实体的方式不适用于公共应用程序,并举例:访问重置密码的action可以由角色管控,但修改密码的action必须限定在某个具体的用户。以上面讨论的两个Restful Objects框架实现来说,这个特例对代码的影响事实上是微不足道的。我们认为能阐明他的论点的更好的例子是:访问某个特定的实体对象,而不是某个方法。这是面向公众的系统的常见需求:比如用户必须能够访问他们自己的购物篮,但不能通过猜测URL访问任意其他人的购物篮。
虽然目前上述的两个Restful Objects实现本身都没有支持基于实体的授权,但该问题存在完全可行的解决方案。在前面我们已经谈到,Restful Objects规范中没有指定URL中实体标识符的形式。另外,URL中实体标识符还可以被服务端加密,比如使用会话产生的私钥。调用服务的action资源以返回我的购物篮,可以使用一个对象表述,其“自身”加密的链接是:
~/objects/baskets/xJDgTljGjyAAOmvBzIci9g
接着,该购物篮上的Total属性对应的URL是:
~/objects/baskets/xJDgTljGjyAAOmvBzIci9g/properties/Total
所以,在这种情况下仍然可以直接了当地解析资源标识符,但要直接获取另一个购物篮就很可能不行了。这显然不够美观,但值得一提的是REST不是用来创建‘美观’的链接的。就像Tim Berners-Lee所强调的,除了服务器地址之外,应该以不透明的形式对待URL;"rel"的值才是关键。这是HATEOAS的其中之一点。
在本文所论述的两个Restful Objects框架中,我们计划让用session-key加密的实体标识符成为一个可配置项。
结论
目前,在大型企业系统上构建RESTful API是一项非常昂贵的活动,从术语角度说,这需要在设计、开发、测试和文档上花费大量资源。而使用实现了Restful Objects规范的框架意味着所有工作会变得微不足道,这让开发者能够将精力集中在最需要的地方——领域对象模型。
另外,Restful Objects规范所激励的资源和表述的标准化为客户端应用程序库开启了巨大机会,这样的程序库可以和任意的Restful Objects服务协同工作,若是完全通用的客户端,则可以和任意符合标准的应用交互。
我们希望读者能深究Restful Objects规范以及相关框架实现,同时,如果有人有兴致于编写新的符合Restful Objects规范的开源框架或程序库,我们将乐于倾听他们的声音。