这个项目提供了一些 API 来简化在使用 Spring 尤其是 Spring MVC 时创建遵循HATEOAS原则的 REST 表示。它试图解决的核心问题是链接创建和表示组装。

© 2012-2021 原作者。

本文档的副本可以供您自己使用和分发给其他人,前提是您不对此类副本收取任何费用,并且进一步前提是每份副本都包含本版权声明,无论是印刷版还是电子版。

1.前言

1.1。迁移到 Spring HATEOAS 1.0

对于 1.0,我们借此机会重新评估了我们为 0.x 分支所做的一些设计和封装结构选择。关于它的反馈数量令人难以置信,而主要版本的凹凸似乎是重构这些的最自然的地方。

1.1.1。变化

包结构的最大变化是通过引入超媒体类型注册 API 来支持 Spring HATEOAS 中的其他媒体类型。这导致客户端和服务器 API(分别命名的包)以及包中的媒体类型实现的明确分离mediatype

将代码库升级到新 API 的最简单方法是使用迁移脚本。在我们跳到那个之前,这里是快速浏览的变化。

表示模型

/// 类的组从来没有真正感觉到适当的命名ResourceSupport。毕竟,这些类型实际上并不体现资源,而是可以通过超媒体信息和可供性丰富的表示模型。以下是新名称与旧名称的映射方式:ResourceResourcesPagedResources

  • ResourceSupport就是现在RepresentationModel

  • Resource就是现在EntityModel

  • Resources就是现在CollectionModel

  • PagedResources就是现在PagedModel

因此,ResourceAssembler已重命名为RepresentationModelAssembler及其方法toResource(…),并toResources(…)已分别重命名为toModel(…)toCollectionModel(…)。名称更改也反映在TypeReferences.

  • RepresentationModel.getLinks()现在公开一个Links实例(通过 a List<Link>),因为它公开了额外的 API 以Links使用各种策略连接和合并不同的实例。此外,它已被转换为自绑定泛型类型,以允许将链接添加到实例的方法返回实例本身。

  • LinkDiscovererAPI 已移至client包中。

  • 和API 已移至包中LinkBuilderEntityLinksserver

  • ControllerLinkBuilder已被移入server.mvc并弃用以替换为WebMvcLinkBuilder.

  • RelProvider已重命名为LinkRelationProvider并返回LinkRelation实例而不是Strings。

  • VndError已移至mediatype.vnderror包中。

1.1.2。迁移脚本

您可以找到从应用程序根目录运行的脚本,该脚本将更新所有导入语句和静态方法引用到我们源代码存储库中移动的 Spring HATEOAS 类型。只需下载它,从您的项目根目录运行它。默认情况下,它将检查所有 Java 源文件并用新的替换旧的 Spring HATEOAS 类型引用。

示例 1. 迁移脚本的示例应用程序
$ ./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(…)取款详情已移至Affordances. 要手动构建Affordance实例,现在使用Affordances.of(link).afford(…). 还要注意为流畅使用而AffordanceBuilder暴露的新类型。Affordances有关详细信息,请参阅可供性

  • AffordanceModelFactory.getAffordanceModel(…)现在接收InputPayloadMetadataPayloadMetadata实例而不是ResolvableTypes 以允许非基于类型的实现。必须相应地调整自定义媒体类型实现。

  • 如果属性值符合规范中定义的默认值,HAL 表单现在不会呈现属性属性。即,如果之前required明确设置为false,我们现在只需省略 的条目requiredPATCH我们现在也只强制它们对于用作 HTTP 方法的模板是非必需的。

2. 基本面

本节介绍 Spring HATEOAS 的基础知识及其基本领域抽象。

超媒体的基本思想是用超媒体元素丰富资源的表示。最简单的形式是链接。它们指示客户端可以导航到某个资源。相关资源的语义在所谓的链接关系中定义。您可能已经在 HTML 文件的标题中看到了这一点:

示例 2. HTML 文档中的链接
<link href="theme.css" rel="stylesheet" type="text/css" />

如您所见,链接指向资源theme.css并指示它是样式表。链接通常带有附加信息,例如资源指向的将返回的媒体类型。但是,链接的基本构建块是它的引用和关系。

Spring HATEOAS 允许您通过其不可变Link值类型处理链接。它的构造函数采用超文本引用和链接关系,后者默认为 IANA 链接关系self在链接关系中阅读有关后者的更多信息。

示例 3. 使用链接
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 的结构,它只需要知道变量的名称。

示例 4. 使用带有模板化 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 模板并在以后添加模板变量。

示例 5. 使用 URI 模板
UriTemplate template = UriTemplate.of("/{segment}/something")
  .with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM);

assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");

使用所谓的链接关系来指示目标资源与当前资源的关系。Spring HATEOAS 提供了一种LinkRelation类型来轻松创建String基于它的实例。

Internet 号码分配机构包含一组预定义的链接关系。它们可以通过 引用IanaLinkRelations

示例 6. 使用 IANA 链接关系
Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);

assertThat(link.getRel()).isEqualTo(LinkRelation.of("next"));
assertThat(IanaLinkRelation.isIanaRel(link.getRel())).isTrue();

2.4.表示模型

RepresentationModel为了轻松创建超媒体丰富的表示,Spring HATEOAS在其根中提供了一组类。它基本上是一个Links 集合的容器,并且有方便的方法将它们添加到模型中。模型稍后可以呈现为各种媒体类型格式,这些格式将定义超媒体元素在表示中的外观。有关这方面的更多信息,请查看媒体类型

示例 7.RepresentationModel类层次结构
类表示模型
类实体模型
类 CollectionModel
类 PagedModel

实体模型 -|> 表示模型
集合模型 -|> 表示模型
PagedModel -|> 集合模型

使用 a 的默认方法RepresentationModel是创建它的子类以包含表示应该包含的所有属性,创建该类的实例,填充属性并使用链接丰富它。

示例 8. 示例表示模型类型
class PersonModel extends RepresentationModel<PersonModel> {

  String firstname, lastname;
}

泛型自键入对于让RepresentationModel.add(…)返回自身的实例是必要的。现在可以像这样使用模型类型:

示例 9. 使用人员表示模型
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,则响应将如下所示:

示例 10. 为人员表示模型生成的 HAL 表示
{
  “_链接”:{
    “自己” : {
      “href”:“https://myhost/people/42”
    }
  },
  “名字”:“戴夫”,
  “姓氏”:“马修斯”
}

2.4.1。项目资源表示模型

