该项目提供了一些 API,以便在使用 Spring,尤其是 Spring MVC 时,轻松创建遵循 HATEOAS 原则的 REST 表示。它试图解决的核心问题是链接创建和表示组装。
© 2012-2021 原始作者。
您可以为自己的使用和分发给他人制作本文档的副本,前提是您不对此类副本收取任何费用,并且每份副本都包含本版权声明,无论是印刷分发还是电子分发。 |
1. 前言
1.1. 迁移到 Spring HATEOAS 1.0
在 1.0 版本中,我们借此机会重新评估了 0.x 版本中做出的一些设计和包结构选择。我们收到了大量关于这方面的反馈,主要版本升级似乎是重构这些内容最自然的时机。
1.1.1. 变更
包结构的最大变化是由引入超媒体类型注册 API 驱动的,该 API 用于支持 Spring HATEOAS 中的其他媒体类型。这使得客户端和服务器 API(分别命名为相应包)以及 mediatype
包中的媒体类型实现清晰分离。
将您的代码库升级到新 API 的最简单方法是使用 迁移脚本。在开始之前,这里快速浏览一下变更。
表示模型
ResourceSupport
/Resource
/Resources
/PagedResources
这组类的命名从未真正让人觉得恰当。毕竟,这些类型实际上并未体现资源本身,而是表示模型,可以利用超媒体信息和 affordances 进行丰富。下面是新旧名称的映射关系:
-
ResourceSupport
现在是RepresentationModel
-
Resource
现在是EntityModel
-
Resources
现在是CollectionModel
-
PagedResources
现在是PagedModel
因此,ResourceAssembler
已重命名为 RepresentationModelAssembler
,其方法 toResource(…)
和 toResources(…)
分别重命名为 toModel(…)
和 toCollectionModel(…)
。此外,名称变更也已反映在 TypeReferences
中包含的类中。
-
RepresentationModel.getLinks()
现在公开了一个Links
实例(而非List<Link>
),因为它公开了额外的 API,可以使用各种策略连接和合并不同的Links
实例。此外,它已被转换为自绑定泛型类型,以允许向实例添加链接的方法返回实例本身。 -
LinkDiscoverer
API 已移至client
包。 -
LinkBuilder
和EntityLinks
API 已移至server
包。 -
ControllerLinkBuilder
已移至server.mvc
包,并已弃用,由WebMvcLinkBuilder
替换。 -
RelProvider
已重命名为LinkRelationProvider
,并返回LinkRelation
实例而非String
。 -
VndError
已移至mediatype.vnderror
包。
1.1.2. 迁移脚本
您可以在应用程序根目录找到一个脚本,运行该脚本将更新所有导入语句和静态方法引用,以指向 Spring HATEOAS 源代码仓库中已移动的类型。只需下载并从您的项目根目录运行它。默认情况下,它将检查所有 Java 源文件,并将旧的 Spring HATEOAS 类型引用替换为新的。
$ ./migrate-to-1.0.sh
Migrating Spring HATEOAS references to 1.0 for files : *.java
Adapting ./src/main/java/…
…
Done!
请注意,该脚本不一定能完全修复所有变更,但应涵盖最重要的重构。
现在,在您喜欢的 Git 客户端中验证对文件所做的更改,并酌情提交。如果您发现方法或类型引用未迁移,请在我们的问题跟踪器中创建工单。
1.1.3. 从 1.0 M3 迁移到 1.0 RC1
-
Link.andAffordance(…)
接受 Affordance 详细信息的方法已移至Affordances
。现在手动构建Affordance
实例,请使用Affordances.of(link).afford(…)
。另请注意从Affordances
中公开的新类型AffordanceBuilder
,以用于流畅的用法。详情请参见 Affordances。 -
AffordanceModelFactory.getAffordanceModel(…)
现在接收InputPayloadMetadata
和PayloadMetadata
实例,而非ResolvableType
s,以允许非基于类型的实现。自定义媒体类型实现必须相应地进行调整。 -
如果属性值符合规范中定义的默认值,HAL Forms 现在不渲染属性。也就是说,如果以前显式地将
required
设置为false
,我们现在只省略required
的条目。此外,我们现在只强制将使用PATCH
作为 HTTP 方法的模板中的属性设置为非必需。
2. 基本概念
本节涵盖 Spring HATEOAS 的基础知识及其基本领域抽象。
2.1. 链接
超媒体的基本思想是用超媒体元素丰富资源的表示。最简单的形式是链接。它们向客户端指示可以导航到特定资源。相关资源的语义在所谓的链接关系中定义。您可能已经在 HTML 文件的头部看到过这一点:
<link href="theme.css" rel="stylesheet" type="text/css" />
如您所见,该链接指向资源 theme.css
并指示它是一个样式表。链接通常携带额外信息,例如指向的资源将返回的媒体类型。然而,链接的基本构建块是其引用和关系。
Spring HATEOAS 允许您通过其不可变的 Link
值类型使用链接。其构造函数接受超文本引用和链接关系,后者默认为 IANA 链接关系 self
。有关后者的更多信息,请参见 链接关系。
Link link = Link.of("/something");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
link = Link.of("/something", "my-rel");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("my-rel"));
Link
根据 RFC-8288 中定义的内容公开其他属性。您可以通过在 Link
实例上调用相应的 wither 方法来设置它们。
有关如何创建指向 Spring MVC 和 Spring WebFlux 控制器的链接的更多信息,请参阅 在 Spring MVC 中构建链接 和 在 Spring WebFlux 中构建链接。
2.2. URI 模板
对于 Spring HATEOAS Link
,超文本引用不仅可以是 URI,还可以是符合 RFC-6570 的 URI 模板。URI 模板包含所谓的模板变量,并允许扩展这些参数。这使得客户端无需了解最终 URI 的结构,即可将参数化模板转换为 URI,只需要知道变量的名称。
Link link = Link.of("/{segment}/something{?parameter}");
assertThat(link.isTemplated()).isTrue(); (1)
assertThat(link.getVariableNames()).contains("segment", "parameter"); (2)
Map<String, Object> values = new HashMap<>();
values.put("segment", "path");
values.put("parameter", 42);
assertThat(link.expand(values).getHref()) (3)
.isEqualTo("/path/something?parameter=42");
1 | Link 实例指示它是模板化的,即它包含一个 URI 模板。 |
2 | 它公开模板中包含的参数。 |
3 | 它允许扩展参数。 |
可以手动构建 URI 模板,并在之后添加模板变量。
UriTemplate template = UriTemplate.of("/{segment}/something")
.with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM);
assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");
2.3. 链接关系
为了指示目标资源与当前资源的关系,使用所谓的链接关系。Spring HATEOAS 提供 LinkRelation
类型,可以轻松创建基于 String
的实例。
2.3.1. IANA 链接关系
互联网号码分配局包含一组 预定义的链接关系。可以通过 IanaLinkRelations
引用它们。
Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);
assertThat(link.getRel()).isEqualTo(LinkRelation.of("next"));
assertThat(IanaLinkRelation.isIanaRel(link.getRel())).isTrue();
2.4. 表示模型
为了轻松创建超媒体丰富的表示,Spring HATEOAS 提供了一组以 RepresentationModel
为根的类。它本质上是 Link
s 集合的容器,并具有方便的方法将这些 Links 添加到模型中。这些模型稍后可以渲染成各种媒体类型格式,这些格式将定义超媒体元素在表示中的样子。有关这方面的更多信息,请参阅 媒体类型。
RepresentationModel
类层次结构使用 RepresentationModel
的默认方式是创建其子类,以包含表示应该包含的所有属性,创建该类的实例,填充属性并用链接丰富它。
class PersonModel extends RepresentationModel<PersonModel> {
String firstname, lastname;
}
泛型自类型是必要的,以便让 RepresentationModel.add(…)
方法返回其自身的实例。现在可以这样使用模型类型:
PersonModel model = new PersonModel();
model.firstname = "Dave";
model.lastname = "Matthews";
model.add(Link.of("https://myhost/people/42"));
如果您从 Spring MVC 或 WebFlux 控制器返回此类实例,并且客户端发送了 Accept
头部设置为 application/hal+json
,则响应将如下所示:
{
"_links" : {
"self" : {
"href" : "https://myhost/people/42"
}
},
"firstname" : "Dave",
"lastname" : "Matthews"
}
2.4.1. 项目资源表示模型
对于由单个对象或概念支持的资源,存在一个便捷的 EntityModel
类型。您无需为每个概念创建自定义模型类型,只需重用已存在的类型,并将其实例包装到 EntityModel
中即可。
EntityModel
包装现有对象Person person = new Person("Dave", "Matthews");
EntityModel<Person> model = EntityModel.of(person);
2.4.2. 集合资源表示模型
对于概念上是集合的资源,可以使用 CollectionModel
。其元素既可以是简单对象,也可以是 RepresentationModel
实例。
CollectionModel
包装现有对象集合Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews"));
CollectionModel<Person> model = CollectionModel.of(people);
虽然 EntityModel
被限制始终包含载荷,因此允许对单个实例上的类型 arrangement 进行推理,但 CollectionModel
的底层集合可能是空的。由于 Java 的类型擦除,我们无法实际检测到 CollectionModel<Person> model = CollectionModel.empty()
实际上是 CollectionModel<Person>
,因为我们看到的只是运行时实例和一个空集合。可以通过在构造时通过 CollectionModel.empty(Person.class)
将缺失的类型信息添加到空实例中,或者作为底层集合可能为空情况下的备选方案。
Iterable<Person> people = repository.findAll();
var model = CollectionModel.of(people).withFallbackType(Person.class);
3. 服务端支持
3.1. 在 Spring MVC 中构建链接
现在我们已经有了领域词汇,但主要挑战仍然存在:如何以一种不那么脆弱的方式创建要封装到 Link
实例中的实际 URI。现在,我们不得不在各处复制 URI 字符串。这样做既脆弱又难以维护。
假设您的 Spring MVC 控制器实现如下:
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll() { … }
@GetMapping("/{person}")
HttpEntity<PersonModel> show(@PathVariable Long person) { … }
}
这里我们看到两种约定。第一种是集合资源,通过控制器方法的 @GetMapping
注解暴露,该集合的单个元素作为直接子资源暴露。集合资源可以在简单的 URI(如刚刚所示)下暴露,也可以在更复杂的 URI(例如 /people/{id}/addresses
)下暴露。假设您想链接到所有人的集合资源。遵循上述方法会带来两个问题:
-
要创建一个绝对 URI,您需要查找协议、主机名、端口、Servlet 基本路径和其他值。这很麻烦,并且需要丑陋的手动字符串拼接代码。
-
您可能不想在基本 URI 之上拼接
/people
,因为那样您将不得不在多个地方维护这些信息。如果您更改映射,那么所有指向它的客户端也必须更改。
Spring HATEOAS 现在提供 WebMvcLinkBuilder
,允许您通过指向控制器类来创建链接。以下示例展示了如何操作:
Link link = linkTo(PersonController.class).withRel("people");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("people"));
assertThat(link.getHref()).endsWith("/people");
WebMvcLinkBuilder
在底层使用 Spring 的 ServletUriComponentsBuilder
从当前请求中获取基本的 URI 信息。假设您的应用程序运行在 localhost:8080/your-app,这正是您在其之上构建额外部分的 URI。该构建器现在检查给定的控制器类的根映射,因此最终得到 localhost:8080/your-app/people。您也可以构建更嵌套的链接。以下示例展示了如何操作:
Person person = new Person(1L, "Dave", "Matthews");
// /person / 1
Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel();
assertThat(link.getRel(), is(IanaLinkRelation.SELF.value()));
assertThat(link.getHref(), endsWith("/people/1"));
该构建器还允许创建 URI 实例以便构建(例如,响应头值)。
HttpHeaders headers = new HttpHeaders();
headers.setLocation(linkTo(PersonController.class).slash(person).toUri());
return new ResponseEntity<PersonModel>(headers, HttpStatus.CREATED);
3.1.1. 构建指向方法的链接
您甚至可以构建指向方法的链接,或者创建虚拟控制器方法调用。第一种方法是将 Method
实例交给 WebMvcLinkBuilder
。以下示例展示了如何操作:
Method method = PersonController.class.getMethod("show", Long.class);
Link link = linkTo(method, 2L).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2"));
这仍然有点令人不满意,因为我们首先必须获取一个 Method
实例,这会抛出异常,并且通常相当麻烦。至少我们没有重复映射。一个更好的方法是在控制器代理上对目标方法进行虚拟方法调用,我们可以通过使用 methodOn(…)
助手来创建该代理。以下示例展示了如何操作:
Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2");
methodOn(…)
创建控制器类的代理,该代理记录方法调用并在为该方法的返回类型创建的代理中暴露它。这允许流畅地表达我们想要获取映射的方法。然而,使用此技术获取的方法有一些限制:
-
返回类型必须能够被代理,因为我们需要在其上暴露方法调用。
-
传递给方法的参数通常被忽略(通过
@PathVariable
引用的参数除外,因为它们构成了 URI)。
控制请求参数的渲染
集合类型的请求参数实际上可以通过两种不同的方式实现。URI 模板规范列出了渲染它们的复合方式,它对每个值重复参数名称(param=value1¶m=value2
),以及非复合方式,它用逗号分隔值(param=value1,value2
)。Spring MVC 可以正确地从这两种格式中解析出集合。默认情况下,值的渲染方式默认为复合样式。如果您想以非复合样式渲染值,可以在请求参数处理方法参数上使用 @NonComposite
注解:
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll(
@NonComposite @RequestParam Collection<String> names) { … } (1)
}
var values = List.of("Matthews", "Beauford");
var link = linkTo(methodOn(PersonController.class).showAll(values)).withSelfRel(); (2)
assertThat(link.getHref()).endsWith("/people?names=Matthews,Beauford"); (3)
1 | 我们使用 @NonComposite 注解声明我们希望值以逗号分隔渲染。 |
2 | 我们使用值列表调用该方法。 |
3 | 请看请求参数如何以预期格式渲染。 |
我们暴露 @NonComposite 的原因是,渲染请求参数的复合方式已固化在 Spring 的 UriComponents 构建器内部,而我们只在 Spring HATEOAS 1.4 中引入了非复合样式。如果今天从头开始,我们可能会默认采用该样式,并宁愿让用户显式选择复合样式,而不是反过来。 |
3.3. Affordances
环境的 affordances 是它所提供的……它提供或赋予的,无论是好的还是坏的。“afford” 这个动词在词典中可以找到,但名词 “affordance” 则没有。是我创造的。
知觉的生态学进路 (The Ecological Approach to Visual Perception)(第 126 页)
基于 REST 的资源不仅提供数据,还提供控制。构成灵活服务的最后一个要素是关于如何使用各种控制的详细 affordances。由于 affordances 与链接相关联,Spring HATEOAS 提供了一个 API,可以将所需的任意多个相关方法附加到链接。就像通过指向 Spring MVC 控制器方法来创建链接一样(详情请参见 在 Spring MVC 中构建链接),您可以 …
以下代码展示了如何获取一个 self 链接并关联另外两个 affordances:
@GetMapping("/employees/{id}")
public EntityModel<Employee> findOne(@PathVariable Integer id) {
Class<EmployeeController> controllerClass = EmployeeController.class;
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel(); (1)
// Return the affordance + a link back to the entire collection resource.
return EntityModel.of(EMPLOYEES.get(id), //
findOneLink //
.andAffordance(afford(methodOn(controllerClass).updateEmployee(null, id))) (2)
.andAffordance(afford(methodOn(controllerClass).partiallyUpdateEmployee(null, id)))); (3)
}
1 | 创建 self 链接。 |
2 | 将 updateEmployee 方法与 self 链接关联。 |
3 | 将 partiallyUpdateEmployee 方法与 self 链接关联。 |
使用 .andAffordance(afford(…))
,您可以使用控制器的方法将 PUT 和 PATCH 操作连接到 GET 操作。想象一下,上面 affords 的相关方法看起来像这样:
updateEmpoyee
方法@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee( //
@RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
partiallyUpdateEmployee
方法@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee( //
@RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
使用 afford(…)
方法指向这些方法,将导致 Spring HATEOAS 分析请求体和响应类型,并捕获元数据,以允许不同的媒体类型实现利用该信息将其转换为输入和输出的描述。
3.3.1. 手动构建 affordances
虽然这是为链接注册 affordances 的主要方式,但也可能需要手动构建其中一些。这可以通过使用 Affordances
API 来实现。
Affordances
API 手动注册 affordancesvar methodInvocation = methodOn(EmployeeController.class).all();
var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) (1)
.afford(HttpMethod.POST) (2)
.withInputAndOutput(Employee.class) //
.withName("createEmployee") //
.andAfford(HttpMethod.GET) (3)
.withOutput(Employee.class) //
.addParameters(//
QueryParameter.optional("name"), //
QueryParameter.optional("role")) //
.withName("search") //
.toLink();
1 | 首先,从 Link 实例创建 Affordances 实例,从而创建描述 affordances 的上下文。 |
2 | 每个 affordance 都以它应该支持的 HTTP 方法开头。然后我们将一个类型注册为载荷描述,并显式命名该 affordance。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这实际上创建了与指向 EmployeeController.newEmployee(…) 所创建的 affordance 相同的功能。 |
3 | 下一个 affordance 的构建旨在反映指向 EmployeeController.search(…) 所发生的情况。在这里,我们将 Employee 定义为创建的响应的模型,并显式注册 QueryParameter s。 |
Affordances 由特定媒体类型的 affordance 模型支持,这些模型将通用 affordance 元数据转换为特定表示。请务必查看 媒体类型 部分中关于 affordances 的章节,以找到有关如何控制该元数据暴露的更多详细信息。
3.4. 转发头处理
RFC-7239 转发头最常用于应用程序位于代理后、负载均衡器后或云中。实际接收 Web 请求的节点是基础设施的一部分,并将请求转发到您的应用程序。
您的应用程序可能运行在 localhost:8080
,但对外而言,您应该位于 reallycoolsite.com
(并且使用 Web 标准端口 80)。通过让代理包含额外的头部(许多代理已经这样做),Spring HATEOAS 可以正确生成链接,因为它使用了 Spring Framework 的功能来获取原始请求的基本 URI。
任何基于外部输入可以改变根 URI 的东西都必须得到适当的保护。这就是为什么默认情况下,转发头处理是禁用的。您必须启用它才能运行。如果您正在部署到云环境或您控制代理和负载均衡器的配置中,那么您肯定会希望使用此功能。 |
要启用转发头处理,您需要在应用程序中注册 Spring MVC 的 Spring ForwardedHeaderFilter
(详情参见此处)或 Spring WebFlux 的 ForwardedHeaderTransformer
(详情参见此处)。在 Spring Boot 应用程序中,这些组件可以简单地声明为 Spring Bean,如此处所述。
ForwardedHeaderFilter
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
这将创建一个 Servlet 过滤器,用于处理所有 X-Forwarded-…
头部。并且会正确地将其注册到 Servlet 处理器。
对于 Spring WebFlux 应用程序,对应的 reactive 组件是 ForwardedHeaderTransformer
。
ForwardedHeaderTransformer
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
这将创建一个函数,用于转换 reactive Web 请求,处理 X-Forwarded-…
头部。并且会正确地将其注册到 WebFlux。
配置如上所示就绪后,通过 X-Forwarded-…
头部传递的请求将看到这些头部反映在生成的链接中。
X-Forwarded-…
头部请求curl -v localhost:8080/employees \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Host: example.com' \
-H 'X-Forwarded-Port: 9001'
{
"_embedded": {
"employees": [
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "https://example.com:9001/employees/1"
},
"employees": {
"href": "https://example.com:9001/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "https://example.com:9001/employees"
},
"root": {
"href": "https://example.com:9001"
}
}
}
3.5. 使用 EntityLinks 接口
EntityLinks 及其各种实现目前并未为 Spring WebFlux 应用程序提供开箱即用的支持。EntityLinks SPI 中定义的契约最初是针对 Spring Web MVC 的,并未考虑 Reactor 类型。正在开发一个支持响应式编程的类似契约。 |
到目前为止,我们通过指向 Web 框架实现(即 Spring MVC 控制器)并检查映射来创建链接。在许多情况下,这些类本质上是读取和写入由模型类支持的表示。
EntityLinks
接口现在公开了一个 API,可以根据模型类型查找 Link
或 LinkBuilder
。这些方法本质上返回的链接指向集合资源(例如 /people
)或项目资源(例如 /people/1
)。以下示例展示了如何使用 EntityLinks
:
EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);
EntityLinks
通过在您的 Spring MVC 配置中激活 @EnableHypermediaSupport
来通过依赖注入可用。这将导致注册各种 EntityLinks
的默认实现。最基础的一个是 ControllerEntityLinks
,它检查 Spring MVC 控制器类。如果您想注册自己的 EntityLinks
实现,请查看 此部分。
3.5.1. 基于 Spring MVC 控制器的 EntityLinks
激活 entity links 功能会导致检查当前 ApplicationContext
中所有可用的 Spring MVC 控制器是否存在 @ExposesResourceFor(…)
注解。该注解暴露了控制器管理的模型类型。除此之外,我们假定您遵守以下 URI 映射设置和约定:
-
一个类型级别的
@ExposesResourceFor(…)
注解,声明控制器暴露集合和项目资源的实体类型。 -
一个类级别的基本映射,代表集合资源。
-
一个额外的方法级别映射,扩展该映射以将标识符追加为额外的路径段。
以下示例展示了 EntityLinks-capable 控制器的实现:
@Controller
@ExposesResourceFor(Order.class) (1)
@RequestMapping("/orders") (2)
class OrderController {
@GetMapping (3)
ResponseEntity orders(…) { … }
@GetMapping("{id}") (4)
ResponseEntity order(@PathVariable("id") … ) { … }
}
1 | 该控制器指示它正在暴露实体 Order 的集合和项目资源。 |
2 | 其集合资源暴露在 /orders 下。 |
3 | 该集合资源可以处理 GET 请求。您可以根据需要添加更多处理其他 HTTP 方法的方法。 |
4 | 一个额外的控制器方法,用于处理从属资源,该方法接受路径变量以暴露一个项目资源,即单个 Order 。 |
配置就绪后,当您在 Spring MVC 配置中启用 EntityLinks
@EnableHypermediaSupport
时,可以按如下方式创建指向该控制器的链接:
@Controller
class PaymentController {
private final EntityLinks entityLinks;
PaymentController(EntityLinks entityLinks) { (1)
this.entityLinks = entityLinks;
}
@PutMapping(…)
ResponseEntity payment(@PathVariable Long orderId) {
Link link = entityLinks.linkToItemResource(Order.class, orderId); (2)
…
}
}
1 | 在您的配置中注入由 @EnableHypermediaSupport 提供的 EntityLinks 。 |
2 | 使用 API 通过实体类型而非控制器类来构建链接。 |
如您所见,您可以引用管理 Order
实例的资源,而无需显式引用 OrderController
。
3.5.2. EntityLinks API 详解
从根本上讲,EntityLinks
允许构建 LinkBuilder
s 和 Link
实例,以指向实体类型的集合和项目资源。以 linkFor…
开头的方法将生成 LinkBuilder
实例,供您使用额外的路径段、参数等进行扩展和增强。以 linkTo
开头的方法则生成完全准备好的 Link
实例。
对于集合资源,提供实体类型就足够了,但指向项目资源的链接需要提供一个标识符。这通常看起来像这样:
entityLinks.linkToItemResource(order, order.getId());
如果您发现自己重复调用这些方法,可以将标识符提取步骤抽取到一个可重用的 Function
中,以便在不同的调用中重复使用。
Function<Order, Object> idExtractor = Order::getId; (1)
entityLinks.linkToItemResource(order, idExtractor); (2)
1 | 标识符提取被外部化,以便可以将其保存在字段或常量中。 |
2 | 使用提取器查找链接。 |
TypedEntityLinks
由于控制器实现通常围绕实体类型分组,您会非常频繁地发现在整个控制器类中使用相同的提取函数(详见 EntityLinks API 详细介绍) 。我们可以通过一次获取一个提供提取器的 TypedEntityLinks
实例,进一步集中标识符提取逻辑,这样实际查找时就完全无需处理提取了。
class OrderController {
private final TypedEntityLinks<Order> links;
OrderController(EntityLinks entityLinks) { (1)
this.links = entityLinks.forType(Order::getId); (2)
}
@GetMapping
ResponseEntity<Order> someMethod(…) {
Order order = … // lookup order
Link link = links.linkToItemResource(order); (3)
}
}
1 | 注入一个 EntityLinks 实例。 |
2 | 指明您将使用某个特定的标识符提取函数查找 Order 实例。 |
3 | 根据单独的 Order 实例查找项目资源链接。 |
3.5.3. EntityLinks 作为 SPI
由 @EnableHypermediaSupport
创建的 EntityLinks
实例类型为 DelegatingEntityLinks
,它会依次选取 ApplicationContext
中所有可用的其他 EntityLinks
实现作为 bean。它被注册为主 bean (primary bean),以便在您通常注入 EntityLinks
时,它始终是唯一的注入候选。ControllerEntityLinks
是默认实现,将被包含在设置中,但用户可以自由实现和注册自己的实现。要使这些实现可用于注入的 EntityLinks
实例,只需将您的实现注册为 Spring bean 即可。
@Configuration
class CustomEntityLinksConfiguration {
@Bean
MyEntityLinks myEntityLinks(…) {
return new MyEntityLinks(…);
}
}
此机制可扩展性的一个例子是 Spring Data REST 的 RepositoryEntityLinks
,它使用仓库映射信息创建指向由 Spring Data 仓库支持的资源的链接。同时,它甚至为其他类型的资源暴露了额外的查找方法。如果您想使用这些方法,只需显式注入 RepositoryEntityLinks
。
3.6. 表示模型汇编器
由于将实体映射到表示模型的逻辑必须在多个地方使用,因此创建一个专门负责此操作的类是很有意义的。这种转换包含非常自定义的步骤,但也包含一些样板步骤:
-
模型类的实例化
-
添加一个
rel
为self
的链接,指向正在渲染的资源。
Spring HATEOAS 现在提供了一个 RepresentationModelAssemblerSupport
基类,它有助于减少您需要编写的代码量。以下示例展示了如何使用它:
class PersonModelAssembler extends RepresentationModelAssemblerSupport<Person, PersonModel> {
public PersonModelAssembler() {
super(PersonController.class, PersonModel.class);
}
@Override
public PersonModel toModel(Person person) {
PersonModel resource = createResource(person);
// … do further mapping
return resource;
}
}
createResource(…) 是您编写的代码,用于根据一个 Person 对象实例化一个 PersonModel 对象。它应该只关注设置属性,而不填充 Links 。 |
如上例所示设置类,将为您带来以下好处:
-
有许多
createModelWithId(…)
方法,可让您创建资源的实例,并为其添加一个rel
为self
的Link
。该链接的 href 由配置的控制器的请求映射加上实体的 ID 决定(例如,/people/1
)。 -
资源类型通过反射实例化,并需要一个无参构造函数。如果您想使用专门的构造函数或避免反射的性能开销,可以覆盖
instantiateModel(…)
。
然后,您可以使用汇编器来汇编一个 RepresentationModel
或一个 CollectionModel
。以下示例创建了一个 PersonModel
实例的 CollectionModel
:
Person person = new Person(…);
Iterable<Person> people = Collections.singletonList(person);
PersonModelAssembler assembler = new PersonModelAssembler();
PersonModel model = assembler.toModel(person);
CollectionModel<PersonModel> model = assembler.toCollectionModel(people);
3.7. 表示模型处理器
有时,您需要在超媒体表示被汇编完成后对其进行调整和修改。
一个完美的例子是,您有一个处理订单履行的控制器,但您需要添加与付款相关的链接。
想象一下,您的订单系统生成了这种类型的超媒体:
{
"orderId" : "42",
"state" : "AWAITING_PAYMENT",
"_links" : {
"self" : {
"href" : "http://localhost/orders/999"
}
}
}
您希望添加一个链接以便客户端可以进行付款,但又不想将关于 PaymentController
的细节混入 OrderController
。与其污染您的订单系统细节,不如编写一个像这样的 RepresentationModelProcessor
:
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> { (1)
@Override
public EntityModel<Order> process(EntityModel<Order> model) {
model.add( (2)
Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) //
.expand(model.getContent().getOrderId()));
return model; (3)
}
}
1 | 此处理器将仅应用于 EntityModel<Order> 对象。 |
2 | 通过添加一个无条件链接来操作现有的 EntityModel 对象。 |
3 | 返回 EntityModel ,以便它可以被序列化为请求的媒体类型。 |
向您的应用程序注册处理器:
@Configuration
public class PaymentProcessingApp {
@Bean
PaymentProcessor paymentProcessor() {
return new PaymentProcessor();
}
}
现在,当您发出 Order
的超媒体表示时,客户端会收到以下内容:
{
"orderId" : "42",
"state" : "AWAITING_PAYMENT",
"_links" : {
"self" : {
"href" : "http://localhost/orders/999"
},
"payments" : { (1)
"href" : "/payments/42" (2)
}
}
}
1 | 您会看到 LinkRelation.of("payments") 作为此链接的关系被插入。 |
2 | URI 由处理器提供。 |
这个例子相当简单,但您可以轻松地:
-
使用
WebMvcLinkBuilder
或WebFluxLinkBuilder
构建指向您的PaymentController
的动态链接。 -
注入所需的任何服务,以便根据状态有条件地添加其他链接(例如
cancel
、amend
)。 -
利用 Spring Security 等横切服务,根据当前用户的上下文添加、移除或修改链接。
此外,在此示例中,PaymentProcessor
修改了提供的 EntityModel<Order>
。您也有权将其替换为另一个对象。但请注意,API 要求返回类型等于输入类型。
3.7.1. 处理空集合模型
为了找到正确的 RepresentationModelProcessor
实例集来调用一个 RepresentationModel
实例,调用基础设施会详细分析注册的 RepresentationModelProcessor
的泛型声明。对于 CollectionModel
实例,这包括检查底层集合的元素,因为在运行时,唯一的模型实例不暴露泛型信息(由于 Java 的类型擦除)。这意味着,默认情况下,对于空集合模型不会调用 RepresentationModelProcessor
实例。为了仍然允许基础设施正确地推断出载荷类型,您可以在开始时就用一个显式的回退载荷类型初始化空的 CollectionModel
实例,或者通过调用 CollectionModel.withFallbackType(…)
来注册它。详情请参见集合资源表示模型。
3.8. 使用 LinkRelationProvider
API
构建链接时,通常需要确定用于链接的关系类型 (rel)。在大多数情况下,关系类型直接与(领域)类型关联。我们将查找关系类型的详细算法封装在 LinkRelationProvider
API 之后,该 API 允许您确定单个资源和集合资源的关系类型。查找关系类型的算法如下:
-
如果类型使用
@Relation
注解,则使用注解中配置的值。 -
如果不是,则默认使用未首字母大写的简单类名,并为集合
rel
附加List
。 -
如果在 classpath 中存在 EVO inflector JAR,则使用复数化算法提供的单个资源
rel
的复数形式。 -
使用
@ExposesResourceFor
注解的@Controller
类(详情请参见使用 EntityLinks 接口)透明地查找注解中配置的类型的关系类型,这样您就可以使用LinkRelationProvider.getItemResourceRelFor(MyController.class)
并获取暴露的领域类型的关系类型。
当您使用 @EnableHypermediaSupport
时,LinkRelationProvider
会自动暴露为 Spring bean。您可以通过实现该接口并将其暴露为 Spring bean 来插入自定义提供者。
4. 媒体类型
4.1. HAL – 超文本应用语言
JSON 超文本应用语言或 HAL 是最简单、最广泛采用的超媒体媒体类型之一,特别是在不讨论特定 Web 技术栈时。
它是 Spring HATEOAS 采用的第一个基于规范的媒体类型。
4.1.1. 构建 HAL 表示模型
从 Spring HATEOAS 1.1 版本开始,我们提供了一个专门的 HalModelBuilder
,它允许通过 HAL 惯用 API 创建 RepresentationModel
实例。以下是其基本假设:
-
一个 HAL 表示可以由任意对象(实体)支持,该对象构建表示中包含的领域字段。
-
表示可以通过各种嵌入式文档进行丰富,这些文档可以是任意对象或其本身就是 HAL 表示(即包含嵌套的嵌入项和链接)。
-
特定的 HAL 模式(例如预览)可以直接在 API 中使用,以便设置表示的代码读起来就像您按照这些惯用方式描述 HAL 表示一样。
以下是使用该 API 的示例:
// An order
var order = new Order(…); (1)
// The customer who placed the order
var customer = customer.findById(order.getCustomerId());
var customerLink = Link.of("/orders/{id}/customer") (2)
.expand(order.getId())
.withRel("customer");
var additional = …
var model = HalModelBuilder.halModelOf(order)
.preview(new CustomerSummary(customer)) (3)
.forLink(customerLink) (4)
.embed(additional) (5)
.link(Link.of(…, IanaLinkRelations.SELF));
.build();
1 | 我们设置了一个领域类型。在此示例中,是一个订单,与下订单的客户有关系。 |
2 | 我们准备一个指向暴露客户详细信息的资源的链接。 |
3 | 我们通过提供应该渲染在 _embeddable 子句中的载荷来开始构建预览。 |
4 | 我们通过提供目标链接来结束预览。它会透明地添加到 _links 对象中,并且其链接关系用作上一步提供的对象的键。 |
5 | 其他对象可以添加到 _embedded 下显示。它们列出的键派生自对象的关系设置。它们可以通过 @Relation 或专门的 LinkRelationProvider 进行定制(详情请参见使用 LinkRelationProvider API)。 |
{
"_links" : {
"self" : { "href" : "…" }, (1)
"customer" : { "href" : "/orders/4711/customer" } (2)
},
"_embedded" : {
"customer" : { … }, (3)
"additional" : { … } (4)
}
}
1 | 显式提供的 self 链接。 |
2 | 通过 ….preview(…).forLink(…) 透明添加的 customer 链接。 |
3 | 提供的预览对象。 |
4 | 通过显式 ….embed(…) 添加的附加元素。 |
在 HAL 中,_embedded
也用于表示顶级集合。它们通常分组在从对象类型派生的链接关系下。例如,一个订单列表在 HAL 中看起来像这样:
{
"_embedded" : {
"order : [
… (1)
]
}
}
1 | 单个订单文档在此处。 |
创建这样的表示很简单,如下所示:
Collection<Order> orders = …;
HalModelBuilder.emptyHalDocument()
.embed(orders);
也就是说,如果订单为空,则无法推导出应出现在 _embedded
中的链接关系,因此如果集合为空,文档将保持为空。
如果您更喜欢显式地通信一个空集合,可以将类型传递给接受 Collection
的 ….embed(…)
方法的重载。如果传递给该方法的集合为空,这将导致渲染一个字段,其链接关系派生自给定的类型。
HalModelBuilder.emptyHalModel()
.embed(Collections.emptyList(), Order.class);
// or
.embed(Collections.emptyList(), LinkRelation.of("orders"));
将创建以下更显式的表示:
{
"_embedded" : {
"orders" : []
}
}
4.1.2. 配置链接渲染
在 HAL 中,_links
条目是一个 JSON 对象。属性名称是链接关系 (link relations),每个值是一个链接对象或一个链接对象数组。
对于具有两个或更多链接的给定链接关系,规范对表示方式明确:
{
"_links": {
"item": [
{ "href": "https://myhost/cart/42" },
{ "href": "https://myhost/inventory/12" }
]
},
"customer": "Dave Matthews"
}
但是,如果给定关系只有一个链接,则规范是模糊的。您可以将其渲染为单个对象或单项数组。
默认情况下,Spring HATEOAS 使用最简洁的方法,并将单个链接关系渲染为:
{
"_links": {
"item": { "href": "https://myhost/inventory/12" }
},
"customer": "Dave Matthews"
}
一些用户在消费 HAL 时更喜欢不切换数组和对象。他们更喜欢这种类型的渲染:
{
"_links": {
"item": [{ "href": "https://myhost/inventory/12" }]
},
"customer": "Dave Matthews"
}
如果您希望定制此策略,只需在您的应用程序配置中注入一个 HalConfiguration
bean。有多种选择。
@Bean
public HalConfiguration globalPolicy() {
return new HalConfiguration() //
.withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 | 覆盖 Spring HATEOAS 的默认设置,将所有单链接关系渲染为数组。 |
如果您只希望覆盖某些特定的链接关系,可以创建如下所示的 HalConfiguration
bean:
@Bean
public HalConfiguration linkRelationBasedPolicy() {
return new HalConfiguration() //
.withRenderSingleLinksFor( //
IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1)
.withRenderSingleLinksFor( //
LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2)
}
1 | 始终将 item 链接关系渲染为数组。 |
2 | 当只有一个链接时,将 prev 链接关系渲染为对象。 |
如果这些都不符合您的需求,可以使用 Ant 风格的路径模式:
@Bean
public HalConfiguration patternBasedPolicy() {
return new HalConfiguration() //
.withRenderSingleLinksFor( //
"http*", RenderSingleLinks.AS_ARRAY); (1)
}
1 | 将所有以 http 开头的链接关系渲染为数组。 |
基于模式的方法使用 Spring 的 AntPathMatcher 。 |
所有这些 HalConfiguration
的 wither 方法都可以组合起来形成一个全面的策略。务必广泛测试您的 API,以避免意外。
4.1.3. 链接标题国际化
HAL 为其链接对象定义了一个 title
属性。这些标题可以使用 Spring 的资源包抽象和名为 rest-messages
的资源包进行填充,以便客户端可以在其 UI 中直接使用它们。此资源包将自动设置,并在 HAL 链接序列化期间使用。
要为链接定义标题,使用键模板 _links.$relationName.title
,如下所示:
rest-messages.properties
示例_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout
这将产生以下 HAL 表示:
{
"_links" : {
"cancel" : {
"href" : "…"
"title" : "Cancel order"
},
"payment" : {
"href" : "…"
"title" : "Proceed to checkout"
}
}
}
4.1.4. 使用 CurieProvider
API
Web Linking RFC 描述了注册和扩展链接关系类型。注册关系是注册到 IANA 链接关系类型注册表的知名字符串。扩展 rel
URI 可用于不希望注册关系类型的应用程序。每个 URI 唯一标识关系类型。rel
URI 可以序列化为紧凑 URI 或 Curie。例如,如果 ex
被定义为 example.com/rels/{rel}
,则 ex:persons
的 Curie 代表链接关系类型 example.com/rels/persons
。如果使用了 Curie,则基本 URI 必须存在于响应范围内。
默认 RelProvider
创建的 rel
值是扩展关系类型,因此必须是 URI,这可能会带来很多开销。CurieProvider
API 负责处理这个问题:它允许您将基本 URI 定义为 URI 模板,并定义代表该基本 URI 的前缀。如果存在 CurieProvider
,则 RelProvider
会在所有未注册到 IANA 的 rel
值前加上 curie 前缀。此外,一个 curies
链接会自动添加到 HAL 资源中。
以下配置定义了一个默认的 curie 提供者:
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type= {HypermediaType.HAL})
public class Config {
@Bean
public CurieProvider curieProvider() {
return new DefaultCurieProvider("ex", new UriTemplate("https://www.example.com/rels/{rel}"));
}
}
请注意,现在 ex:
前缀会自动出现在所有未在 IANA 注册的 rel 值之前,例如 ex:orders
。客户端可以使用 curies
链接将 curie 解析为其完整形式。以下示例展示了如何操作:
{
"_links": {
"self": {
"href": "https://myhost/person/1"
},
"curies": {
"name": "ex",
"href": "https://example.com/rels/{rel}",
"templated": true
},
"ex:orders": {
"href": "https://myhost/person/1/orders"
}
},
"firstname": "Dave",
"lastname": "Matthews"
}
由于 CurieProvider
API 的目的是允许自动创建 curie,因此每个应用程序范围只能定义一个 CurieProvider
bean。
4.2. HAL-FORMS
HAL-FORMS“看起来像 HAL”。然而,重要的是要记住 HAL-FORMS 与 HAL 不同——两者绝不能被视为可互换。
HAL-FORMS 规范
要启用此媒体类型,请在您的代码中添加以下配置:
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {
}
每当客户端提供带有 application/prs.hal-forms+json
的 Accept
头时,您可以期待类似这样的内容:
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/employees/1"
}
},
"_templates" : {
"default" : {
"method" : "put",
"properties" : [ {
"name" : "firstName",
"required" : true
}, {
"name" : "lastName",
"required" : true
}, {
"name" : "role",
"required" : true
} ]
},
"partiallyUpdateEmployee" : {
"method" : "patch",
"properties" : [ {
"name" : "firstName",
"required" : false
}, {
"name" : "lastName",
"required" : false
}, {
"name" : "role",
"required" : false
} ]
}
}
}
请查看 HAL-FORMS 规范以了解 _templates 属性的详细信息。阅读Affordances API以使用这些额外元数据增强您的控制器。
至于单项 (EntityModel
) 和聚合根集合 (CollectionModel
),Spring HATEOAS 将它们渲染得与 HAL 文档相同。
4.2.1. 定义 HAL-FORMS 元数据
HAL-FORMS 允许描述每个表单字段的标准。Spring HATEOAS 允许通过塑造输入和输出类型的模型类型并在其上使用注解来定制这些标准。
每个模板将定义以下属性:
属性 | 描述 |
---|---|
|
服务器期望接收的媒体类型。仅当指向的控制器方法暴露了 |
|
提交模板时使用的 HTTP 方法。 |
|
提交表单的目标 URI。仅当 affordance 目标与其声明的链接不同时,才渲染此属性。 |
|
显示模板时的人类可读标题。 |
|
要与表单一起提交的所有属性(见下文)。 |
每个属性将定义以下属性:
属性 | 描述 |
---|---|
|
如果属性没有 setter 方法,则设置为 |
|
可以通过在字段或类型上使用 JSR-303 的 |
|
可以使用 JSR-303 的 |
|
属性允许的最大值。源自 JSR-303 的 |
|
属性允许的最大长度值。源自 Hibernate Validator 的 |
|
属性允许的最小值。源自 JSR-303 的 |
|
属性允许的最小长度值。源自 Hibernate Validator 的 |
|
提交表单时可供选择的值。详情请参见为属性定义 HAL-FORMS 选项。 |
|
渲染表单输入时使用的用户可读提示。详情请参见属性提示。 |
|
一个用户可读的占位符,用于提供期望格式的示例。定义占位符的方式遵循属性提示,但使用后缀 |
|
从显式 |
对于您无法手动注解的类型,可以通过 application context 中存在的 HalFormsConfiguration
bean 注册自定义模式。
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
}
}
此设置将导致 CreditCardNumber
类型的表示模型属性的 HAL-FORMS 模板属性声明一个值为 [0-9]{16}
的 regex
字段。
为属性定义 HAL-FORMS 选项
对于其值应该与特定超集值匹配的属性,HAL-FORMS 在属性定义中定义了 options
子文档。通过 HalFormsConfiguration
的 withOptions(…)
可以描述某个属性可用的选项,该方法接受一个指向类型属性的指针和一个创建函数,用于将 PropertyMetadata
转换为 HalFormsOptions
实例。
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.withOptions(Order.class, "shippingMethod" metadata ->
HalFormsOptions.inline("FedEx", "DHL"));
}
}
请注意我们如何将选项值 FedEx
和 DHL
设置为 Order.shippingMethod
属性的可选项。或者,HalFormsOptions.remote(…)
可以指向提供动态值的远程资源。有关选项设置的更多约束,请参阅规范或 HalFormsOptions
的 Javadoc。
4.2.2. 表单属性国际化
HAL-FORMS 包含一些旨在供人类解释的属性,如模板标题或属性提示。这些属性可以使用 Spring 的资源包支持和 Spring HATEOAS 默认配置的 rest-messages
资源包进行定义和国际化。
模板标题
要定义模板标题,请使用以下模式:_templates.$affordanceName.title
。请注意,在 HAL-FORMS 中,如果模板是唯一的,其名称为 default
。这意味着您通常需要使用 affordance 描述的本地或完全限定的输入类型名称来限定键。
_templates.default.title=Some title (1)
_templates.putEmployee.title=Create employee (2)
Employee._templates.default.title=Create employee (3)
com.acme.Employee._templates.default.title=Create employee (4)
1 | 使用 default 作为键的全局标题定义。 |
2 | 使用实际 affordance 名称作为键的全局标题定义。除非在创建 affordance 时明确定义,否则此名称默认为创建 affordance 时指向的方法的名称。 |
3 | 本地定义的标题,将应用于所有名为 Employee 的类型。 |
4 | 使用完全限定类型名称的标题定义。 |
使用实际 affordance 名称的键优先于默认键。 |
属性提示
属性提示也可以通过 Spring HATEOAS 自动配置的 rest-messages
资源包解析。键可以全局、本地或完全限定定义,并且需要在实际属性键后连接 ._prompt
。
email
属性的提示firstName._prompt=Firstname (1)
Employee.firstName._prompt=Firstname (2)
com.acme.Employee.firstName._prompt=Firstname (3)
1 | 所有名为 firstName 的属性都将渲染为“Firstname”,与其在哪个类型中声明无关。 |
2 | 名为 Employee 的类型中的 firstName 属性将提示为“Firstname”。 |
3 | com.acme.Employee 的 firstName 属性将被分配提示“Firstname”。 |
4.2.3. 完整示例
让我们看一个结合了上述所有定义和定制属性的示例代码。客户的 RepresentationModel
可能看起来像这样:
class CustomerRepresentation
extends RepresentationModel<CustomerRepresentation> {
String name;
LocalDate birthdate; (1)
@Pattern(regex = "[0-9]{16}") String ccn; (2)
@Email String email; (3)
}
1 | 我们定义了一个 LocalDate 类型的 birthdate 属性。 |
2 | 我们期望 ccn 遵守正则表达式。 |
3 | 我们使用 JSR-303 @Email 注解将 email 定义为电子邮件。 |
请注意,此类型不是领域类型。它是特意设计用来捕获各种潜在的无效输入,以便可以一次性拒绝潜在错误的字段值。
接下来,让我们看看控制器如何使用该模型:
@Controller
class CustomerController {
@PostMapping("/customers")
EntityModel<?> createCustomer(@RequestBody CustomerRepresentation payload) { (1)
// …
}
@GetMapping("/customers")
CollectionModel<?> getCustomers() {
CollectionModel<?> model = …;
CustomerController controller = methodOn(CustomerController.class);
model.add(linkTo(controller.getCustomers()).withSelfRel() (2)
.andAfford(controller.createCustomer(null)));
return ResponseEntity.ok(model);
}
}
1 | 声明了一个控制器方法,用于在使用 POST 请求到 /customers 时,将请求体绑定到上面定义的表示模型。 |
2 | 向 /customers 发出 GET 请求会准备一个模型,为其添加一个 self 链接,并在此链接上额外声明一个指向映射到 POST 的控制器方法的 affordance。这将导致构建一个 affordance 模型,该模型(取决于最终要渲染的媒体类型)将被转换为媒体类型特定的格式。 |
接下来,让我们添加一些额外的元数据,使表单对人类更易访问:
rest-messages.properties
中声明的附加属性。CustomerRepresentation._template.createCustomer.title=Create customer (1)
CustomerRepresentation.ccn._prompt=Credit card number (2)
CustomerRepresentation.ccn._placeholder=1234123412341234 (2)
1 | 我们为通过指向 createCustomer(…) 方法创建的模板定义了一个显式标题。 |
2 | 我们明确为 CustomerRepresentation 模型的 ccn 属性指定了一个提示和占位符。 |
如果客户端现在使用 Accept
头 application/prs.hal-forms+json
向 /customers
发出 GET
请求,响应的 HAL 文档将被扩展为一个 HAL-FORMS 文档,包含以下 _templates
定义:
{
…,
"_templates" : {
"default" : { (1)
"title" : "Create customer", (2)
"method" : "post", (3)
"properties" : [ {
"name" : "name",
"required" : true,
"type" : "text" (4)
} , {
"name" : "birthdate",
"required" : true,
"type" : "date" (4)
} , {
"name" : "ccn",
"prompt" : "Credit card number", (5)
"placeholder" : "1234123412341234" (5)
"required" : true,
"regex" : "[0-9]{16}", (6)
"type" : "text"
} , {
"name" : "email",
"prompt" : "Email",
"required" : true,
"type" : "email" (7)
} ]
}
}
}
1 | 暴露了一个名为 default 的模板。它的名称是 default ,因为它定义的唯一模板,规范要求使用该名称。如果附加了多个模板(通过声明额外的 affordances),它们将分别以它们指向的方法命名。 |
2 | 模板标题源自资源包中定义的值。请注意,根据随请求发送的 Accept-Language 头和可用性,可能会返回不同的值。 |
3 | method 属性的值源自 derive affordance 的方法的映射。 |
4 | type 属性的值 text 源自属性的类型 String 。birthdate 属性也同样适用,但结果是 date 。 |
5 | ccn 属性的提示和占位符也源自资源包。 |
6 | ccn 属性的 @Pattern 声明暴露为模板属性的 regex 属性。 |
7 | email 属性上的 @Email 注解已转换为相应的 type 值。 |
HAL-FORMS 模板会被例如 HAL Explorer 考虑,它会自动从这些描述渲染 HTML 表单。
4.3. HTTP 问题详情
Problem Details for HTTP APIs 是一种媒体类型,用于在 HTTP 响应中携带机器可读的错误详细信息,以避免为 HTTP API 定义新的错误响应格式。
HTTP Problem Details 定义了一组 JSON 属性,这些属性携带附加信息以向 HTTP 客户端描述错误详情。有关这些属性的更多详情,请参阅 RFC 文档的相关部分。
您可以在 Spring MVC 控制器中使用 Problem
媒体类型领域类型创建这样的 JSON 响应:
Problem
类型报告问题详情@RestController
class PaymentController {
@PutMapping
ResponseEntity<?> issuePayment(@RequestBody PaymentRequest request) {
PaymentResult result = payments.issuePayment(request.orderId, request.amount);
if (result.isSuccess()) {
return ResponseEntity.ok(result);
}
String title = messages.getMessage("payment.out-of-credit");
String detail = messages.getMessage("payment.out-of-credit.details", //
new Object[] { result.getBalance(), result.getCost() });
Problem problem = Problem.create() (1)
.withType(OUT_OF_CREDIT_URI) //
.withTitle(title) (2)
.withDetail(detail) //
.withInstance(PAYMENT_ERROR_INSTANCE.expand(result.getPaymentId())) //
.withProperties(map -> { (3)
map.put("balance", result.getBalance());
map.put("accounts", Arrays.asList( //
ACCOUNTS.expand(result.getSourceAccountId()), //
ACCOUNTS.expand(result.getTargetAccountId()) //
));
});
return ResponseEntity.status(HttpStatus.FORBIDDEN) //
.body(problem);
}
}
1 | 首先,使用暴露的工厂方法创建一个 Problem 实例。 |
2 | 您可以为媒体类型定义的默认属性定义值,例如使用 Spring 的国际化功能(见上文)定义类型 URI、标题和详细信息。 |
3 | 自定义属性可以通过 Map 或显式对象添加(见下文)。 |
要为自定义属性使用专用对象,请声明一个类型,创建并填充其实例,然后通过 ….withProperties(…)
或在实例创建时通过 Problem.create(…)
将其传递给 Problem
实例。
class AccountDetails {
int balance;
List<URI> accounts;
}
problem.withProperties(result.getDetails());
// or
Problem.create(result.getDetails());
这将产生如下所示的响应:
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345",
"/account/67890"]
}
4.4. Collection+JSON
Collection+JSON 是一个 JSON 规范,注册为 IANA 批准的媒体类型 application/vnd.collection+json
。
Collection+JSON 是一种基于 JSON 的读/写超媒体类型,旨在支持简单集合的管理和查询。
Collection+JSON 规范
Collection+JSON 提供了一种统一的方式来表示单项资源和集合。要启用此媒体类型,请在您的代码中添加以下配置:
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {
}
此配置将使您的应用程序响应带有 Accept
头 application/vnd.collection+json
的请求,如下所示。
以下来自规范的示例展示了一个单项:
{
"collection": {
"version": "1.0",
"href": "https://example.org/friends/", (1)
"links": [ (2)
{
"rel": "feed",
"href": "https://example.org/friends/rss"
},
{
"rel": "queries",
"href": "https://example.org/friends/?queries"
},
{
"rel": "template",
"href": "https://example.org/friends/?template"
}
],
"items": [ (3)
{
"href": "https://example.org/friends/jdoe",
"data": [ (4)
{
"name": "fullname",
"value": "J. Doe",
"prompt": "Full Name"
},
{
"name": "email",
"value": "[email protected]",
"prompt": "Email"
}
],
"links": [ (5)
{
"rel": "blog",
"href": "https://examples.org/blogs/jdoe",
"prompt": "Blog"
},
{
"rel": "avatar",
"href": "https://examples.org/images/jdoe",
"prompt": "Avatar",
"render": "image"
}
]
}
]
}
}
1 | self 链接存储在文档的 href 属性中。 |
2 | 文档顶部的 links 部分包含集合级链接(减去 self 链接)。 |
3 | items 部分包含数据的集合。由于这是一个单项文档,因此它只有一个条目。 |
4 | data 部分包含实际内容。它由属性组成。 |
5 | 项目的独立 links 。 |
前面的片段取自规范。当 Spring HATEOAS 渲染一个
|
渲染资源集合时,文档几乎相同,只是 items
JSON 数组中会有多个条目,每个条目对应一个。
Spring HATEOAS 更具体地会:
-
将整个集合的
self
链接放入顶层href
属性中。 -
CollectionModel
的链接(减去self
)将放入顶层links
中。 -
每个项目级
href
将包含来自CollectionModel.content
集合中每个条目对应的self
链接。 -
每个项目级
links
将包含来自CollectionModel.content
的每个条目的所有其他链接。
4.5. UBER - Uniform Basis for Exchanging Representations
UBER 是一种实验性的 JSON 规范。
UBER 文档格式是一种极简的读/写超媒体类型,旨在支持简单的状态转移和特设的基于超媒体的转换。
UBER 规范
UBER 提供了一种统一的方式来表示单项资源和集合。要启用此媒体类型,请在您的代码中添加以下配置:
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {
}
此配置将使您的应用程序响应使用 Accept
头 application/vnd.amundsen-uber+json
的请求,如下所示:
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "self" ],
"url" : "/employees/1"
}, {
"name" : "employee",
"data" : [ {
"name" : "role",
"value" : "ring bearer"
}, {
"name" : "name",
"value" : "Frodo"
} ]
} ]
}
}
此媒体类型及其规范本身仍在开发中。如果您在使用过程中遇到问题,请随时提交工单。
UBER 媒体类型与网约车公司 Uber Technologies Inc. 没有任何关联。 |
4.6. ALPS - Application-Level Profile Semantics
ALPS 是一种媒体类型,用于提供关于另一个资源的基于配置文件的元数据。
ALPS 文档可以用作配置文件,解释具有应用无关媒体类型(如 HTML, HAL, Collection+JSON, Siren 等)的文档的应用语义。这提高了配置文件文档在不同媒体类型之间的可重用性。
ALPS 规范
ALPS 无需特殊激活。相反,您可以“构建”一个 Alps
记录并从 Spring MVC 或 Spring WebFlux web 方法返回它,如下所示:
Alps
记录@GetMapping(value = "/profile", produces = ALPS_JSON_VALUE)
Alps profile() {
return Alps.alps() //
.doc(doc() //
.href("https://example.org/samples/full/doc.html") //
.value("value goes here") //
.format(Format.TEXT) //
.build()) //
.descriptor(getExposedProperties(Employee.class).stream() //
.map(property -> Descriptor.builder() //
.id("class field [" + property.getName() + "]") //
.name(property.getName()) //
.type(Type.SEMANTIC) //
.ext(Ext.builder() //
.id("ext [" + property.getName() + "]") //
.href("https://example.org/samples/ext/" + property.getName()) //
.value("value goes here") //
.build()) //
.rt("rt for [" + property.getName() + "]") //
.descriptor(Collections.singletonList(Descriptor.builder().id("embedded").build())) //
.build()) //
.collect(Collectors.toList()))
.build();
}
-
此示例利用
PropertyUtils.getExposedProperties()
提取有关领域对象属性的元数据。
此片段插入了测试数据。它产生了如下所示的 JSON:
{ "version": "1.0", "doc": { "format": "TEXT", "href": "https://example.org/samples/full/doc.html", "value": "value goes here" }, "descriptor": [ { "id": "class field [name]", "name": "name", "type": "SEMANTIC", "descriptor": [ { "id": "embedded" } ], "ext": { "id": "ext [name]", "href": "https://example.org/samples/ext/name", "value": "value goes here" }, "rt": "rt for [name]" }, { "id": "class field [role]", "name": "role", "type": "SEMANTIC", "descriptor": [ { "id": "embedded" } ], "ext": { "id": "ext [role]", "href": "https://example.org/samples/ext/role", "value": "value goes here" }, "rt": "rt for [role]" } ] }
您可以选择手动编写字段,而不是“自动”将每个字段链接到领域对象的字段。也可以使用 Spring Framework 的消息包和 MessageSource
接口。这使您能够将这些值委托给特定语言环境的消息包,甚至对元数据进行国际化。
4.7. 社区主导的媒体类型
得益于创建自定义媒体类型的能力,有一些社区主导的工作来构建额外的媒体类型。
4.7.1. JSON:API
<dependency>
<groupId>com.toedter</groupId>
<artifactId>spring-hateoas-jsonapi</artifactId>
<version>{see project page for current version}</version>
</dependency>
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'
如果你想要快照版本,请访问项目页面获取更多详情。
4.7.2. Siren
-
媒体类型标识:
application/vnd.siren+json
-
项目负责人:Ingo Griebsch
<dependency>
<groupId>de.ingogriebsch.hateoas</groupId>
<artifactId>spring-hateoas-siren</artifactId>
<version>{see project page for current version}</version>
<scope>compile</scope>
</dependency>
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'
4.8. 注册自定义媒体类型
Spring HATEOAS 允许您通过 SPI 集成自定义媒体类型。这种实现的构建块包括
-
某种形式的 Jackson
ObjectMapper
定制。最简单的情况是实现一个 JacksonModule
。 -
一个
LinkDiscoverer
实现,以便客户端支持能够检测表示中的链接。 -
少量的基础设施配置,这将允许 Spring HATEOAS 找到并使用自定义实现。
4.8.1. 自定义媒体类型配置
Spring HATEOAS 通过扫描应用上下文(application context)来查找 HypermediaMappingInformation
接口的任何实现,从而获取自定义媒体类型的实现。每个媒体类型都必须实现此接口以便
-
应用于
WebClient
、WebTestClient
或RestTemplate
实例。 -
支持 Spring Web MVC 和 Spring WebFlux 控制器提供该媒体类型。
定义您自己的媒体类型可以像这样简单
@Configuration
public class MyMediaTypeConfiguration implements HypermediaMappingInformation {
@Override
public List<MediaType> getMediaTypes() {
return Collections.singletonList(MediaType.parseMediaType("application/vnd-acme-media-type")); (1)
}
@Override
public Module getJacksonModule() {
return new Jackson2MyMediaTypeModule(); (2)
}
@Bean
MyLinkDiscoverer myLinkDiscoverer() {
return new MyLinkDiscoverer(); (3)
}
}
1 | 配置类返回它支持的媒体类型。这适用于服务器端和客户端场景。 |
2 | 它覆盖 getJacksonModule() 方法以提供自定义序列化器来创建特定于媒体类型的表示。 |
3 | 它还声明了一个自定义的 LinkDiscoverer 实现,以提供进一步的客户端支持。 |
Jackson 模块通常声明用于表示模型类型 RepresentationModel
、EntityModel
、CollectionModel
和 PagedModel
的 Serializer
和 Deserializer
实现。如果需要进一步定制 Jackson ObjectMapper
(例如自定义的 HandlerInstantiator
),您可以选择覆盖 configureObjectMapper(…)
方法。
以前版本的参考文档提到过实现 |
4.8.2. 建议
实现媒体类型表示的首选方式是提供一个与预期格式匹配的类型层次结构,并且可以直接由 Jackson 进行序列化。在为 RepresentationModel
注册的 Serializer
和 Deserializer
实现中,将实例转换为特定于媒体类型的模型类型,然后查找用于这些类型的 Jackson 序列化器。
默认支持的媒体类型使用与第三方实现相同的配置机制。因此值得研究 mediatype
包中的实现。请注意,内置的媒体类型实现将其配置类保留为包私有(package private),因为它们是通过 @EnableHypermediaSupport
激活的。自定义实现可能应该将其改为 public,以确保用户可以从他们的应用程序包中导入这些配置类。
5. 配置
本节介绍如何配置 Spring HATEOAS。
5.1. 使用 @EnableHypermediaSupport
为了让 RepresentationModel
的子类型按照各种超媒体表示类型的规范进行渲染,您可以通过 @EnableHypermediaSupport
激活对特定超媒体表示格式的支持。该注解接受一个 HypermediaType
枚举作为其参数。目前,我们支持 HAL 以及默认渲染。使用该注解将触发以下行为:
-
它注册必要的 Jackson 模块,以便以特定于超媒体的格式渲染
EntityModel
和CollectionModel
。 -
如果 classpath 中存在 JSONPath,它会自动注册一个
LinkDiscoverer
实例,以便在纯 JSON 表示中按其rel
查找链接(参见 使用LinkDiscoverer
实例)。 -
默认情况下,它会启用 entity links,并自动查找
EntityLinks
实现并将它们捆绑到一个DelegatingEntityLinks
实例中,您可以对其进行 autowire。 -
它自动查找
ApplicationContext
中的所有RelProvider
实现,并将它们捆绑到一个DelegatingRelProvider
实例中,您可以对其进行 autowire。它注册提供者来考虑域类型上的@Relation
以及 Spring MVC 控制器。如果 classpath 中存在 EVO inflector,集合rel
值将使用该库中实现的复数化算法推导得到(参见 [spis.rel-provider])。
5.1.1. 显式启用对特定 Web 栈的支持
默认情况下,@EnableHypermediaSupport
会通过反射检测您正在使用的 Web 应用程序栈,并挂接到为这些栈注册的 Spring 组件中,以启用对超媒体表示的支持。但是,在某些情况下,您可能只想显式激活对特定栈的支持。例如,如果您的基于 Spring WebMVC 的应用程序使用 WebFlux 的 WebClient
发起对外请求,并且该 WebClient
不应处理超媒体元素,您可以通过在配置中显式声明 WebMVC 来限制要启用的功能。
@EnableHypermediaSupport(…, stacks = WebStack.WEBMVC)
class MyHypermediaConfiguration { … }
6. 客户端支持
本节介绍 Spring HATEOAS 对客户端的支持。
6.1. Traverson
Spring HATEOAS 提供了一个用于客户端服务遍历的 API。它受到了 Traverson JavaScript 库的启发。以下示例展示了如何使用它
Map<String, Object> parameters = new HashMap<>();
parameters.put("user", 27);
Traverson traverson = new Traverson(URI.create("http://localhost:8080/api/"), MediaTypes.HAL_JSON);
String name = traverson
.follow("movies", "movie", "actor").withTemplateParameters(parameters)
.toObject("$.name");
您可以设置一个 Traverson
实例,通过将其指向一个 REST 服务器并配置您想作为 Accept
头发送的媒体类型。然后,您可以定义您想发现和跟踪的关系名(relation names)。关系名可以是简单的名称,也可以是 JSONPath 表达式(以 $
开头)。
然后,该示例将一个参数 map 传递给 Traverson
实例。这些参数用于展开在遍历过程中找到的 URI(这些 URI 是模板化的)。遍历通过访问最终遍历结果的表示来结束。在前面的示例中,我们评估一个 JSONPath 表达式来访问 actor 的名字。
前面的示例是遍历的最简单版本,其中 rel
值是字符串,并且在每个 hop(跳转)中应用相同的模板参数。
有更多选项可以在每个级别定制模板参数。以下示例展示了这些选项。
ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};
EntityModel<Item> itemResource = traverson.//
follow(rel("items").withParameter("projection", "noImages")).//
follow("$._embedded.items[0]._links.self.href").//
toObject(resourceParameterizedTypeReference);
静态的 rel(…)
函数是定义单个 Hop
的便捷方式。使用 .withParameter(key, value)
可以轻松指定 URI 模板变量。
.withParameter() 返回一个新的 Hop 对象,该对象支持链式调用。您可以随意将任意数量的 .withParameter 调用串联起来。结果是一个单独的 Hop 定义。以下示例展示了一种方法 |
ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};
Map<String, Object> params = Collections.singletonMap("projection", "noImages");
EntityModel<Item> itemResource = traverson.//
follow(rel("items").withParameters(params)).//
follow("$._embedded.items[0]._links.self.href").//
toObject(resourceParameterizedTypeReference);
您还可以加载整个参数 Map
,通过使用 .withParameters(Map)
方法。
follow() 方法支持链式调用,这意味着您可以串联多个 hop,如前面的示例所示。您可以放入多个基于字符串的 rel 值(follow("items", "item") ),或者一个带有特定参数的单个 hop。 |
6.1.1. EntityModel<T>
与 CollectionModel<T>
到目前为止展示的示例演示了如何规避 Java 的类型擦除,并将单个 JSON 格式的资源转换为一个 EntityModel<Item>
对象。但是,如果您得到一个像 \_embedded
HAL 集合那样的集合怎么办?您只需稍作调整即可实现,如下面的示例所示
CollectionModelType<Item> collectionModelType =
TypeReferences.CollectionModelType<Item>() {};
CollectionModel<Item> itemResource = traverson.//
follow(rel("items")).//
toObject(collectionModelType);
与获取单个资源不同,这个示例将一个集合反序列化到 CollectionModel
中。
6.2. 使用 LinkDiscoverer
实例
在使用支持超媒体的表示时,一个常见的任务是查找包含特定关系类型(relation type)的链接。Spring HATEOAS 为默认表示渲染或开箱即用的 HAL 提供了基于 JSONPath 的 LinkDiscoverer
接口实现。使用 @EnableHypermediaSupport
时,我们会自动将一个支持配置的超媒体类型的实例暴露为一个 Spring bean。
或者,您可以按如下方式设置和使用一个实例:
String content = "{'_links' : { 'foo' : { 'href' : '/foo/bar' }}}";
LinkDiscoverer discoverer = new HalLinkDiscoverer();
Link link = discoverer.findLinkWithRel("foo", content);
assertThat(link.getRel(), is("foo"));
assertThat(link.getHref(), is("/foo/bar"));
6.3. 配置 WebClient 实例
如果您需要配置一个 WebClient
以便与超媒体交互,这很容易。获取一个 HypermediaWebClientConfigurer
实例,如下所示:
WebClient
@Bean
WebClient.Builder hypermediaWebClient(HypermediaWebClientConfigurer configurer) { (1)
return configurer.registerHypermediaTypes(WebClient.builder()); (2)
}
1 | 在您的 @Configuration 类中,获取 Spring HATEOAS 注册的 HypermediaWebClientConfigurer bean 的一个副本。 |
2 | 创建 WebClient.Builder 后,使用该 configurer 来注册超媒体类型。 |
HypermediaWebClientConfigurer 的作用是向 WebClient.Builder 注册所有正确的编码器和解码器。要使用它,您需要将该 builder 注入到应用程序的某个位置,并运行 build() 方法来生成一个 WebClient 实例。 |
如果您使用 Spring Boot,还有另一种方式:WebClientCustomizer
。
@Bean (4)
WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) { (1)
return webClientBuilder -> { (2)
configurer.registerHypermediaTypes(webClientBuilder); (3)
};
}
1 | 创建 Spring bean 时,请求 Spring HATEOAS 的 HypermediaWebClientConfigurer bean 的一个副本。 |
2 | 使用 Java 8 lambda 表达式来定义一个 WebClientCustomizer 。 |
3 | 在函数调用内部,应用 registerHypermediaTypes 方法。 |
4 | 将整个内容作为 Spring bean 返回,这样 Spring Boot 就可以获取它并将其应用于其自动配置的 WebClient.Builder bean。 |
在此阶段,无论何时您需要一个具体的 WebClient
实例,只需将 WebClient.Builder
注入到您的代码中,并使用 build()
方法。该 WebClient
实例将能够使用超媒体进行交互。
6.4. 配置 WebTestClient
实例
在使用支持超媒体的表示时,一个常见的任务是使用 WebTestClient
运行各种测试。
要在测试用例中配置一个 WebTestClient
实例,请参阅此示例:
WebTestClient
@Test // #1225
void webTestClientShouldSupportHypermediaDeserialization() {
// Configure an application context programmatically.
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(HalConfig.class); (1)
context.refresh();
// Create an instance of a controller for testing
WebFluxEmployeeController controller = context.getBean(WebFluxEmployeeController.class);
controller.reset();
// Extract the WebTestClientConfigurer from the app context.
HypermediaWebTestClientConfigurer configurer = context.getBean(HypermediaWebTestClientConfigurer.class);
// Create a WebTestClient by binding to the controller and applying the hypermedia configurer.
WebTestClient client = WebTestClient.bindToApplicationContext(context).build().mutateWith(configurer); (2)
// Exercise the controller.
client.get().uri("http://localhost/employees").accept(HAL_JSON) //
.exchange() //
.expectStatus().isOk() //
.expectBody(new TypeReferences.CollectionModelType<EntityModel<Employee>>() {}) (3)
.consumeWith(result -> {
CollectionModel<EntityModel<Employee>> model = result.getResponseBody(); (4)
// Assert against the hypermedia model.
assertThat(model.getRequiredLink(IanaLinkRelations.SELF)).isEqualTo(Link.of("http://localhost/employees"));
assertThat(model.getContent()).hasSize(2);
});
}
1 | 注册您的配置类,该类使用 @EnableHypermediaSupport 以启用 HAL 支持。 |
2 | 使用 HypermediaWebTestClientConfigurer 来应用超媒体支持。 |
3 | 请求一个类型为 CollectionModel<EntityModel<Employee>> 的响应,使用 Spring HATEOAS 的 TypeReferences.CollectionModelType 帮助类。 |
4 | 在获取到 Spring HATEOAS 格式的“body”后,对其进行断言! |
WebTestClient 是一个不可变的值类型(immutable value type),因此您不能原地修改它。HypermediaWebClientConfigurer 返回一个变异后的变体,您必须捕获它才能使用。 |
如果您使用 Spring Boot,还有其他选项,例如这样
WebTestClient
@SpringBootTest
@AutoConfigureWebTestClient (1)
class WebClientBasedTests {
@Test
void exampleTest(@Autowired WebTestClient.Builder builder, @Autowired HypermediaWebTestClientConfigurer configurer) { (2)
client = builder.apply(configurer).build(); (3)
client.get().uri("/") //
.exchange() //
.expectBody(new TypeReferences.EntityModelType<Employee>() {}) (4)
.consumeWith(result -> {
// assert against this EntityModel<Employee>!
});
}
}
1 | 这是 Spring Boot 的测试注解,它将为这个测试类配置一个 WebTestClient.Builder 。 |
2 | 将 Spring Boot 的 WebTestClient.Builder autowire 到 builder 中,并将 Spring HATEOAS 的 configurer 作为方法参数。 |
3 | 使用 HypermediaWebTestClientConfigurer 来注册对超媒体的支持。 |
4 | 表明您希望返回一个 EntityModel<Employee> ,使用 TypeReferences 。 |
同样,您可以使用与之前示例类似的断言。
还有许多其他方法来构建测试用例。WebTestClient
可以绑定到控制器、函数和 URL。本节并非旨在展示所有这些方法。相反,这为您提供了一些入门示例。重要的是,通过应用 HypermediaWebTestClientConfigurer
,任何 WebTestClient
实例都可以被修改以处理超媒体。
6.5. 配置 RestTemplate 实例
如果您想创建自己的 RestTemplate
副本,并配置它以与超媒体交互,您可以使用 HypermediaRestTemplateConfigurer
RestTemplate
/**
* Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}.
*/
@Bean
RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { (1)
return configurer.registerHypermediaTypes(new RestTemplate()); (2)
}
1 | 在您的 @Configuration 类中,获取 Spring HATEOAS 注册的 HypermediaRestTemplateConfigurer bean 的一个副本。 |
2 | 创建 RestTemplate 后,使用该 configurer 来应用超媒体类型。 |
您可以自由地将此模式应用于任何您需要的 RestTemplate
实例,无论是创建一个注册的 bean,还是在您定义的服务内部。
如果您使用 Spring Boot,还有另一种方法。
通常,Spring Boot 已经不再推荐在应用上下文(application context)中注册 RestTemplate
bean 的概念。
-
与不同的服务进行通信时,您通常需要不同的凭据。
-
当
RestTemplate
使用底层连接池时,您会遇到额外的问题。 -
用户通常需要不同的实例,而不是单个 bean。
为了弥补这一点,Spring Boot 提供了一个 RestTemplateBuilder
。这个自动配置的 bean 允许您定义用于构建 RestTemplate
实例的各种 bean。您可以请求一个 RestTemplateBuilder
bean,调用其 build()
方法,然后应用最终设置(例如凭据和其他详细信息)。
要注册基于超媒体的消息转换器,将以下内容添加到您的代码中:
@Bean (4)
RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { (1)
return restTemplate -> { (2)
configurer.registerHypermediaTypes(restTemplate); (3)
};
}
1 | 创建 Spring bean 时,请求 Spring HATEOAS 的 HypermediaRestTemplateConfigurer bean 的一个副本。 |
2 | 使用 Java 8 lambda 表达式来定义一个 RestTemplateCustomizer 。 |
3 | 在函数调用内部,应用 registerHypermediaTypes 方法。 |
4 | 将整个内容作为 Spring bean 返回,这样 Spring Boot 就可以获取它并将其应用于其自动配置的 RestTemplateBuilder 。 |
在此阶段,无论何时您需要一个具体的 RestTemplate
实例,只需将 RestTemplateBuilder
注入到您的代码中,并使用 build()
方法。该 RestTemplate
实例将能够使用超媒体进行交互。