本项目提供了一些 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/Resource/Resources/PagedResources 这组类的名称从未真正感到恰当。毕竟,这些类型实际上并未体现资源,而是可以富含超媒体信息和可供性的表示模型。以下是新名称与旧名称的映射方式

  • 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 包。

  • LinkBuilderEntityLinks API 已移至 server 包。

  • ControllerLinkBuilder 已移至 server.mvc 并已弃用,由 WebMvcLinkBuilder 取代。

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

  • 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(…)。另请注意 Affordances 中公开的新 AffordanceBuilder 类型,以便于流畅使用。有关详细信息,请参阅可供性

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

  • HAL Forms 现在不再渲染属性属性,如果其值符合规范中定义的默认值。也就是说,如果之前 required 明确设置为 false,我们现在只是省略 required 的条目。我们现在也只对使用 PATCH 作为 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 的实例。

互联网号码分配机构包含一组预定义的链接关系。它们可以通过 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. 表示模型

为了方便地创建富含超媒体的表示,Spring HATEOAS 提供了一组以 RepresentationModel 为根的类。它本质上是 Link 集合的容器,并具有方便的方法将这些链接添加到模型中。模型以后可以渲染成各种媒体类型格式,这些格式将定义超媒体元素在表示中的外观。有关更多信息,请参阅媒体类型

示例 7. RepresentationModel 类层次结构
diagram classes

使用 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 控制器返回此类实例,并且客户端发送了设置为 application/hal+jsonAccept 头部,则响应将如下所示

示例 10. 为人员表示模型生成的 HAL 表示
{
  "_links" : {
    "self" : {
      "href" : "https://myhost/people/42"
    }
  },
  "firstname" : "Dave",
  "lastname" : "Matthews"
}

2.4.1. 实体资源表示模型

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

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

2.4.2. 集合资源表示模型

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

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

虽然 EntityModel 始终受限于包含有效负载,因此允许在单个实例上推断类型排列,但 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. 服务器端支持

现在我们已经有了领域词汇,但主要的挑战仍然存在:如何以一种不易出错的方式创建要包装到 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,它允许您通过指向控制器类来创建链接。以下示例展示了如何实现

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 在底层使用 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);

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

待办事项

3.3. 可供性(Affordances)

环境的可供性是它提供的东西……它提供或提供的好与坏。动词“to afford”在词典中可以找到,但名词“affordance”没有。我创造了它。

— 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(…​)),您可以使用控制器的方法将 PUTPATCH 操作连接到 GET 操作。想象一下,上面可供的相关方法看起来像这样

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

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

3.3.1. 手动构建可供性

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

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

可供性由媒体类型特定的可供性模型支持,这些模型将通用可供性元数据转换为特定的表示。请务必查看媒体类型部分中有关可供性的部分,以找到有关如何控制该元数据暴露的更多详细信息。

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,如此处所述。

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

这将创建一个 servlet 过滤器,用于处理所有 X-Forwarded-… 头部。它将正确地与 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 应用程序提供开箱即用的功能。EntityLinks SPI 中定义的契约最初旨在用于 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);

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

3.5.1. 基于 Spring MVC 控制器的 EntityLinks

激活实体链接功能会导致检查当前 ApplicationContext 中所有可用的 Spring MVC 控制器,以查找 @ExposesResourceFor(…) 注解。该注解公开了控制器管理哪种模型类型。除此之外,我们假设您遵循以下 URI 映射设置和约定

  • 一个类型级别的 @ExposesResourceFor(…),声明控制器为其公开集合和项目资源的实体类型。

  • 一个类级别的基本映射,表示集合资源。

  • 一个附加的方法级别映射,扩展映射以将标识符作为附加路径段追加。

以下示例显示了支持 EntityLinks 的控制器的实现

@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 允许构建 LinkBuilderLink 实例,指向实体类型的集合和项目资源。以 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