对于由单一对象或概念支持的资源,EntityModel存在便利类型。无需为每个概念创建自定义模型类型,您只需重用现有类型并将其实例包装到EntityModel.

示例 11.EntityModel用于包装现有对象
Person person = new Person("Dave", "Matthews");
EntityModel<Person> model = EntityModel.of(person);

2.4.2. 集合资源表示模型

对于概念上是集合的资源,aCollectionModel可用。它的元素可以是简单的对象,也可以是RepresentationModel实例。

示例 12.CollectionModel用于包装现有对象的集合
Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews"));
CollectionModel<Person> model = CollectionModel.of(people);

虽然 anEntityModel被限制为始终包含有效负载,因此允许推断唯一实例上的类型排列,但 aCollectionModel的基础集合可以是空的。由于 Java 的类型擦除,我们实际上无法检测到 aCollectionModel<Person> model = CollectionModel.empty()实际上是 a CollectionModel<Person>,因为我们看到的只是运行时实例和一个空集合。可以通过将缺失的类型信息添加到模型中,方法是将其添加到构造时的空实例中,CollectionModel.empty(Person.class)或者作为后备,以防基础集合可能为空:

Iterable<Person> people = repository.findAll();
var model = CollectionModel.of(people).withFallbackType(Person.class);

3.服务器端支持

现在我们已经有了领域词汇表,但主要挑战仍然存在:如何创建实际的 URI,以Link不那么脆弱的方式包装到实例中。现在,我们必须在各处复制 URI 字符串。这样做是脆弱且不可维护的。

假设您的 Spring MVC 控制器实现如下:

@Controller
class PersonController {

  @GetMapping("/people")
  HttpEntity<PersonModel> showAll() { … }

  @GetMapping(value = "/{person}", method = RequestMethod.GET)
  HttpEntity<PersonModel> show(@PathVariable Long person) { … }
}

我们在这里看到两个约定。第一个是通过@GetMapping控制器方法的注释公开的集合资源,该集合的各个元素作为直接子资源公开。集合资源可能以简单的 URI(如图所示)或更复杂的 URI(例如 )公开/people/{id}/addresses。假设您想链接到所有人的收藏资源。遵循上面的方法会导致两个问题:

  • 要创建绝对 URI,您需要查找协议、主机名、端口、servlet 基础和其他值。这很麻烦,并且需要丑陋的手动字符串连接代码。

  • 您可能不想/people在基本 URI 顶部连接 on,因为这样您就必须在多个位置维护信息。如果更改映射,则必须更改指向它的所有客户端。

Spring HATEOAS 现在提供了一个WebMvcLinkBuilder允许您通过指向控制器类来创建链接的方法。以下示例显示了如何执行此操作:

import static org.sfw.hateoas.server.mvc.WebMvcLinkBuilder.*;

Link link = linkTo(PersonController.class).withRel("people");

assertThat(link.getRel()).isEqualTo(LinkRelation.of("people"));
assertThat(link.getHref()).endsWith("/people");

WebMvcLinkBuilder后台使用 SpringServletUriComponentsBuilder从当前请求中获取基本的 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);

您甚至可以构建指向方法的链接或创建虚拟控制器方法调用。第一种方法是将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&param=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是渲染请求参数的复合方式已融入 SpringUriComponents构建器的内部,我们仅在 Spring HATEOAS 1.4 中引入了非复合样式。如果我们今天从头开始,我们可能会默认使用该样式,而是让用户明确地选择复合样式,而不是相反。

去做

3.3. 供养

环境的供给是它所提供的……它提供或提供的东西,无论是好是坏。在字典中可以找到动词“负担”,但没有找到名词“负担”。我已经编好了。

— James J. Gibson
视觉感知的生态学方法(第 126 页)

基于 REST 的资源不仅提供数据,还提供控制。形成灵活服务的最后一个要素是如何使用各种控件的详细说明。因为可供性与链接相关联,所以 Spring HATEOAS 提供了一个 API,可以根据需要将尽可能多的相关方法附加到链接。就像您可以通过指向 Spring MVC 控制器方法来创建链接一样(参见 在 Spring MVC 中构建链接以获取详细信息)你……

以下代码显示了如何获取自我链接并关联另外两个可供性:

示例 13. 将可供性连接到GET /employees/{id}
@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 创建自我链接。
2 updateEmployee方法与self链接相关联。
3 partiallyUpdateEmployee方法与self链接相关联。

使用.andAffordance(afford(…​)),您可以使用控制器的方法将一个PUT和一个PATCH操作连接到一个GET操作。想象一下上面提供的相关方法如下所示:

示例 14.updateEmpoyee响应的方法PUT /employees/{id}
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee( //
    @RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
示例 15.partiallyUpdateEmployee响应的方法PATCH /employees/{id}
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee( //
    @RequestBody EntityModel<Employee> employee, @PathVariable Integer id)

使用这些方法指向这些afford(…)方法将导致 Spring HATEOAS 分析请求正文和响应类型并捕获元数据以允许不同的媒体类型实现使用该信息将其转换为输入和输出的描述。

3.3.1。手动构建可供性

虽然是为链接注册可供性的主要方式,但可能需要手动构建其中的一些。这可以通过使用AffordancesAPI 来实现:

示例 16. 使用AffordancesAPI 手动注册可供性
var 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 您首先AffordancesLink创建描述可供性的上下文的实例创建一个实例。
2 每个可供性都从它应该支持的 HTTP 方法开始。然后,我们将一个类型注册为有效负载描述并明确命名可供性。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这有效地创建了与指向EmployeeController.newEmployee(…)created 的指针相同的可供性。
3 构建下一个可供性以反映指向EmployeeController.search(…). 在这里,我们定义Employee为创建响应的模型并显式注册QueryParameter

可供性由特定于媒体类型的可供性模型支持,这些模型将一般可供性元数据转换为特定的表示。请务必查看媒体类型部分中有关可供性的部分,以了解有关如何控制该元数据的公开的更多详细信息。

当您的应用程序位于代理后面、负载均衡器后面或云中时,最常使用RFC-7239 转发标头。实际接收 Web 请求的节点是基础架构的一部分,并将请求转发到您的应用程序。

您的应用程序可能正在运行localhost:8080,但对于外界来说,您应该在reallycoolsite.com(以及在网络的标准端口 80 上)运行。通过让代理包含额外的标头(许多人已经这样做了),Spring HATEOAS 可以正确生成链接,因为它使用 Spring Framework 功能来获取原始请求的基本 URI。

任何可以根据外部输入更改根 URI 的东西都必须得到适当的保护。这就是默认情况下禁用转发标头处理的原因。您必须使其能够运行。如果您要部署到云或配置到您控制代理和负载平衡器的配置中,那么您肯定会想要使用此功能。

要启用转发的标头处理,您需要在您的应用程序中ForwardedHeaderFilter为 Spring MVC(详情请点击此处)或ForwardedHeaderTransformerSpring WebFlux(请点击此处详情)注册 Spring 。在 Spring Boot 应用程序中,这些组件可以简单地声明为 Spring bean,如此所述。

示例 17. 注册一个ForwardedHeaderFilter
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
    return new ForwardedHeaderFilter();
}

