或我如何学会不再担心和爱HATEOAS
REST已成为实现Web服务的事实上的解决方案,至少已成为一种流行的解决方案。 这是可以理解的,因为REST在使用HTTP规范时提供了一定程度的自我文档。 它经久耐用,可扩展,并提供了其他一些理想的特性。
但是,许多所谓的RESTful服务都没有实现HATEOAS (作为应用程序状态引擎的超媒体),这会使Roy Fielding晚上 忙 起来 (如果您认为介绍不好,请阅读评论部分 )。 这是一个不幸的趋势,因为包括超媒体控件提供了很多优势,尤其是在客户端与服务器解耦方面。
本文是一个分为两部分的系列文章的第一篇,将介绍控制REST的底层实现细节和设计问题。 我们将讨论在您的RESTful服务中实现HATEOAS值得付出额外的努力,因为您的服务面临着不断变化的业务需求。
第二部分将于3月28日发布,将是一个使用Spring-HATEOAS实现HATEOAS服务的实时代码示例。 在我即将于2016年3月2日星期三在堪萨斯城Spring用户组的主题为“ 我如何学会停止照顾并开始爱上HATEOAS ”的演讲中,您还可以看到其中的一些概念。
REST,成功实现建筑约束的成功故事
作为开发人员,我不得不经常沮丧地学习,以在高位建筑师对我施加的约束下工作。 自从最近向架构师过渡以来,我现在可以定义自己的约束并尽自己的一份力量继续苦难的循环。 但是,在研究本文时,我了解了REST体系结构中经过深思熟虑的约束是如何使其成为Web服务世界的先驱。 苦难的循环至少在这次减少了。
Roy Fielding 在2000年的博士论文中定义了控制REST的六种主要建筑风格约束。 我将详细介绍其中的五个。 第六,按需编码,这是可选的,将不涉及。 五个幸运的样式约束是:客户端-服务器,无状态,可缓存,统一接口和分层体系结构。
1.客户端-服务器
第一种样式约束是客户端-服务器分离。 具有讽刺意味的是,这是当开发人员选择不实施HATEOAS时受到最大影响的约束。
关注点分离是好的系统设计的基本原则之一。 在REST和Web服务的上下文中,这种关注点分离在可伸缩性方面具有一些好处,因为RESTful服务的新实例也不需要处理客户端的拆包。
真正的好处,就像在任何时候都实现关注点分离约束一样,尽管允许独立发展。 客户端处理演示,服务器处理存储。 这种分离意味着对服务器的每次更改都不需要对客户端进行更改(并且不需要协调两者之间的发布),反之亦然。
在本文的后面,我们将更详细地介绍如何不实施HATEOAS来模糊客户端和服务器之间的界限。
2.无状态
如果您要问开发人员RESTful服务的关键特征是什么,您可能会首先收到的答复是它是无状态的。 这是一种流行的响应,因为无状态在REST最令人期望的两个特性中起着核心作用:持久性和可伸缩性。
在这种情况下,无状态意味着每个请求都包含服务器接受或拒绝请求所需的所有信息,并且服务器不需要检查会话状态即可确定请求的有效性。 这将导致持久性,因为客户端不再绑定到特定的服务实例。 如果客户端正在与实例“ A”进行对话并且故障,负载平衡器可以将客户端重定向到另一个可用实例,没有人是明智的。
另一个好处是可伸缩性,因为服务器资源不会因存储用户状态而消耗(如果服务足够流行,则可能会消耗大量资源)。 它还可以响应于流量激增,加快附加服务实例的旋转速度。 也就是说,要实现该功能需要高度的DevOps成熟度。
3.可缓存
第三个样式约束是请求可以是可缓存的。 在这种情况下,可缓存性是指客户端缓存请求的能力。 这与像Redis这样的服务器托管的缓存相反,尽管这是在以后的约束中启用的。 缓存客户端请求是每个主流浏览器中都已实现的功能,可通过使用http标头来激活它,如下图所示(缓存控制)。
客户端缓存请求的好处是不需要服务器重新提供对未更改且经常访问的资源的响应,从而减少了服务器负载。 同样,由于浏览器比从服务器更快地检索本地缓存的响应,因此可以改善客户端的感知性能。
4.统一的界面
RESTful服务的端点是资源。 状态的变化是通过操纵这些资源而发生的。 发送到这些资源的消息是自描述的,超媒体是应用程序状态的引擎(最后一个约束听起来很熟悉)。
在下面的Richardson成熟度模型部分中,我们将逐步介绍在服务上实现这四个约束的外观。
5.分层架构
像食人魔和洋葱一样,REST体系结构也有层次。 RESTful服务中的分层体系结构是通过通过它发送的消息具有自描述性而实现的,并且每一层都无法从接口看到到下一层。
当我提交在Netflix上观看电影的请求时,无论我使用什么客户端,都将发送GET请求。 该请求可能会到达路由服务。 看到这是一个GET请求(即检索),然后该路由服务可以将该请求发送到服务器缓存。 该缓存可以检查其是否具有与请求的查询匹配的未过期资源。 在我的请求可以实现之前,这可能会持续进行几层甚至在Netflix体系结构中的某些区域。 所有这些路由和重定向都可能发生,因为REST消息是自描述的。 只要层可以理解HTTP,它就可以理解它收到的消息。
理查森成熟度模型
因此,我们已经涵盖了控制REST的六个主要体系结构样式约束中的五个。 现在,让我们仔细研究一下第四个样式约束,即统一界面,如先前所承诺的。 统一的接口定义了RESTful服务的许多“外观”,在该接口中定义了以下端点:GET:/ users / bob。 这也是定义HATEOAS的地方,这就是本文的重点。 为了使这些约束的影响可视化,并了解许多RESTful服务的不足之处,我将以有用的Richardson成熟度模型(RMM)为指南。
POX的沼泽
这是RMM上的级别0。 在这里,服务不能真诚地描述为RESTful。 客户与之连接的端点不是资源,我们在请求中未使用正确的HTTP动词,并且服务器未使用超媒体控件进行响应。 我们都已经在这样的服务上进行了工作,确实有可能(虽然可能不太可能)这种服务易于使用和维护……但是无论它是否绝对不是RESTful的。
通过RMM时,我们将通过通过像Amazon这样的在线零售商订购电视来进行交互,以观察REST中统一接口约束的实现如何影响服务器与客户端之间的交互。
在这里,我们看到了级别0的交互:
POST: viewItem
{
“id”: “1234”
}
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00
}
POST: orderItem
{
“id” : 1,
“items” : [
“item” : {
“id” : 1234
}
]
}
Response:
HTTP 1.1 200
{
“id” : 1,
“items” : [
“item” : {
“id” : 1234
}
]
}
资源资源
在RMM的这一级别上,我们正在实现统一接口的前两个约束。 我们正在通过URI(/ items / 1234,/ orders / 1)识别与我们交互的资源,而我们如何通过处理这些资源来与服务交互。
在向我们的服务发送请求时,为我们的每个资源提供专用的端点而不是单个端点,可以为客户与之交互的实体提供更多的标识。 它还为收集分析数据提供了机会,我们的客户如何与我们的服务交互。 热图可以更轻松地显示正在请求哪些资源以及该资源中的特定实体。
POST: /items/1234
{}
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00
}
POST: /orders/1
{
“item” : {
“id” : 1234
}
}
Response:
HTTP 1.1 200
{
“id” : 1,
“items” : [
“item” : {
“id” : 1234
}
]
}
因此,现在我们要使用资源终结点而不是所有请求都将通过的匿名终结点。 但是,我们与服务交互的性质尚不清楚。 当我们发布到/ items / 1234时,是在创建一个新项目还是要检索? 当我们发布到/ orders / 1时,是在更新现有实体还是创建新实体? 发送请求时,客户端尚不清楚这些交互。
HTTP
到目前为止,我们一直主要使用HTTP作为客户端与RESTful服务进行交互的传输机制。 在此级别,我们将开始使用已定义的HTTP规范。 到目前为止,我们已经使用POST提交了所有请求,现在我们将开始使用更合适的HTTP动词(方法类型)。 这不是一条单向的街道,但是我们的服务器还将响应更合适的状态代码,而不是对每个成功请求都响应200状态代码。
下表列出了RESTful服务通常实现的动词以及对这些动词的一些约束。 如果您不熟悉“幂等”一词(作者曾经),请知道这意味着当执行次数大于零时,执行请求的副作用是相同的。
GET调用应始终返回相同的项目列表。 DELETE请求应删除元素,但后续的DELETE请求应导致服务器状态不变。 注意,这并不意味着响应总是必须相同。 在第二个示例中,第二个DELETE请求可能返回错误响应。 安全表示该操作不会影响服务器的状态。 GET仅是检索,它不会更改其检索资源的状态。 但是,PUT请求可能导致状态更改,因此不是安全动词。
安全 | 不安全 | |
势力 | GET,HEAD,TRACE,选项 | 删除,放入 |
不占优势 | 开机自检 |
当我们开始在交互中使用正确的HTTP动词和状态代码时,这就是交互的外观:
GET: /items/1234
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00
}
PUT: /orders/1
{
“items” : [
“item” : {
“id” : 1234
}
]
}
Response:
HTTP 1.1 226
{
“items” : [
“item” : {
“id” : 1234
}
]
}
即使不深入了解HTTP规范,客户端与服务器之间的交互也变得更加清晰。 我们正在从服务器获取项目; 我们正在服务器上放置一些东西。 有一些字幕可以帮助您理解HTTP,了解PUT意味着修改会告诉开发人员一个订单已经存在,而我们正在修改它,而不是创建新订单(可能是POST请求)。
理解HTTP状态代码还将使开发人员对服务器如何响应来自客户端的请求有更多的了解。 尽管我们的服务器仍对初始GET请求返回适当的200响应,但服务器现在发送的PUT请求响应为226(已使用IM),这意味着仅返回已更改资源的增量。 如果您在“资源”部分下查看向订单添加商品的响应,则服务器将返回订单ID和商品列表。 在此响应中,仅返回添加到订单中的项目。 如果订单中已经有其他项目,则它们也将在“资源”响应中返回,但在此响应中省略。
或者,如果不存在ID为1234的项目,则HTTP已经定义了正确的响应,而不是返回空的响应正文或某种错误消息。 你能猜出来吗?
GET: /items/1234
Response:
HTTP 1.1 404
超媒体控件
上述为电视下订单的场景为实现超媒体控件带来好处提供了一个很好的用例。 在此情况下,我已经假定用户已经有一个ID为“ 1”的预先存在的订单,但是可能并非总是如此。
在不使用HATEOAS将状态应用程序传达给客户端的情况下,客户端必须足够聪明才能确定用户是否有未结订单,如果有,则确定该订单的ID。 这会导致工作重复,因为确定用户状态的业务逻辑现在同时存在于客户端和服务器上。 随着业务的变化,客户端和服务器之间将具有依赖关系来确定用户订单的状态,客户端和服务器代码都将发生更改,并且需要协调两者之间的发布。 HATEOAS通过其返回的链接告诉客户端状态(即客户端下一步可以做什么)来解决此问题。
GET: /items/1234
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00,
“link” : {
“rel” : “next”,
“href” : “/orders”
}
}
}
POST: /orders
{
“id” : 1,
“items” : [
{
“id” : 1234
}
]
}
Response:
HTTP 1.1 201:
{
“id” : 1,
“items” : [
{
“id” : 1234
}
]
links : [
{
“rel” : “next”,
“href” : “/orders/1/payment”
},
{
“rel” : “self”,
“href” : “/orders/1”
}
]
}
可以省去确定用户是否有有效订单的相对简单性,因为它不够复杂,不足以证明在服务器端实施HATEOAS然后开发可以解释服务产生的超媒体控件的客户端所花的时间(都不其中是微不足道的)。 也就是说,该示例也非常简单,仅代表客户端和服务器之间的一种交互。
死亡,税收和变化,HATEOAS就是这样
开发人员知道,“唯一可以确定的就是死亡和税收”的成语是错误的,第三个可以肯定的是:变化。 任何开发的应用程序都将在其生命周期内发生变化; 添加了新的业务需求,修改了现有业务需求,并一起删除了一些业务需求。
尽管我不希望HATEOAS成为银弹,但我确实相信HATEOAS是为数不多的因遇到现实问题而受益的技术之一。 以下是三个用例的样本,将它们结合在一起使用时,可以与其他可以想象的用例一起,为您说明为什么要在RESTful服务中实现HATEOAS的有力案例。
用例1:管理员和普通用户通过同一客户端进行交互
普通用户和管理员都使用同一客户端与服务进行交互。 在这种使用情况下,普通用户只能在/ items资源上执行GET,但是管理员也将具有PUT和DELETE特权。 如果我们在Richardson成熟度模型(HTTP)的第2级上停止,我们将需要让客户端理解用户所具有的特权类型,以便正确地向用户呈现接口。
使用HATEOAS,可能就像客户端呈现服务器发送的一些新控件一样简单。 这是请求中的差异可能看起来的样子。 另外,我们可能不希望管理员下达订单:
Request:
[Headers]
user: bob
roles: USER
GET: /items/1234
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00,
“links” : [
{
“rel” : “next”,
“href” : “/orders”
}
]
}
}
Request:
[ Headers ]
user: jim
roles: USER, ADMIN
GET: /items/1234
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00,
“links” : [
{
“rel” : “modify”,
“href” : “/items/1234”
},
{
“rel” : “delete”,
“href” : “/items/1234”
}
]
}
}
用例2:管理员不再可以删除
业务需求发生变化,管理员不再能够删除项目。 在上一个用例中,很可能会说不需要更改客户端(例如,管理员用户将需要一个表单来修改项目的字段),但是删除DELETE动词绝对可以完成而无需更改客户。
随着HATEOAS服务不再返回DELETE链接,客户端将不再将其显示给管理员用户。
Request:
[Headers]
user: jim
roles: USER, ADMIN
GET: /items/1234
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00,
“links” : [
{
“rel” : “modify”,
“href” : “/items/1234”
}
]
}
}
用例3:用户可以出售自己的商品
现在,企业要求用户具有销售自己的用户商品的能力。 这个用例比前两个更为实际,开始显示出客户端上业务逻辑的数量和复杂性Swift增加,并且还引入了客户端和服务器之间的可能耦合。
用户可以出售自己的物品,但他们也只能只修改自己准备出售的物品。 用户Bob不能修改用户Steve的项目,反之亦然。 解决此问题的常见方法可能是在项目实体内返回一个指定所有权的新字段,但是现在我们正在修改项目,只是为了使我们的客户端可以出于任何业务原因向用户正确呈现界面。
现在,我们正在引入客户端和服务器之间的耦合,并且它们之间的界限很快开始变得模糊。 有了HATEOAS服务,至少对于客户而言,这种复杂性就很多了,并且我们的项目实体保持不变。 以下是一些带有和不带有HATEOAS的示例请求,请注意在HATEOAS示例中,响应看起来与用例1的响应相同。
没有HATEOAS:
Request:
[Headers]
user: jim
roles: USER
GET: /items/1234
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00,
“owner” : “jim”
}
使用HATEOAS:
Request:
[Headers]
user: jim
roles: USER
GET: /items/1234
Response:
HTTP 1.1 200
{
“id” : 1234,
“description” : “FooBar TV”,
“image” : “fooBarTv.jpg”,
“price” : 50.00,
“links” : [
{
“rel” : “modify”,
“href” : “/items/1234”
},
{
“rel” : “delete”,
“href” : “/items/1234”
}
]
}
}
摘要
尽管REST的第一个样式约束要求将客户端和服务器之间的关注点分离,但是这种样式约束在不实现HATEOAS的过程中受到了损害。 围绕如何计算用户状态的业务逻辑进行更改意味着需要在客户端和服务器中进行更改。 客户端和服务器的独立可扩展性丢失了(必须同步客户端和服务器的发布),并且重复了业务逻辑。 世界需要更多的HATEOAS来解决这个问题。
参考书目
- http://roy.gbiv.com/untangled/2008/rest-apis-must-be-超文本驱动
- http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-745
- https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
- http://martinfowler.com/articles/richardsonMaturityModel.html
- https://zh.wikipedia.org/wiki/No_Silver_Bullet
- http://www.crummy.com/
- http://www.crummy.com/writing/speaking/2008-QCon/act3.html
翻译自: https://www.javacodegeeks.com/2016/03/dont-hate-hateoas.html