@EnableHypermediaSupport 创建的 EntityLinks 实例类型为 DelegatingEntityLinks,它将反过来获取 ApplicationContext 中所有其他可用的 EntityLinks 实现作为 bean。它被注册为主要 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. 添加一个 relself 的链接,指向正在渲染的资源。

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(…) 方法允许您创建资源实例,并向其添加一个 relselfLink。该链接的 href 由配置的控制器的请求映射加上实体 ID 确定(例如,/people/1)。

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

然后,您可以使用装配器来装配 RepresentationModelCollectionModel。以下示例创建 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" : "https:///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" : "https:///orders/999"
    },
    "payments" : { (1)
      "href" : "/payments/42" (2)
    }
  }
}
1 您看到 LinkRelation.of("payments") 被插入为该链接的关系。
2 URI 由处理器提供。

这个例子很简单,但您可以轻松地

  • 使用 WebMvcLinkBuilderWebFluxLinkBuilder 构造指向 PaymentController 的动态链接。

  • 注入任何必要的服务,以根据状态有条件地添加其他链接(例如 cancelamend)。

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

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

3.7.1. 处理空集合模型

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

3.8. 使用 LinkRelationProvider API

在构建链接时,您通常需要确定要用于链接的关系类型。在大多数情况下,关系类型直接与(域)类型关联。我们将查找关系类型的详细算法封装在 LinkRelationProvider API 后面,该 API 允许您确定单个和集合资源的关系类型。查找关系类型的算法如下

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

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

  3. 如果 classpath 中存在 EVO inflector JAR,我们使用复数化算法提供的单个资源 rel 的复数。

  4. @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 实例。其基本假设如下

  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 进行自定义(有关详细信息,请参阅使用 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 对象。属性名称是链接关系,每个值都是链接对象或链接对象数组

对于具有两个或更多链接的给定链接关系,规范对其表示方式明确

示例 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"
}

如果您希望自定义此策略,只需将 HalConfiguration bean 注入到您的应用程序配置中即可。有多种选择。

示例 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 的 wither 方法都可以组合成一个全面的策略。请务必广泛测试您的 API,以避免意外。

4.1.3. 链接标题国际化

HAL 为其链接对象定义了一个 title 属性。这些标题可以通过使用 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. 使用 CurieProvider API

Web 链接 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 会将 curie 前缀添加到所有未在 IANA 注册的 rel 值。此外,HAL 资源会自动添加一个 curies 链接。

以下配置定义了一个默认的 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 与 HAL 不同——两者绝不应被视为可以互换。

— Mike Amundsen
HAL-FORMS 规范

要启用此媒体类型,请在您的代码中添加以下配置

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

}

每当客户端提供带有 application/prs.hal-forms+jsonAccept 头部时,您都可以预期如下内容