这将创建一个处理所有X-Forwarded-…标头的 servlet 过滤器。它会用 servlet 处理程序正确注册它。

对于 Spring WebFlux 应用程序,响应式对应物是ForwardedHeaderTransformer

示例 18. 注册一个ForwardedHeaderTransformer
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
    return new ForwardedHeaderTransformer();
}

这将创建一个转换响应式 Web 请求、处理X-Forwarded-…标头的函数。它会用 WebFlux 正确注册它。

使用如上所示的配置,请求传递X-Forwarded-…标头将看到那些反映在生成的链接中的标头:

示例 19. 使用X-Forwarded-…标头的请求
curl -v localhost:8080/employees \
    -H 'X-Forwarded-Proto: https' \
    -H 'X-Forwarded-Host: example.com' \
    -H 'X-Forwarded-Port: 9001'
示例 20. 相应的响应以及为考虑这些标头而生成的链接
{
  "_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"
    }
  }
}
EntityLinks并且目前还没有为 Spring WebFlux 应用程序提供开箱即用的各种实现。SPI 中定义的契约EntityLinks最初是针对 Spring Web MVC 的,不考虑 Reactor 类型。开发支持反应式编程的类似合约仍在进行中。

到目前为止,我们已经通过指向 Web 框架实现(即 Spring MVC 控制器)创建了链接并检查了映射。在许多情况下,这些类本质上是由模型类支持的读写表示。

EntityLinks接口现在公开了一个 API 来查找LinkLinkBuilder基于模型类型。这些方法本质上返回指向集合资源(例如/people)或项目资源(例如/people/1)的链接。下面的例子展示了如何使用EntityLinks

EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);

EntityLinks@EnableHypermediaSupport通过在 Spring MVC 配置中激活,可以通过依赖注入获得。EntityLinks这将导致被注册的各种默认实现。最基本的一个是ControllerEntityLinks检查 SpringMVC 控制器类。如果您想注册自己的实现EntityLinks,请查看此部分

3.5.1。基于 Spring MVC 控制器的 EntityLinks

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.

有了这个,当你EntityLinks @EnableHypermediaSupport在 Spring MVC 配置中启用时,你可以创建到控制器的链接,如下所示:

@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 在您的配置中注入EntityLinks可用。@EnableHypermediaSupport
2 通过使用实体类型而不是控制器类来使用 API 来构建链接。

如您所见,您可以引用资源管理Order实例而无需OrderController显式引用。

3.5.2. EntityLinks API 详解

从根本上说,EntityLinks允许为实体类型的集合和项目资源构建LinkBuilders 和实例。Link以 开头的方法linkFor…将为您生成LinkBuilder实例,以使用其他路径段、参数等进行扩展和扩充。以 开头的方法将linkTo生成完全准备好的Link实例。

虽然对于集合资源来说,提供实体类型就足够了,但到项目资源的链接需要提供标识符。这通常看起来像这样:

示例 21. 获取项目资源的链接
entityLinks.linkToItemResource(order, order.getId());

如果您发现自己重复了这些方法调用,则可以将标识符提取步骤提取到可重用Function中,以便在不同的调用中重用:

Function<Order, Object> idExtractor = Order::getId; (1)

entityLinks.linkToItemResource(order, idExtractor); (2)
1 标识符提取被外部化,以便它可以保存在字段或常量中。
2 使用提取器的链接查找。
TypedEntityLinks

由于控制器实现通常围绕实体类型进行分组,因此您经常会发现自己在整个控制器类中使用相同的提取器函数(详细信息请参阅 EntityLinks API )。我们可以通过获取提供一次提取器的实例来更加集中标识符提取逻辑TypedEntityLinks,这样实际的查找就根本不必再处理提取了。

示例 22. 使用 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

由创建的EntityLinks实例@EnableHypermediaSupport是类型DelegatingEntityLinks,它将依次拾取所有其他EntityLinks可用的实现作为ApplicationContext. 它已注册为主要 bean,因此通常在您注入时它始终是唯一的注入候选EntityLinks者。 ControllerEntityLinks是将包含在设置中的默认实现,但用户可以自由实现和注册自己的实现。使这些可用于EntityLinks实例可用于注入是将您的实现注册为 Spring bean 的问题。

示例 23. 声明自定义 EntityLinks 实现
@Configuration
class CustomEntityLinksConfiguration {

  @Bean
  MyEntityLinks myEntityLinks(…) {
    return new MyEntityLinks(…);
  }
}

这种机制可扩展性的一个例子是 Spring Data REST RepositoryEntityLinks,它使用存储库映射信息来创建指向 Spring Data 存储库支持的资源的链接。同时,它甚至为其他类型的资源公开了额外的查找方法。如果您想使用这些,只需RepositoryEntityLinks显式注入。

3.6.表示模型组装器

由于从实体到表示模型的映射必须在多个地方使用,因此创建一个专门的类来负责这样做是有意义的。转换包含非常自定义的步骤,但也包含一些样板步骤:

  1. 模型类的实例化

  2. 添加一个指向要呈现的资源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(…​)是您编写的用于实例化PersonModel给定对象的Person对象的代码。它应该只专注于设置属性,而不是填充Links.

像我们在前面的示例中所做的那样设置类可以为您带来以下好处:

  • 有一些createModelWithId(…)方法可以让您创建资源实例并添加Link一个 rel self。该链接的 href 由配置的控制器的请求映射加上实体的 ID 确定(例如,/people/1)。

  • 资源类型通过反射进行实例化,并需要一个无参数的构造函数。如果要使用专用构造函数或避免反射性能开销,可以覆盖instantiateModel(…).

然后,您可以使用汇编器来汇编 aRepresentationModel或 a CollectionModel。以下示例创建一个CollectionModel实例PersonModel

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 由处理器提供。

