6.2 启用超媒体
6.2 启用超媒体
到目前为止,我们所创建的API非常简单,但是只要消费它的客户端知道API的URL模式,它们就可以正常运行。例如,客户端可能会以硬编码的形式对“/design/recent”发送GET请求,以便于获取最近创建的taco。类似的,客户端会以硬编码的形式将taco列表中的ID拼接到“/design”上形成获取特定taco资源的URL。
在API客户端编码中,使用硬编码模式和字符串操作是很常见的。但是,我们设想一下,如果API的URL模式发生了变化又会怎么样呢?硬编码的客户端代码掌握的依然是旧的API信息,因此客户端代码将无法正常运行。对API URL进行硬编码和字符串操作会让客户端代码变得很脆弱。
超媒体作为应用状态引擎(Hypermedia as the Engine of Application State,HATEOAS)是一种创建自描述API的方式。API所返回的资源中会包含相关资源的链接,客户端只需要了解最少的API URL信息就能导航整个API。这种方式能够掌握API所提供的资源之间的关系,客户端能够基于API的URL中所发现的关系对它们进行遍历。
举例来说,假设某个客户端想要请求最近设计的taco的列表,按照原始的形式,在没有超链接的情况下,客户端以JSON格式接收到的taco列表会如下所示(为了简洁,这里只保留了第一个taco,剩余的省略了):
1 | [ |
如果客户端想要获取某个taco或者对其进行其他HTTP操作,就需要将它的id属性以硬编码的方式拼接到一个路径为“/design”的URL上。与之类似,如果客户端想要对某个配料执行HTTP请求,就需要将该配料id属性的值拼接到路径为“/ingredients”的URL上。在这两种情况下,都需要在路径上添加“http://”或“https://”前缀以及API的主机名。
如果API启用了超媒体功能,那么API将会描述自己的URL,从而减轻客户端对其进行硬编码的痛苦。如果嵌入超链接,那么最近创建的taco列表将会如程序清单6.3所示。
程序清单6.3 包含超链接的taco资源列表
1 | { |
这种特殊风格的HATEOAS被称为HAL(超文本应用语言,Hypertext ApplicationLanguage)。这是一种在JSON响应中嵌入超链接的简单通用格式。
虽然这个列表看上去不像前面那样简洁,但是它确实提供了一些有用的信息。这个新taco列表中的每个元素都包含了一个名为“_links”的属性,为客户端提供导航API的超链接。在本例中,taco和配料都有一个“self”链接,用来引用该资源;整个列表有一个“recents”链接,用来引用该API自身。
如果客户端应用需要对列表中的taco执行HTTP请求,那么在开发的时候不需要关心taco资源的URL是什么样子。相反,它只需要请求“self”链接就可以了,该属性将会映射至http://localhost:8080/design/4。如果客户端想要处理特定的配料,只需要查找该配料的“self”链接即可。
Spring HATEOAS项目为Spring提供了超链接的支持。它提供了一些类和资源装配器(assembler),在Spring MVC控制器返回资源之前能够为其添加链接。
为了在Taco Cloud API中启用超媒体功能,我们需要在构建文件中添加如下的Spring HATEOAS starter依赖:
1 | <dependency> |
这个starter不仅会将Spring HATEOAS添加到项目的类路径中,还会提供自动配置功能以启用Spring HATEOAS。我们所需要做的就是重新实现控制器,让它们返回资源类型,而不是领域类型。
我们首先为最近taco列表添加超链接,也就是针对“/design/recent”的GET请求。
6.2.1 添加超链接
Spring HATEOAS提供了两个主要的类型来表示超链接资源:Resource和Resources。Resource代表一个资源,而Resources代表资源的集合。这两种类型都能携带到其他资源的链接。当从Spring MVC REST控制器返回时,它们所携带的链接将会包含到客户端所接收到的JSON(或XML)中。
为了给最近创建的taco添加超链接,我们需要重新实现程序清单6.2中的recentTacos()方法。原始的实现返回的是List
程序清单6.4 为资源添加超链接
1 |
|
在这个新版本的recentTacos()中,我们不再直接返回taco的列表,而是使用Resources.wrap()将taco列表包装为Resources<Resource
1 | "_links": { |
这是一个很好的起点,但是我们还有一些事情需要完成。现在,我们只是为整体的列表添加了链接,还没有为taco资源本身以及每个taco中的配料添加链接。我们很快就会实现该功能,但是在此之前,我们要先解决recents链接中的硬编码问题。
像这样对URL进行硬编码是一种很糟糕的办法。除非Taco Cloud的目标仅限于在本地开发机器上运行应用,否则,我们需要找一种方式避免在URL中使用硬编码的localhost:8080。幸运的是,Spring HATEOAS以链接构建者(link builder)的方式为我们提供了帮助。
在Spring HATEOAS中,最有用的链接构建者是ControllerLinkBuilder。这个链接构建者非常智能,它能自动探知主机名是什么,这样就能避免对其进行硬编码。同时,它还提供了流畅的API,允许我们相对于控制器的基础URL构建链接。
借助ControllerLinkBuilder,我们可以将recentTacos()中硬编码的Link创建改造成如下的形式:
1 | Resources<Resource<Taco>> recentResources = Resources.wrap(tacos); |
我们不仅不需要硬编码主机名,而且不再需要指定“/design”。在这里,我们向DesignTacoController请求获取一个链接,它的基础路径为“/design”。ControllerLinkBuilder使用控制器的基础路径作为我们创建的Link对象的基础。
接下来,调用了在Spring项目中我最喜欢的一个方法:slash()。我喜欢这个方法的原因是这个方法非常简洁地描述了它要做的事情。这个方法会为URL添加斜线(/)和给定的值,所形成的URL路径是“/design/recent”。
最后,我们为Link指定了一个关系名。在本例中,关系名为recents。
尽管我是slash()的忠实粉丝,但是ControllerLinkBuilder还有另外一个方法,能够消除链接URL上的所有硬编码。此时,我们不再需要调用slash(),而是调用linkTo(),并将控制器中的一个方法传递给它,这样ControllerLinkBuilder就能推断出控制器的基础路径和该方法的映射路径。如下的代码就以这种方式使用了linkTo()方法:
1 | Resources<Resource<Taco>> recentResources = Resources.wrap(tacos); |
在这里,我静态导入了linkTo()和methodOn()方法(它们都来自ControllerLinkBuilder),从而让代码更易于阅读。methodOn()方法传入控制器类,从而允许我们调用recentTacos()方法,这个调用会被ControllerLinkBuilder拦截,用来确定控制器的基础路径和recentTacos()的映射路径。现在,整个URL都从控制器的映射中判断出来了,而且完全没有硬编码。非常棒!
6.2.2 创建资源装配器
现在,我们需要为列表中的taco资源添加链接。有种方案就是遍历Resources对象中所携带的每个Resource<Taco>
元素,为它们依次添加Link。但是,这种方式有点过于枯燥,在需要返回taco列表的所有地方都需要在API中重复循环相关的代码。
我们需要有一种不同的策略。
对于列表中的每个taco,我们不再使用Resources.wrap()来创建Resource,而是定义一个将Taco对象转换为TacoResource对象的工具类。TacoResource对象与Taco类似,但是它本身能携带链接。程序清单6.5展示了TacoResource。
程序清单6.5 能够携带领域数据和超链接列表taco资源
1 | package tacos.web.api; |
从很多方面来看,TacoResource都与Taco领域类型没有区别。它们都有name、createdAt和ingredients属性。但是,TacoResource扩展了ResourceSupport,从而继承了一个Link对象的列表和管理链接列表的方法。
除此之外,TacoResource并没有包含Taco的id属性。这是因为没有必要在API中暴露数据库相关的ID。从API客户端的角度来看,资源的self链接将会作为该资源的标识符。
TacoResource有一个很简单的构造器,会接收一个Taco对象并且会将Taco中的相关属性复制到自己的属性中。这样的话,我们可以很容易地将一个Taco对象转换为TacoResource。但是,如果我们就此止步,就依然需要遍历Taco列表才能将其转换成Resources<TacoResource>
。
为了将Taco对象转换成TacoResource对象,我们需要创建一个资源装配器(resource assembler)。我们所需要的装配器如程序清单6.6所示。
程序清单6.6 装配taco资源的资源装配器
1 | package tacos.web.api; |
TacoResourceAssembler有一个默认的构造器,会告诉超类(ResourceAssemblerSupport)在创建TacoResource中的链接时将会使用DesignTacoController来确定所有URL的基础路径。
instantiateResource()方法进行了重写,以便基于给定的Taco实例化TacoResource。如果TacoResource有默认构造器,那么这个方法是可选的。但是,在本例中,TacoResource的构造过程需要Taco,所以我们要重写它。
最后是toResource()方法,这是在扩展ResourceAssemblerSupport时唯一强制实现的方法。在这里,我们告诉它要通过Taco创建TacoResource,并且要设置一个self链接,这个链接的URL是根据Taco对象的id属性衍生出来的。
在表面上,toResource()和instantiateResource()的用途很相似,但是它们的目的略有不同。instantiateResource()只是为了实例化一个Resource对象,而toResource()的意图不仅是创建Resource对象,还要为其填充链接。在内部,toResource()将会调用instantiateResource()。
现在,我们调整一下recentTacos(),让它使用TacoResourceAssembler:
1 |
|
在这里,recentTacos()的返回值不再是Resources<Resource<Taco>>
类型,而是利用我们新定义的TacoResource类型返回了一个Resources<TacoResource>
。在从repository获取taco之后,我们将Taco对象的列表传递给TacoResourceAssembler的toResources()方法。这个便利的方法会循环所有的Taco对象,调用我们在TacoResourceAssembler中重写的toResource()方法来创建TacoResource对象的列表。
在有了TacoResource列表之后,我们接下来创建了Resources<TacoResource>
对象,然后像前面版本的recentTacos()一样,为其填充了recents链接。
此时,对“/design/recent”发起GET请求将会生成taco的一个列表,其中的每个taco都有一个self链接,而列表整体有一个recents链接。但是,配料目前还没有链接。为了解决这个问题,我们需要为配料创建一个新的资源装配器:
1 | package tacos.web.api; |
我们可以看到,IngredientResourceAssembler与TacoResourceAssembler非常相似,但是它使用的是Ingredient和IngredientResource对象而不是Taco和TacoResource对象。
谈到IngredientResource对象,它的源码如下所示:
1 | package tacos.web.api; |
与TacoResource类似,IngredientResource扩展了ResourceSupport,并且会将领域类型相关的属性复制到自己的属性中(id属性除外)。
接下来,我们需要修改一下TacoResource,让它能够携带IngredientResource对象,而不是Ingredient对象:
1 | package tacos.web.api; |
这个新版本的TacoResource会创建一个static final的IngredientResourceAssembler实例,并且会使用它的toResource()方法将给定Taco对象的Ingredient列表转换成IngredientResource列表。
我们现在的taco列表已经完全具备了超链接,不仅是它本身(recents链接),而且所有的taco条目和每个taco中的配料都有了超链接。响应的内容应该与程序清单6.3非常相似了。
你可以就此止步并跳到下一节,但是在此之前,我想解决程序清单6.3中一些不太理想的问题。
6.2.3 命名嵌套式的关联关系
如果你仔细看一下程序清单6.3,就会发现顶层的元素如下所示:
1 | { |
最值得注意的是在embedded之下有一个名为tacoResourceList的属性。之所以有这个名称,是因为Resources对象是通过List<TacoResource>
创建出来的。尽管可能性不太大,但是假设我们将TacoResource类重构成了其他的名称,那么结果JSON中的字段名将会随之发生变化。这样,所有依赖该名称的客户端代码都会产生问题。
@Relation
注解能够帮助我们消除JSON字段名和Java代码中定义的资源类名之间的耦合。通过为TacoResource添加@Relation
注解,我们就能指定SpringHATEOAS该如何命名结果JSON中的字段名:
1 |
|
在这里,我们指定当在Resources对象中引用TacoResource对象列表时它应该被命名为tacos。虽然在我们的API中没有用到,但是如果在JSON中引用单个TacoResource对象,那么它的名字将会是taco。这样的话,“/design/recent”所返回的JSON将会如下所示(不管我们是否要对TacoResource进行重构,这个结构都不会发生变化):
1 | { |
借助Spring HATEOAS,向API中添加链接变得非常简单直接。尽管如此,它也会添加一些额外的代码。所以,很多开发人员会选择在API中不使用HATEOAS,但是如果API的URL模式发生变化,那么客户端代码就不可用了。所以,我建议你认真考虑一下HATEOAS,不要因为偷懒而忽略在资源中添加超链接。
如果你真的想要偷懒,那么只要你使用Spring Data来实现repository,我们就还有一个双赢的方案。接下来,我们看一下基于在第3章中使用Spring Data所创建的数据repository,如何借助Spring Data REST自动创建API。