示例 33. HAL-FORMS 示例文档
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "role" : "ring bearer",
  "_links" : {
    "self" : {
      "href" : "https://: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 属性的详细信息。阅读可供性 API,为您的控制器添加此额外元数据。

对于单项(EntityModel)和聚合根集合(CollectionModel),Spring HATEOAS 的渲染方式与HAL 文档相同。

4.2.1. 定义 HAL-FORMS 元数据

HAL-FORMS 允许描述每个表单字段的标准。Spring HATEOAS 允许通过为输入和输出类型塑造模型类型并在其上使用注解来自定义这些标准。

每个模板将定义以下属性

表 1. 模板属性
属性 描述

contentType

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

method

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

target

提交表单的目标 URI。仅当可供性目标与声明它的链接不同时才会渲染。

title

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

属性

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

每个属性将定义以下属性

表 2. 属性属性
属性 描述

只读

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

regex

可以通过在字段或类型上使用 JSR-303 的 @Pattern 注解进行自定义。在后一种情况下,模式将用于声明为该特定类型的所有属性。默认情况下不渲染。

required

可以通过使用 JSR-303 的 @NotNull 进行自定义。默认情况下不渲染,因此默认为 false。使用 PATCH 作为方法的模板将自动将所有属性设置为非必需。

max

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

maxLength

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

min

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

minLength

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

options

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

prompt

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

placeholder

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

类型

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

对于无法手动注解的类型,您可以通过应用程序上下文中存在的 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 子文档。可以使用 HalFormsConfigurationwithOptions(…) 方法来描述某个属性可用的选项,该方法接受指向类型属性的指针和用于将 PropertyMetadata 转换为 HalFormsOptions 实例的创建器函数。

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

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

请注意我们如何将选项值 FedExDHL 设置为 Order.shippingMethod 属性的选择选项。另外,HalFormsOptions.remote(…) 可以指向提供动态值的远程资源。有关选项设置的更多限制,请参阅规范HalFormsOptions 的 Javadoc。

4.2.2. 表单属性的国际化

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

模板标题

要定义模板标题,请使用以下模式:_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 使用 default 作为键的标题的全局定义。
2 使用实际可供性名称作为键的标题的全局定义。除非在创建可供性时明确定义,否则此名称默认为创建可供性时指向的方法的名称。
3 应用于所有名为 Employee 的类型的局部定义标题。
4 使用完全限定类型名称的标题定义。
使用实际可供性名称的键优先于默认键。
属性提示

属性提示也可以通过 Spring HATEOAS 自动配置的 rest-messages 资源包进行解析。键可以全局、局部或完全限定定义,并且需要将 ._prompt 连接到实际属性键

示例 35. 为 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.EmployeefirstName 属性将分配提示“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 我们定义了一个类型为 LocalDatebirthdate 属性。
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 声明了一个控制器方法,如果向 /customers 发出 POST 请求,则使用上面定义的表示模型将请求体绑定到它。
2 /customersGET 请求准备一个模型,向其添加一个 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 我们明确为 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,因为它是唯一定义的模板,并且规范要求使用该名称。如果附加了多个模板(通过声明额外的可供性),它们将各自以其指向的方法命名。
2 模板标题来自资源包中定义的值。请注意,根据请求发送的 Accept-Language 头部和可用性,可能会返回不同的值。
3 method 属性的值来自派生可供性方法的映射。
4 type 属性的值 text 源自属性类型 Stringbirthdate 属性也适用,但结果为 date
5 ccn 属性的提示和占位符也来自资源包。
6 ccn 属性的 @Pattern 声明作为模板属性的 regex 属性公开。
7 email 属性上的 @Email 注解已转换为相应的 type 值。

HAL-FORMS 模板被例如 HAL Explorer 考虑,它会自动从这些描述渲染 HTML 表单。

4.3. HTTP 问题详情

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

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

您可以通过在 Spring MVC 控制器中使用 Problem 媒体类型域类型来创建此类 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 您可以定义媒体类型定义的默认属性的值,例如类型 URI、标题和详细信息,使用 Spring 的国际化功能(参见上文)。
3 可以通过 Map 或显式对象添加自定义属性(参见下文)。

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

使用专用类型捕获扩展问题属性
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. Collection+JSON

Collection+JSON 是一个 JSON 规范,已在 IANA 批准的媒体类型 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": "[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 渲染 EntityModel 时,它将

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

  • 将模型的其余链接放入顶级 links 和项目级 links

  • EntityModel 中提取属性并将其转换为……

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

Spring HATEOAS 更具体地会

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

  • CollectionModel 链接(减去 self)将放入顶级 links 中。

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

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

4.5. UBER - 统一表示交换基础

UBER 是一个实验性的 JSON 规范

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

— 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 无需特殊激活。相反,您“构建”一个 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
{
  "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

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. Siren

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. 某种形式的 Jackson ObjectMapper 自定义。在最简单的情况下,它是一个 Jackson Module 实现。

  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 模块通常为表示模型类型 RepresentationModelEntityModelCollectionModelPagedModel 声明 SerializerDeserializer 实现。如果您需要进一步自定义 Jackson ObjectMapper(例如自定义 HandlerInstantiator),您可以选择覆盖 configureObjectMapper(…)

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

4.8.2. 建议

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

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

5. 配置

本节介绍如何配置 Spring HATEOAS。

5.1. 使用 @EnableHypermediaSupport

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

  • 它注册必要的 Jackson 模块,以超媒体特定格式渲染 EntityModelCollectionModel

  • 如果 JSONPath 在类路径上,它会自动注册一个 LinkDiscoverer 实例,以在纯 JSON 表示中按其 rel 查找链接(请参阅 使用 LinkDiscoverer 实例)。

  • 默认情况下,它启用 实体链接,并自动拾取 EntityLinks 实现,并将它们打包到一个您可以自动装配的 DelegatingEntityLinks 实例中。

  • 它会自动拾取 ApplicationContext 中的所有 RelProvider 实现,并将它们打包到一个您可以自动装配的 DelegatingRelProvider 中。它注册提供程序以考虑领域类型上的 @Relation 以及 Spring MVC 控制器。如果 EVO inflector 在类路径上,集合 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. Traverson

Spring HATEOAS 为客户端服务遍历提供了 API。它受到 Traverson JavaScript 库的启发。以下示例展示了如何使用它:

Map<String, Object> parameters = new HashMap<>();
parameters.put("user", 27);

Traverson traverson = new Traverson(URI.create("https://:8080/api/"), MediaTypes.HAL_JSON);
String name = traverson
    .follow("movies", "movie", "actor").withTemplateParameters(parameters)
    .toObject("$.name");

您可以通过将其指向 REST 服务器并配置要设置为 Accept 头的媒体类型来设置 Traverson 实例。然后,您可以定义要发现和遵循的关系名称。关系名称可以是简单名称或 JSONPath 表达式(以 $ 开头)。

然后,示例将参数映射传递给 Traverson 实例。这些参数用于扩展在遍历期间找到的 URI(这些 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);

您还可以使用 .withParameters(Map) 加载整个参数 Map

follow() 是可链式调用的,这意味着您可以连接多个跳跃,如前面的示例所示。您可以放置多个基于字符串的 rel 值(follow("items", "item"))或具有特定参数的单个跳跃。

6.1.1. EntityModel<T> vs. CollectionModel<T>

到目前为止所示的示例演示了如何规避 Java 的类型擦除,并将单个 JSON 格式的资源转换为 EntityModel<Item> 对象。但是,如果您获得一个像 \_embedded HAL 集合这样的集合怎么办?您可以仅通过一个小小的调整来实现,如下面的示例所示:

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

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

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

在使用启用超媒体的表示时,一个常见的任务是查找其中具有特定关系类型的链接。Spring HATEOAS 为默认表示渲染或开箱即用的 HAL 提供了基于 JSONPathLinkDiscoverer 接口实现。当使用 @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 类中,获取 Spring HATEOAS 注册的 HypermediaWebClientConfigurer bean 的副本。
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 的 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 实例,请查看此示例:

示例 45. 使用 Spring HATEOAS 时配置 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("https:///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("https:///employees"));
        assertThat(model.getContent()).hasSize(2);
      });
}
1 注册使用 @EnableHypermediaSupport 启用 HAL 支持的配置类。
2 使用 HypermediaWebTestClientConfigurer 应用超媒体支持。
3 使用 Spring HATEOAS 的 TypeReferences.CollectionModelType 助手请求 CollectionModel<EntityModel<Employee>> 响应。
4 以 Spring HATEOAS 格式获取“body”后,对其进行断言!
WebTestClient 是一个不可变值类型,因此您无法原地更改它。HypermediaWebClientConfigurer 返回一个已更改的变体,您必须捕获它才能使用它。

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

示例 46. 使用 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 自动装配到 builder 中,并将 Spring HATEOAS 的配置器作为方法参数。
3 使用 HypermediaWebTestClientConfigurer 注册对超媒体的支持。
4 使用 TypeReferences 指示您想要返回 EntityModel<Employee>

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

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

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 类中,获取 Spring HATEOAS 注册的 HypermediaRestTemplateConfigurer bean 的副本。
2 创建 RestTemplate 后,使用配置器应用超媒体类型。

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

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

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

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

  • RestTemplate 使用底层连接池时,您会遇到额外的问题。

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

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

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

示例 48. 让 Spring Boot 配置事物
@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 实例将能够使用超媒体进行交互。

© . This site is unofficial and not affiliated with VMware.