这个例子很简单,但你可以很容易地:

  • 使用WebMvcLinkBuilderWebFluxLinkBuilder构建指向您的PaymentController.

  • 注入有条件地添加由状态驱动的其他链接(例如 )cancel所需的任何服务。amend

  • 利用 Spring Security 等横切服务根据当前用户的上下文添加、删除或修改链接。

此外,在此示例中,PaymentProcessor更改了提供的EntityModel<Order>. 您还可以 用另一个对象替换它。请注意,API 要求返回类型等于输入类型。

3.7.1. 处理空集合模型

为了找到RepresentationModelProcessor为一个实例调用的正确实例集,调用基础结构对已注册的 sRepresentationModel的泛型声明进行详细分析。RepresentationModelProcessor例如CollectionModel,这包括检查底层集合的元素,因为在运行时,唯一的模型实例不公开泛型信息(由于 Java 的类型擦除)。这意味着,默认情况下,RepresentationModelProcessor不会为空集合模型调用实例。为了仍然允许基础设施正确推断负载类型,您可以CollectionModel从一开始就使用显式回退负载类型初始化空实例,或者通过调用CollectionModel.withFallbackType(…). 有关详细信息,请参阅集合资源表示模型

3.8.使用LinkRelationProviderAPI

在构建链接时,您通常需要确定要用于链接的关系类型。在大多数情况下,关系类型直接与(域)类型相关联。我们封装了详细的算法来查找LinkRelationProviderAPI 背后的关系类型,让您可以确定单个资源和集合资源的关系类型。查找关系类型的算法如下:

  1. 如果类型用 注释@Relation,我们使用注释中配置的值。

  2. List如果没有,我们默认为未大写的简单类名加上集合的附加名称rel

  3. 如果EVO 变形器JAR 在类路径中,我们使用rel复数算法提供的单个资源的复数。

  4. @Controller用注释的类@ExposesResourceFor(见使用 EntityLinks 接口了解详情)透明地查找注解中配置的类型的关系类型,以便您可以使用LinkRelationProvider.getItemResourceRelFor(MyController.class)和获取暴露的域类型的关系类型。

当您使用ALinkRelationProvider时,它会自动作为 Spring bean 公开@EnableHypermediaSupport。您可以通过实现接口并依次将它们公开为 Spring bean 来插入自定义提供程序。

4.媒体类型

4.1。HAL——超文本应用语言

JSON 超文本应用程序语言或 HAL 是在不讨论特定 Web 堆栈时采用的最简单和最广泛采用的超媒体媒体类型之一。

它是 Spring HATEOAS 采用的第一个基于规范的媒体类型。

4.1.1。构建 HAL 表示模型

从 Spring HATEOAS 1.1 开始,我们提供了一个专用HalModelBuilder的,允许RepresentationModel通过 HAL 惯用 API 创建实例。这些是它的基本假设:

  1. HAL 表示可以由构建表示中包含的域字段的任意对象(实体)支持。

  2. 可以通过各种嵌入文档来丰富表示,这些文档可以是任意对象或 HAL 表示本身(即包含嵌套的嵌入和链接)。

  3. 某些 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(参见使用LinkRelationProviderAPI 获取详细信息)。
{
  "_links" : {
    "self" : { "href" : "…" }, (1)
    "customer" : { "href" : "/orders/4711/customer" } (2)
  },
  "_embedded" : {
    "customer" : { … }, (3)
    "additional" : { … } (4)
  }
}
1 self明确提供的链接。
2 customer链接通过透明添加….preview(…).forLink(…)
3 提供的预览对象。
4 通过显式添加的附加元素….embed(…)

在 HAL_embedded中也用于表示顶级集合。它们通常分组在从对象类型派生的链接关系下。即 HAL 中的订单列表如下所示:

{
  "_embedded" : {
    "orders : [
      … (1)
    ]
  }
}
1 单独的订单文件在这里。

创建这样的表示就像这样简单:

Collection<Order> orders = …;

HalModelBuilder.emptyHalDocument()
  .embed(orders);

也就是说,如果订单为空,则无法派生出现在内部的链接关系_embedded,因此如果集合为空,则文档将保持为空。

如果您更喜欢显式传达空集合,则可以将类型传递给….embed(…)采用Collection. 如果传递给方法的集合是空的,这将导致呈现一个字段,其链接关系派生自给定类型。

HalModelBuilder.emptyHalModel()
  .embed(Collections.emptyList(), Order.class);
  // or
  .embed(Collections.emptyList(), LinkRelation.of("orders"));

将创建以下更明确的表示。

{
  "_embedded" : {
    "orders" : []
  }
}

4.1.2. 配置链接呈现

在 HAL 中,_links条目是 JSON 对象。属性名称是链接关系,每个值要么是一个链接对象,要么是一个链接对象数组

对于具有两个或多个链接的给定链接关系,规范在表示上是明确的:

示例 24. 具有与一个关系关联的两个链接的 HAL 文档
{
  "_links": {
    "item": [
      { "href": "https://myhost/cart/42" },
      { "href": "https://myhost/inventory/12" }
    ]
  },
  "customer": "Dave Matthews"
}

但是,如果给定关系只有一个链接,则规范是模棱两可的。您可以将其渲染为单个对象或单个项目数组。

默认情况下,Spring HATEOAS 使用最简洁的方法并呈现如下的单链接关系:

示例 25. 将单个链接呈现为对象的 HAL 文档
{
  "_links": {
    "item": { "href": "https://myhost/inventory/12" }
  },
  "customer": "Dave Matthews"
}

一些用户在使用 HAL 时不喜欢在数组和对象之间切换。他们更喜欢这种类型的渲染:

示例 26. 将单个链接呈现为数组的 HAL
{
  "_links": {
    "item": [{ "href": "https://myhost/inventory/12" }]
  },
  "customer": "Dave Matthews"
}

如果您希望自定义此策略,您所要做的就是将一个HalConfigurationbean 注入您的应用程序配置中。有多种选择。

示例 27. 全局 HAL 单链接渲染策略
@Bean
public HalConfiguration globalPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 通过将所有单链接关系呈现为数组来覆盖 Spring HATEOAS 的默认值。

如果你喜欢只覆盖一些特定的链接关系,你可以HalConfiguration 像这样创建一个 bean:

示例 28. 基于链接关系的 HAL 单链接渲染策略
@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 样式的路径模式:

示例 29. 基于模式的 HAL 单链接渲染策略
@Bean
public HalConfiguration patternBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          "http*", RenderSingleLinks.AS_ARRAY); (1)
}
1 http渲染所有以数组开头的链接关系。
基于模式的方法使用 Spring 的AntPathMatcher.

所有这些HalConfiguration都可以结合起来形成一个全面的政策。请务必广泛测试您的 API 以避免意外。

4.1.3。链接标题国际化

HALtitle为其链接对象定义了一个属性。这些标题可以通过使用 Spring 的资源包抽象和命名的资源包来填充,rest-messages以便客户端可以直接在其 UI 中使用它们。此捆绑包将自动设置并在 HAL 链接序列化期间使用。

要为链接定义标题,请使用以下密钥模板_links.$relationName.title

示例 30. 一个示例rest-messages.properties
_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout

这将导致以下 HAL 表示:

示例 31. 定义了链接标题的示例 HAL 文档
{
  "_links" : {
    "cancel" : {
      "href" : "…"
      "title" : "Cancel order"
    },
    "payment" : {
      "href" : "…"
      "title" : "Proceed to checkout"
    }
  }
}

4.1.4。使用CurieProviderAPI

Web Linking RFC描述了注册和扩展链接关系类型。已注册的 rel 是在IANA 链接关系类型注册表中注册的众所周知的字符串。rel不希望注册关系类型的应用程序可以使用扩展URI。每一个都是唯一标识关系类型的 URI。relURI 可以序列化为紧凑 URI或Curie。例如,如果定义为,ex:persons则表示链接关系类型的居里。如果使用居里,则基本 URI 必须存在于响应范围中。example.com/rels/personsexexample.com/rels/{rel}

默认创建的relRelProvider是扩展关系类型,因此必须是 URI,这会导致大量开销。API 负责处理:它允许您将CurieProvider基本 URI 定义为 URI 模板和代表该基本 URI 的前缀。如果 aCurieProvider存在,则RelProvider在所有rel值前面加上居里前缀。此外,curies链接会自动添加到 HAL 资源。

以下配置定义了一个默认的居里提供者:

@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链接将居里解析为完整形式。以下示例显示了如何执行此操作:

{
  "_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"
}

由于CurieProviderAPI 的目的是允许自动创建居里,因此每个应用程序范围只能定义一个CurieProviderbean。

4.2. 哈尔形式

HAL-FORMS旨在为HAL 媒体类型添加运行时 FORM 支持。

HAL-FORMS“看起来像 HAL”。但是,重要的是要记住,HAL-FORMS 与 HAL 不同——两者不应被认为可以以任何方式互换。

— Mike Amundsen
HAL-FORMS 规范

要启用此媒体类型,请将以下配置放入您的代码中:

示例 32. 启用 HAL-FORMS 的应用程序
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {

}

每当客户端提供带有 的Accept标头时application/prs.hal-forms+json,您都可以期待以下内容:

示例 33. HAL-FORMS 示例文档
{
  "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 允许通过为输入和输出类型塑造模型类型并在它们上使用注释来自定义它们。

每个模板都将定义以下属性:

表 1. 模板属性
属性 描述

contentType

服务器预期接收的媒体类型。仅在指向公开@RequestMapping(consumes = "…")属性的控制器方法或在设置可供性时明确定义媒体类型时才包括在内。

method

提交模板时使用的 HTTP 方法。

target

要将表单提交到的目标 URI。仅当示能目标与声明它的链接不同时才会呈现。

title

显示模板时人类可读的标题。

properties

与表格一起提交的所有属性(见下文)。

每个属性都将定义以下属性:

表 2. 属性属性
属性 描述

readOnly

true如果该属性没有 setter 方法,则设置为。如果存在,@JsonProperty(Access.READ_ONLY)请在访问器或字段上明确使用 Jackson。默认情况下不呈现,因此默认为false.

regex

可以通过@Pattern在字段或类型上使用 JSR-303 的注释来自定义。在后者的情况下,模式将用于声明为该特定类型的每个属性。默认不渲染。

required

可以使用 JSR-303 的@NotNull. 默认情况下不呈现,因此默认为false. 使用 as 方法的模板PATCH将自动将所有属性设置为不需要。

max

属性允许的最大值。派生自 Hibernate Validator@Range或 JSR-303@Max@DecimalMax注释。

maxLength

属性允许的最大长度值。源自 Hibernate Validator 的@Length注解。

min

属性允许的最小值。派生自 Hibernate Validator@Range或 JSR-303@Min@DecimalMin注释。

minLength

属性允许的最小长度值。源自 Hibernate Validator 的@Length注解。

options

提交表单时选择值的选项。有关详细信息,请参阅为属性定义 HAL-FORMS 选项

prompt

呈现表单输入时使用的用户可读提示。有关详细信息,请参阅属性提示

placeholder

一个用户可读的占位符,用于给出预期格式的示例。定义方式遵循属性提示,但使用后缀_placeholder.

type

@InputType从显式注释、JSR-303 验证注释或属性类型派生的 HTML 输入类型。

对于无法手动注释的类型,您可以通过HalFormsConfiguration应用程序上下文中的 bean 注册自定义模式。

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

    HalFormsConfiguration configuration = new HalFormsConfiguration();
    configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
  }
}

此设置将导致类型的表示模型属性的 HAL-FORMS 模板属性CreditCardNumber声明regex具有 value 的字段[0-9]{16}

为属性定义 HAL-FORMS 选项

对于其值应该与某个值的超集匹配的属性,HAL-FORMSoptions在属性定义中定义子文档。HalFormsConfiguration可以通过'withOptions(…)获取指向类型属性的指针和将 aPropertyMetadata转换为HalFormsOptions实例的创建者函数来描述可用于特定属性的选项。

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

    HalFormsConfiguration configuration = new HalFormsConfiguration();
    configuration.withOptions(Order.class, "shippingMethod" metadata ->
      HalFormsOptions.inline("FedEx", "DHL"));
  }
}

了解我们如何设置选项值FedEx以及DHLOrder.shippingMethod属性选择的选项。或者,HalFormsOptions.remote(…)可以指向动态提供值的远程资源。有关选项设置的更多限制,请参阅规范HalFormsOptions.

4.2.2. 表单属性的国际化

HAL-FORMS 包含用于人工解释的属性,例如模板的标题或属性提示。这些可以使用 Spring 的资源包支持和rest-messagesSpring HATEOAS 默认配置的资源包来定义和国际化。

模板标题

要定义模板标题,请使用以下模式:_templates.$affordanceName.title. 请注意,在 HAL-FORMS 中,模板的名称default是唯一的。这意味着您通常必须使用启示描述的本地或完全限定的输入类型名称来限定键。

示例 34. 定义 HAL-FORMS 模板标题
_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 使用defaultas 键的标题的全局定义。
2 使用实际可供性名称作为键的标题的全局定义。除非在创建示能时明确定义,否则默认为创建示能时已指向的方法的名称。
3 一个本地定义的标题,应用于所有名为 的类型Employee
4 使用完全限定类型名称的标题定义。
使用实际可供性名称的键优先于默认名称。
属性提示

属性提示也可以rest-messages通过 Spring HATEOAS 自动配置的资源包来解决。这些键可以在全局、本地或完全限定的范围内定义,并且需要._prompt连接到实际的属性键:

示例 35. 为email属性定义提示
firstName._prompt=Firstname (1)
Employee.firstName._prompt=Firstname (2)
com.acme.Employee.firstName._prompt=Firstname (3)
1 所有命名的属性firstName都将呈现“名字”,与它们声明的类型无关。
2 命名类型中的firstName属性Employee将提示“名字”。
3 firstName属性com.acme.Employee将获得分配“名字”的提示。

4.2.3。一个完整的例子

让我们看一些结合了上述所有定义和自定义属性的示例代码。客户的 ARepresentationModel可能看起来像这样:

class CustomerRepresentation
  extends RepresentationModel<CustomerRepresentation> {

  String name;
  LocalDate birthdate; (1)
  @Pattern(regex = "[0-9]{16}") String ccn; (2)
  @Email String email; (3)
}
1 我们定义了一个birthdatetype 的属性LocalDate
2 我们希望ccn遵守正则表达式。
3 我们定义email为使用 JSR-303@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 声明一个控制器方法以使用上面定义的表示模型将请求主体绑定到如果 aPOST被发布到/customers
2 准备模型的GET请求/customers,添加一个self指向它的链接,并在该链接上额外声明一个可供性,指向映射到的控制器方法POST。这将导致建立一个可供性模型,该模型(取决于最终要呈现的媒体类型)将被转换为特定于媒体类型的格式。

接下来,让我们添加一些额外的元数据以使表单更易于人类访问:

中声明的附加属性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 ccn我们明确地为模型的属性提供了提示符和占位符CustomerRepresentation

如果客户端现在发出GET请求以/customers使用 的Accept标头application/prs.hal-forms+json,则响应 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因为它是定义的唯一模板,并且规范要求使用该名称。如果附加了多个模板(通过声明额外的功能),它们将分别以它们指向的方法命名。
2 模板标题派生自资源包中定义的值。请注意,根据Accept-Language随请求发送的标头和可用性,可能会返回不同的值。
3 属性的method值是派生自示能所源自的方法的映射。
4 type属性的值派生text自属性的类型String。这同样适用于birthdate财产,但导致date.
5 属性的提示符和占位符ccn也来自资源包。
6 该属性的@Pattern声明作为模板属性的ccn属性公开。regex
7 属性上的@Email注解email已被翻译成相应的type值。

HAL-FORMS 模板由例如HAL Explorer考虑,它会根据这些描述自动呈现 HTML 表单。

4.3. HTTP 问题详细信息

HTTP API 的问题详细信息是一种媒体类型,用于在 HTTP 响应中携带机器可读的错误详细信息,以避免需要为 HTTP API 定义新的错误响应格式。

HTTP 问题详细信息定义了一组 JSON 属性,这些属性携带附加信息以向 HTTP 客户端描述错误详细信息。在RFC 文档的相关部分中查找有关这些属性的更多详细信息。

Problem您可以通过在 Spring MVC 控制器中使用媒体类型域类型来创建这样的 JSON 响应:

使用 Spring HATEOAS 的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或显式对象添加自定义属性(见下文)。

要将专用对象用于自定义属性,请声明一个类型,创建并填充它的实例,然后Problem通过….withProperties(…)或在创建实例时通过 将其传递给实例Problem.create(…)

使用专用类型来捕获扩展问题属性
class AccountDetails {
  int balance;
  List<URI> accounts;
}

problem.withProperties(result.getDetails());

// or

Problem.create(result.getDetails());

这将导致如下所示的响应:

示例 HTTP 问题详细信息响应
{
  "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. 集合+JSON

Collection+JSON是使用 IANA 批准的媒体类型注册的 JSON 规范application/vnd.collection+json

Collection+JSON是一种基于 JSON 的读/写超媒体类型,旨在支持简单集合的管理和查询。

— Mike Amundsen
Collection+JSON 规范

Collection+JSON 提供了一种统一的方式来表示单个项目资源和集合。要启用此媒体类型,请将以下配置放入您的代码中:

示例 36. 启用 Collection+JSON 的应用程序
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {

}

此配置将使您的应用程序响应具有 如下所示Accept标头的请求。application/vnd.collection+json

规范中的以下示例显示了一个项目:

示例 37. 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": "jdoe@example.org",
            "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 渲染一个EntityModel时,它将:

  • self链接放入文档的href属性和项目级href属性中。

  • 将模型的其余链接放入顶层和linksitem-levellinks中。

  • 从 中提取属性EntityModel并将它们变成…

渲染资源集合时,文档几乎是一样的,只是 JSON 数组中会有多个条目items,每个条目对应一个条目。

更具体地说,Spring HATEOAS 将:

  • 将整个集合的self链接放入顶级href属性。

  • CollectionModel链接(减号)将self被放入顶层links

  • 每个项目级别将包含集合中每个条目href的相应self链接。CollectionModel.content

  • 每个项目级别links将包含来自 的每个条目的所有其他链接CollectionModel.content

4.5. UBER - 交换代表的统一基础

UBER是一个实验性的 JSON 规范

UBER 文档格式是一种最小的读/写超媒体类型,旨在支持简单的状态传输和基于 ad-hoc 超媒体的转换。

— Mike Amundsen
UBER 规范

UBER 提供了一种统一的方式来表示单个项目资源和集合。要启用此媒体类型,请将以下配置放入您的代码中:

示例 38. 启用 UBER+JSON 的应用程序
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {

}

此配置将使您的应用程序使用Accept标头响应请求application/vnd.amundsen-uber+json ,如下所示:

示例 39. UBER 示例文档
{
  "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 - 应用程序级配置文件语义

ALPS是一种媒体类型,用于提供有关另一个资源的基于配置文件的元数据。

ALPS 文档可用作配置文件来解释具有与应用程序无关的媒体类型(例如 HTML、HAL、Collection+JSON、Siren 等)的文档的应用程序语义。这增加了跨媒体类型的配置文件的可重用性。

— Mike Amundsen
阿尔卑斯山规格

ALPS 不需要特殊激活。相反,您“构建”一条Alps记录并从 Spring MVC 或 Spring WebFlux web 方法返回它,如下所示:

示例 40. 建立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:

示例 41. ALPS JSON
{
  “版本”:“1.0”,
  “文档”:{
    "格式": "文本",
    "href": "https://example.org/samples/full/doc.html",
    “价值”:“价值在这里”
  },
  “描述符”:[
    {
      "id": "类字段 [名称]",
      “名称”:“名称”,
      “类型”:“语义”,
      “描述符”:[
        {
          “id”:“嵌入”
        }
      ],
      “分机”:{
        "id": "ext [名称]",
        "href": "https://example.org/samples/ext/name",
        “价值”:“价值在这里”
      },
      "rt": "[name] 的 rt"
    },
    {
      "id": "类字段 [角色]",
      “名称”:“角色”,
      “类型”:“语义”,
      “描述符”:[
        {
          “id”:“嵌入”
        }
      ],
      “分机”:{
        "id": "ext [角色]",
        "href": "https://example.org/samples/ext/role",
        “价值”:“价值在这里”
      },
      “rt”:“[角色]的rt”
    }
  ]
}

如果您愿意,您可以手动编写它们,而不是将每个字段“自动”链接到域对象的字段。也可以使用 Spring Framework 的消息包和MessageSource接口。这使您能够将这些值委托给特定于语言环境的消息包,甚至可以国际化元数据。

4.7. 基于社区的媒体类型

由于能够创建自己的媒体类型,因此有几个社区主导的努力来构建其他媒体类型。

4.7.1. JSON:API

Maven坐标
<dependency>
    <groupId>com.toedter</groupId>
    <artifactId>spring-hateoas-jsonapi</artifactId>
    <version>{see project page for current version}</version>
</dependency>
Gradle坐标
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'

如果您需要快照版本,请访问项目页面以获取更多详细信息。

4.7.2. 警笛

Maven坐标
<dependency>
    <groupId>de.ingogriebsch.hateoas</groupId>
    <artifactId>spring-hateoas-siren</artifactId>
    <version>{see project page for current version}</version>
    <scope>compile</scope>
</dependency>
Gradle坐标
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'

4.8. 注册自定义媒体类型

Spring HATEOAS 允许您通过 SPI 集成自定义媒体类型。这种实现的构建块是:

  1. 某种形式的杰克逊ObjectMapper定制。在最简单的情况下,这是一个 JacksonModule实现。

  2. 一种LinkDiscoverer实现,以便客户端支持能够检测表示中的链接。

  3. 一小部分基础设施配置将允许 Spring HATEOAS 找到自定义实现并选择它。

4.8.1. 自定义媒体类型配置

自定义媒体类型实现由 Spring HATEOAS 通过扫描应用程序上下文以查找HypermediaMappingInformation接口的任何实现来获取。每个媒体类型都必须实现这个接口,以便:

定义你自己的媒体类型看起来很简单:

@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 模块通常为表示模型类型、和声明SerializerDeserializer实现。如果您需要进一步自定义 Jackson (如 custom ),您也可以覆盖.RepresentationModelEntityModelCollectionModelPagedModelObjectMapperHandlerInstantiatorconfigureObjectMapper(…)

参考文档的早期版本已经提到实现MediaTypeConfigurationProvider接口并将其注册到spring.factories. 这不是必需的。此 SPI 仅用于 Spring HATEOAS 提供的开箱即用媒体类型。只需实现HypermediaMappingInformation接口并将其注册为 Spring bean 即可。

4.8.2. 建议

实现媒体类型表示的首选方法是提供与预期格式匹配的类型层次结构,并且可以由 Jackson 按原样序列化。在为 注册的SerializerDeserializer实现中RepresentationModel,将实例转换为特定于媒体类型的模型类型,然后查找 Jackson 序列化程序。

默认支持的媒体类型使用与第三方实现相同的配置机制。所以值得研究包中mediatype实现。请注意,内置媒体类型实现将其配置类包保持为私有,因为它们是通过@EnableHypermediaSupport. 自定义实现可能应该将这些公开,以确保用户可以从他们的应用程序包中导入这些配置类。

5.配置

本节介绍如何配置 Spring HATEOAS。

5.1。使用@EnableHypermediaSupport

要让RepresentationModel子类型根据各种超媒体表示类型的规范进行渲染,您可以通过 激活对特定超媒体表示格式的支持@EnableHypermediaSupport。注释将HypermediaType枚举作为其参数。目前,我们支持HAL以及默认渲染。使用注释会触发以下操作:

  • EntityModel它注册必要的Jackson 模块以呈现CollectionModel超媒体特定格式。

  • 如果 JSONPath 在类路径上,它会自动注册一个实例以通过它们以纯 JSON 表示形式LinkDiscoverer查找链接(请参阅使用实例)。relLinkDiscoverer

  • 默认情况下,它启用实体链接并自动选择EntityLinks实现并将它们捆绑到DelegatingEntityLinks您可以自动装配的实例中。

  • 它会自动获取RelProvider中的所有实现ApplicationContext并将它们捆绑到DelegatingRelProvider您可以自动装配的中。它注册提供程序以考虑@Relation域类型以及 Spring MVC 控制器。如果EVO 变形器rel位于类路径上,则使用库中实现的复数算法派生集合值(请参阅[spis.rel-provider])。

5.1.1。明确启用对专用 Web 堆栈的支持

默认情况下,@EnableHypermediaSupport将反射性地检测您正在使用的 Web 应用程序堆栈,并连接到为那些注册的 Spring 组件以启用对超媒体表示的支持。但是,在某些情况下,您只想明确地想要激活对特定堆栈的支持。例如,如果您的基于 Spring WebMVC 的应用程序使用 WebFlux'WebClient发出传出请求,并且该请求不应该与超媒体元素一起使用,您可以通过在配置中显式声明 WebMVC 来限制要启用的功能:

示例 42. 显式激活对特定 Web 堆栈的超媒体支持
@EnableHypermediaSupport(…, stacks = WebStack.WEBMVC)
class MyHypermediaConfiguration { … }

6. 客户端支持

本节介绍 Spring HATEOAS 对客户端的支持。

6.1。特拉弗森

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标头的媒体类型来设置实例。然后,您可以定义要发现和遵循的关系名称。关系名称可以是简单名称或 JSONPath 表达式(以 开头$)。

然后样本将参数映射传递给Traverson实例。这些参数用于扩展遍历过程中找到的 URI(模板化)。通过访问最终遍历的表示来结束遍历。在前面的示例中,我们评估 JSONPath 表达式以访问演员的姓名。

前面的示例是最简单的遍历版本,其中rel值是字符串,并且在每一跳处应用相同的模板参数。

有更多选项可以在每个级别自定义模板参数。以下示例显示了这些选项。

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()是可链接的,这意味着您可以将多个跃点串在一起,如前面的示例所示。您可以放置​​多个基于字符串的rel值 ( follow("items", "item")) 或具有特定参数的单个跃点。

6.1.1. EntityModel<T>对比CollectionModel<T>

到目前为止显示的示例演示了如何回避 Java 的类型擦除并将单个 JSON 格式的资源转换为EntityModel<Item>对象。但是,如果你得到一个像\_embeddedHAL 集合这样的集合呢?您只需稍作调整即可做到这一点,如以下示例所示:

CollectionModelType<Item> collectionModelType =
    TypeReferences.CollectionModelType<Item>() {};

CollectionModel<Item> itemResource = traverson.//
    follow(rel("items")).//
    toObject(collectionModelType);

这不是获取单个资源,而是将集合反序列化为CollectionModel.

使用启用超媒体的表示时,一项常见任务是在其中找到具有特定关系类型的链接。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图所示:

例子 43. 配置一个WebClient你自己
@Bean
WebClient.Builder hypermediaWebClient(HypermediaWebClientConfigurer configurer) { (1)
 return configurer.registerHypermediaTypes(WebClient.builder()); (2)
}
1 在您的@Configuration类中,获取HypermediaWebClientConfigurerbean Spring HATEOAS 寄存器的副本。
2 创建后WebClient.Builder,使用配置器注册超媒体类型。
它用什么HypermediaWebClientConfigurer注册了所有正确的编码器和解码器WebClient.Builder。要使用它,您需要将构建器注入应用程序的某个位置,然后运行该build()方法以生成WebClient.

如果您使用的是 Spring Boot,还有另一种方法:将WebClientCustomizer.

示例 44. 让 Spring Boot 配置事物
@Bean (4)
WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) { (1)
    return webClientBuilder -> { (2)
        configurer.registerHypermediaTypes(webClientBuilder); (3)
    };
}
1 创建 Spring bean 时,请求 Spring HATEOAS 的HypermediaWebClientConfigurerbean 的副本。
2 使用 Java 8 lambda 表达式来定义WebClientCustomizer.
3 在函数调用内部,应用该registerHypermediaTypes方法。
4 将整个内容作为 Spring bean 返回,以便 Spring Boot 可以将其拾取并将其应用于其自动配置的WebClient.Builderbean。

在这个阶段,只要你需要一个具体的WebClient,只需注入WebClient.Builder你的代码,然后使用build(). 该WebClient实例将能够使用超媒体进行交互。

6.4. 配置WebTestClient实例

使用支持超媒体的表示时,一项常见任务是使用WebTestClient.

WebTestClient要在测试用例中配置一个实例,请查看以下示例:

示例 45.WebTestClient使用 Spring HATEOAS 时进行配置
@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是一个不可变的值类型,所以你不能就地改变它。HypermediaWebClientConfigurer返回一个突变的变体,然后您必须捕获该变体才能使用它。

如果您使用的是 Spring Boot,还有其他选项,如下所示:

示例 46.WebTestClient使用 Spring Boot 时进行配置
@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 Autowire Spring Boot 的WebTestClient.Builderintobuilder和 Spring HATEOAS 的配置器作为方法参数。
3 用于HypermediaWebTestClientConfigurer注册对超媒体的支持。
4 信号你想要一个EntityModel<Employee>返回 using TypeReferences

同样,您可以使用与前面示例类似的断言。

还有许多其他方法可以设计测试用例。WebTestClient可以绑定到控制器、函数和 URL。本节并不打算展示所有这些。相反,这为您提供了一些入门示例。重要的是,通过应用HypermediaWebTestClientConfigurerWebTestClient可以更改 的任何实例以处理超媒体。

6.5。配置 RestTemplate 实例

如果您想创建自己的副本并RestTemplate配置为使用超媒体,您可以使用HypermediaRestTemplateConfigurer

示例 47. 配置RestTemplate自己
/**
 * Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}.
 */
@Bean
RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { (1)
	return configurer.registerHypermediaTypes(new RestTemplate()); (2)
}
1 在您的@Configuration类中,获取HypermediaRestTemplateConfigurerbean Spring HATEOAS 寄存器的副本。
2 创建 后RestTemplate,使用配置器应用超媒体类型。

您可以自由地将这种模式应用于RestTemplate您需要的任何实例,无论是创建注册的 bean,还是在您定义的服务内部。

如果您使用的是 Spring Boot,还有另一种方法。

一般来说,Spring Boot 已经远离了RestTemplate在应用程序上下文中注册 bean 的概念。

  • 与不同的服务交谈时,您通常需要不同的凭据。

  • 使用RestTemplate底层连接池时,您会遇到其他问题。

  • 用户通常需要不同的实例而不是单个 bean。

为了弥补这一点,Spring Boot 提供了一个RestTemplateBuilder. 这个自动配置的 bean 允许您定义各种用于形成RestTemplate实例的 bean。您请求一个RestTemplateBuilderbean,调用它的build()方法,然后应用最终设置(例如凭据和其他详细信息)。

要注册基于超媒体的消息转换器,请将以下内容添加到您的代码中:

示例 48. 让 Spring Boot 配置事物
@Bean (4)
RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { (1)
    return restTemplate -> { (2)
        configurer.registerHypermediaTypes(restTemplate); (3)
    };
}
1 创建 Spring bean 时,请求 Spring HATEOAS 的HypermediaRestTemplateConfigurerbean 的副本。
2 使用 Java 8 lambda 表达式来定义RestTemplateCustomizer.
3 在函数调用内部,应用该registerHypermediaTypes方法。
4 将整个东西作为 Spring bean 返回,以便 Spring Boot 可以将其拾取并将其应用于其 autoconfigured RestTemplateBuilder

在这个阶段,只要你需要一个具体的RestTemplate,只需注入RestTemplateBuilder你的代码,然后使用build(). 该RestTemplate实例将能够使用超媒体进行交互。


1. see XML Configuration