该项目提供了一些 API,以便在使用 Spring,尤其是 Spring MVC 时,轻松创建遵循 HATEOAS 原则的 REST 表示。它试图解决的核心问题是链接创建和表示组装。

© 2012-2021 原始作者。

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

1. 前言

1.1. 迁移到 Spring HATEOAS 1.0

在 1.0 版本中,我们借此机会重新评估了 0.x 版本中做出的一些设计和包结构选择。我们收到了大量关于这方面的反馈,主要版本升级似乎是重构这些内容最自然的时机。

1.1.1. 变更

包结构的最大变化是由引入超媒体类型注册 API 驱动的,该 API 用于支持 Spring HATEOAS 中的其他媒体类型。这使得客户端和服务器 API(分别命名为相应包)以及 mediatype 包中的媒体类型实现清晰分离。

将您的代码库升级到新 API 的最简单方法是使用 迁移脚本。在开始之前,这里快速浏览一下变更。

表示模型

ResourceSupport/Resource/Resources/PagedResources 这组类的命名从未真正让人觉得恰当。毕竟,这些类型实际上并未体现资源本身,而是表示模型,可以利用超媒体信息和 affordances 进行丰富。下面是新旧名称的映射关系:

  • ResourceSupport 现在是 RepresentationModel

  • Resource 现在是 EntityModel

  • Resources 现在是 CollectionModel

  • PagedResources 现在是 PagedModel

因此,ResourceAssembler 已重命名为 RepresentationModelAssembler,其方法 toResource(…)toResources(…) 分别重命名为 toModel(…)toCollectionModel(…)。此外,名称变更也已反映在 TypeReferences 中包含的类中。

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

  • LinkDiscoverer API 已移至 client 包。

  • 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(…) 接受 Affordance 详细信息的方法已移至 Affordances。现在手动构建 Affordance 实例,请使用 Affordances.of(link).afford(…)。另请注意从 Affordances 中公开的新类型 AffordanceBuilder,以用于流畅的用法。详情请参见 Affordances

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

  • 如果属性值符合规范中定义的默认值,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 为根的类。它本质上是 Links 集合的容器,并具有方便的方法将这些 Links 添加到模型中。这些模型稍后可以渲染成各种媒体类型格式,这些格式将定义超媒体元素在表示中的样子。有关这方面的更多信息,请参阅 媒体类型

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

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

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

  String firstname, lastname;
}

泛型自类型是必要的,以便让 RepresentationModel.add(…) 方法返回其自身的实例。现在可以这样使用模型类型:

示例 9. 使用 person 表示模型
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. 为 person 表示模型生成的 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 被限制始终包含载荷,因此允许对单个实例上的类型 arrangement 进行推理,但 CollectionModel 的底层集合可能是空的。由于 Java 的类型擦除,我们无法实际检测到 CollectionModel<Person> model = CollectionModel.empty() 实际上是 CollectionModel<Person>,因为我们看到的只是运行时实例和一个空集合。可以通过在构造时通过 CollectionModel.empty(Person.class) 将缺失的类型信息添加到空实例中,或者作为底层集合可能为空情况下的备选方案。

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

3. 服务端支持

现在我们已经有了领域词汇,但主要挑战仍然存在:如何以一种不那么脆弱的方式创建要封装到 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 中引入了非复合样式。如果今天从头开始,我们可能会默认采用该样式,并宁愿让用户显式选择复合样式,而不是反过来。

TODO

3.3. Affordances

环境的 affordances 是它所提供的……它提供或赋予的,无论是好的还是坏的。“afford” 这个动词在词典中可以找到,但名词 “affordance” 则没有。是我创造的。

— James J. Gibson
知觉的生态学进路 (The Ecological Approach to Visual Perception)(第 126 页)

基于 REST 的资源不仅提供数据,还提供控制。构成灵活服务的最后一个要素是关于如何使用各种控制的详细 affordances。由于 affordances 与链接相关联,Spring HATEOAS 提供了一个 API,可以将所需的任意多个相关方法附加到链接。就像通过指向 Spring MVC 控制器方法来创建链接一样(详情请参见 在 Spring MVC 中构建链接),您可以 …​

以下代码展示了如何获取一个 self 链接并关联另外两个 affordances:

示例 13. 将 affordances 连接到 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 创建 self 链接。
2 updateEmployee 方法与 self 链接关联。
3 partiallyUpdateEmployee 方法与 self 链接关联。

使用 .andAffordance(afford(…​)),您可以使用控制器的方法将 PUT 和 PATCH 操作连接到 GET 操作。想象一下,上面 affords 的相关方法看起来像这样:

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

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

示例 16. 使用 Affordances API 手动注册 affordances
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 实例,从而创建描述 affordances 的上下文。
2 每个 affordance 都以它应该支持的 HTTP 方法开头。然后我们将一个类型注册为载荷描述,并显式命名该 affordance。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这实际上创建了与指向 EmployeeController.newEmployee(…) 所创建的 affordance 相同的功能。
3 下一个 affordance 的构建旨在反映指向 EmployeeController.search(…) 所发生的情况。在这里,我们将 Employee 定义为创建的响应的模型,并显式注册 QueryParameters。

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

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 应用程序,对应的 reactive 组件是 ForwardedHeaderTransformer

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

这将创建一个函数,用于转换 reactive 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);

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

3.5.1. 基于 Spring MVC 控制器的 EntityLinks

激活 entity links 功能会导致检查当前 ApplicationContext 中所有可用的 Spring MVC 控制器是否存在 @ExposesResourceFor(…) 注解。该注解暴露了控制器管理的模型类型。除此之外,我们假定您遵守以下 URI 映射设置和约定:

  • 一个类型级别的 @ExposesResourceFor(…) 注解,声明控制器暴露集合和项目资源的实体类型。

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

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

以下示例展示了 EntityLinks-capable 控制器的实现:

@Controller
@ExposesResourceFor(Order.class) (1)
@RequestMapping("/orders") (2)
class OrderController {

  @GetMapping (3)
  ResponseEntity orders(…) { … }

  @GetMapping("{id}") (4)
  ResponseEntity order(@PathVariable("id") … ) { … }
}
1 该控制器指示它正在暴露实体 Order 的集合和项目资源。
2 其集合资源暴露在 /orders 下。
3 该集合资源可以处理 GET 请求。您可以根据需要添加更多处理其他 HTTP 方法的方法。
4 一个额外的控制器方法,用于处理从属资源,该方法接受路径变量以暴露一个项目资源,即单个 Order

配置就绪后,当您在 Spring MVC 配置中启用 EntityLinks @EnableHypermediaSupport 时,可以按如下方式创建指向该控制器的链接:

@Controller
class PaymentController {

  private final EntityLinks entityLinks;

  PaymentController(EntityLinks entityLinks) { (1)
    this.entityLinks = entityLinks;
  }

  @PutMapping(…)
  ResponseEntity payment(@PathVariable Long orderId) {

    Link link = entityLinks.linkToItemResource(Order.class, orderId); (2)
    …
  }
}
1 在您的配置中注入由 @EnableHypermediaSupport 提供的 EntityLinks
2 使用 API 通过实体类型而非控制器类来构建链接。

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

3.5.2. EntityLinks API 详解

从根本上讲,EntityLinks 允许构建 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

@EnableHypermediaSupport 创建的 EntityLinks 实例类型为 DelegatingEntityLinks,它会依次选取 ApplicationContext 中所有可用的其他 EntityLinks 实现作为 bean。它被注册为主 bean (primary 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(…)

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

Person person = new Person(…);
Iterable<Person> people = Collections.singletonList(person);

PersonModelAssembler assembler = new PersonModelAssembler();
PersonModel model = assembler.toModel(person);
CollectionModel<PersonModel> model = assembler.toCollectionModel(people);

3.7. 表示模型处理器

有时,您需要在超媒体表示被汇编完成后对其进行调整和修改。

一个完美的例子是,您有一个处理订单履行的控制器,但您需要添加与付款相关的链接。

想象一下,您的订单系统生成了这种类型的超媒体:

{
  "orderId" : "42",
  "state" : "AWAITING_PAYMENT",
  "_links" : {
    "self" : {
      "href" : "http://localhost/orders/999"
    }
  }
}

您希望添加一个链接以便客户端可以进行付款,但又不想将关于 PaymentController 的细节混入 OrderController。与其污染您的订单系统细节,不如编写一个像这样的 RepresentationModelProcessor

public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> { (1)

  @Override
  public EntityModel<Order> process(EntityModel<Order> model) {

    model.add( (2)
        Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) //
            .expand(model.getContent().getOrderId()));

    return model; (3)
  }
}
1 此处理器将仅应用于 EntityModel<Order> 对象。
2 通过添加一个无条件链接来操作现有的 EntityModel 对象。
3 返回 EntityModel,以便它可以被序列化为请求的媒体类型。

向您的应用程序注册处理器:

@Configuration
public class PaymentProcessingApp {

  @Bean
  PaymentProcessor paymentProcessor() {
    return new PaymentProcessor();
  }
}

现在,当您发出 Order 的超媒体表示时,客户端会收到以下内容:

{
  "orderId" : "42",
  "state" : "AWAITING_PAYMENT",
  "_links" : {
    "self" : {
      "href" : "http://localhost/orders/999"
    },
    "payments" : { (1)
      "href" : "/payments/42" (2)
    }
  }
}
1 您会看到 LinkRelation.of("payments") 作为此链接的关系被插入。
2 URI 由处理器提供。

这个例子相当简单,但您可以轻松地:

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

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

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

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

3.7.1. 处理空集合模型

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

3.8. 使用 LinkRelationProvider API

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

  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 对象。属性名称是链接关系 (link relations),每个值是一个链接对象或一个链接对象数组

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

示例 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 Linking RFC 描述了注册和扩展链接关系类型。注册关系是注册到 IANA 链接关系类型注册表的知名字符串。扩展 rel URI 可用于不希望注册关系类型的应用程序。每个 URI 唯一标识关系类型。rel URI 可以序列化为紧凑 URI 或 Curie。例如,如果 ex 被定义为 example.com/rels/{rel},则 ex:persons 的 Curie 代表链接关系类型 example.com/rels/persons。如果使用了 Curie,则基本 URI 必须存在于响应范围内。

默认 RelProvider 创建的 rel 值是扩展关系类型,因此必须是 URI,这可能会带来很多开销。CurieProvider API 负责处理这个问题:它允许您将基本 URI 定义为 URI 模板,并定义代表该基本 URI 的前缀。如果存在 CurieProvider,则 RelProvider 会在所有未注册到 IANA 的 rel 值前加上 curie 前缀。此外,一个 curies 链接会自动添加到 HAL 资源中。

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

@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type= {HypermediaType.HAL})
public class Config {

  @Bean
  public CurieProvider curieProvider() {
    return new DefaultCurieProvider("ex", new UriTemplate("https://www.example.com/rels/{rel}"));
  }
}

请注意,现在 ex: 前缀会自动出现在所有未在 IANA 注册的 rel 值之前,例如 ex:orders。客户端可以使用 curies 链接将 curie 解析为其完整形式。以下示例展示了如何操作:

{
  "_links": {
    "self": {
      "href": "https://myhost/person/1"
    },
    "curies": {
      "name": "ex",
      "href": "https://example.com/rels/{rel}",
      "templated": true
    },
    "ex:orders": {
      "href": "https://myhost/person/1/orders"
    }
  },
  "firstname": "Dave",
  "lastname": "Matthews"
}

由于 CurieProvider API 的目的是允许自动创建 curie,因此每个应用程序范围只能定义一个 CurieProvider bean。

4.2. HAL-FORMS

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

HAL-FORMS“看起来像 HAL”。然而,重要的是要记住 HAL-FORMS 与 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" : "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 = "…") 属性,或在设置 affordance 时明确定义了媒体类型时,才包含此属性。

method

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

target

提交表单的目标 URI。仅当 affordance 目标与其声明的链接不同时,才渲染此属性。

title

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

properties

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

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

表 2. 属性属性
属性 描述

readOnly

如果属性没有 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

type

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

对于您无法手动注解的类型,可以通过 application context 中存在的 HalFormsConfiguration bean 注册自定义模式。

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

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

此设置将导致 CreditCardNumber 类型的表示模型属性的 HAL-FORMS 模板属性声明一个值为 [0-9]{16}regex 字段。

为属性定义 HAL-FORMS 选项

对于其值应该与特定超集值匹配的属性,HAL-FORMS 在属性定义中定义了 options 子文档。通过 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。这意味着您通常需要使用 affordance 描述的本地或完全限定的输入类型名称来限定键。

示例 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 使用实际 affordance 名称作为键的全局标题定义。除非在创建 affordance 时明确定义,否则此名称默认为创建 affordance 时指向的方法的名称。
3 本地定义的标题,将应用于所有名为 Employee 的类型。
4 使用完全限定类型名称的标题定义。
使用实际 affordance 名称的键优先于默认键。
属性提示

属性提示也可以通过 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 我们定义了一个 LocalDate 类型的 birthdate 属性。
2 我们期望 ccn 遵守正则表达式。
3 我们使用 JSR-303 @Email 注解将 email 定义为电子邮件。

请注意,此类型不是领域类型。它是特意设计用来捕获各种潜在的无效输入,以便可以一次性拒绝潜在错误的字段值。

接下来,让我们看看控制器如何使用该模型:

@Controller
class CustomerController {

  @PostMapping("/customers")
  EntityModel<?> createCustomer(@RequestBody CustomerRepresentation payload) { (1)
    // …
  }

  @GetMapping("/customers")
  CollectionModel<?> getCustomers() {

    CollectionModel<?> model = …;

    CustomerController controller = methodOn(CustomerController.class);

    model.add(linkTo(controller.getCustomers()).withSelfRel() (2)
      .andAfford(controller.createCustomer(null)));

    return ResponseEntity.ok(model);
  }
}
1 声明了一个控制器方法,用于在使用 POST 请求到 /customers 时,将请求体绑定到上面定义的表示模型。
2 /customers 发出 GET 请求会准备一个模型,为其添加一个 self 链接,并在此链接上额外声明一个指向映射到 POST 的控制器方法的 affordance。这将导致构建一个 affordance 模型,该模型(取决于最终要渲染的媒体类型)将被转换为媒体类型特定的格式。

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

rest-messages.properties 中声明的附加属性。
CustomerRepresentation._template.createCustomer.title=Create customer (1)
CustomerRepresentation.ccn._prompt=Credit card number (2)
CustomerRepresentation.ccn._placeholder=1234123412341234 (2)
1 我们为通过指向 createCustomer(…) 方法创建的模板定义了一个显式标题。
2 我们明确为 CustomerRepresentation 模型的 ccn 属性指定了一个提示和占位符。

如果客户端现在使用 Acceptapplication/prs.hal-forms+json/customers 发出 GET 请求,响应的 HAL 文档将被扩展为一个 HAL-FORMS 文档,包含以下 _templates 定义:

{
  …,
  "_templates" : {
    "default" : { (1)
      "title" : "Create customer", (2)
      "method" : "post", (3)
      "properties" : [ {
        "name" : "name",
        "required" : true,
        "type" : "text" (4)
      } , {
        "name" : "birthdate",
        "required" : true,
        "type" : "date" (4)
      } , {
        "name" : "ccn",
        "prompt" : "Credit card number", (5)
        "placeholder" : "1234123412341234" (5)
        "required" : true,
        "regex" : "[0-9]{16}", (6)
        "type" : "text"
      } , {
        "name" : "email",
        "prompt" : "Email",
        "required" : true,
        "type" : "email" (7)
      } ]
    }
  }
}
1 暴露了一个名为 default 的模板。它的名称是 default,因为它定义的唯一模板,规范要求使用该名称。如果附加了多个模板(通过声明额外的 affordances),它们将分别以它们指向的方法命名。
2 模板标题源自资源包中定义的值。请注意,根据随请求发送的 Accept-Language 头和可用性,可能会返回不同的值。
3 method 属性的值源自 derive affordance 的方法的映射。
4 type 属性的值 text 源自属性的类型 Stringbirthdate 属性也同样适用,但结果是 date
5 ccn 属性的提示和占位符也源自资源包。
6 ccn 属性的 @Pattern 声明暴露为模板属性的 regex 属性。
7 email 属性上的 @Email 注解已转换为相应的 type 值。

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

4.3. HTTP 问题详情

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

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

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

使用 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 或显式对象添加(见下文)。

要为自定义属性使用专用对象,请声明一个类型,创建并填充其实例,然后通过 ….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 {

}

此配置将使您的应用程序响应带有 Acceptapplication/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 - Uniform Basis for Exchanging Representations

UBER 是一种实验性的 JSON 规范。

UBER 文档格式是一种极简的读/写超媒体类型,旨在支持简单的状态转移和特设的基于超媒体的转换。

—— Mike Amundsen
UBER 规范

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

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

}

此配置将使您的应用程序响应使用 Acceptapplication/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 - Application-Level Profile Semantics

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 通过扫描应用上下文(application context)来查找 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 模块通常声明用于表示模型类型 RepresentationModelEntityModelCollectionModelPagedModelSerializerDeserializer 实现。如果需要进一步定制 Jackson ObjectMapper(例如自定义的 HandlerInstantiator),您可以选择覆盖 configureObjectMapper(…​) 方法。

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

4.8.2. 建议

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

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

5. 配置

本节介绍如何配置 Spring HATEOAS。

5.1. 使用 @EnableHypermediaSupport

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

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

  • 如果 classpath 中存在 JSONPath,它会自动注册一个 LinkDiscoverer 实例,以便在纯 JSON 表示中按其 rel 查找链接(参见 使用 LinkDiscoverer 实例)。

  • 默认情况下,它会启用 entity links,并自动查找 EntityLinks 实现并将它们捆绑到一个 DelegatingEntityLinks 实例中,您可以对其进行 autowire。

  • 它自动查找 ApplicationContext 中的所有 RelProvider 实现,并将它们捆绑到一个 DelegatingRelProvider 实例中,您可以对其进行 autowire。它注册提供者来考虑域类型上的 @Relation 以及 Spring MVC 控制器。如果 classpath 中存在 EVO inflector,集合 rel 值将使用该库中实现的复数化算法推导得到(参见 [spis.rel-provider])。

5.1.1. 显式启用对特定 Web 栈的支持

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

示例 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("http://localhost:8080/api/"), MediaTypes.HAL_JSON);
String name = traverson
    .follow("movies", "movie", "actor").withTemplateParameters(parameters)
    .toObject("$.name");

您可以设置一个 Traverson 实例,通过将其指向一个 REST 服务器并配置您想作为 Accept 头发送的媒体类型。然后,您可以定义您想发现和跟踪的关系名(relation names)。关系名可以是简单的名称,也可以是 JSONPath 表达式(以 $ 开头)。

然后,该示例将一个参数 map 传递给 Traverson 实例。这些参数用于展开在遍历过程中找到的 URI(这些 URI 是模板化的)。遍历通过访问最终遍历结果的表示来结束。在前面的示例中,我们评估一个 JSONPath 表达式来访问 actor 的名字。

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

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

ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};

EntityModel<Item> itemResource = traverson.//
    follow(rel("items").withParameter("projection", "noImages")).//
    follow("$._embedded.items[0]._links.self.href").//
    toObject(resourceParameterizedTypeReference);

静态的 rel(…​) 函数是定义单个 Hop 的便捷方式。使用 .withParameter(key, value) 可以轻松指定 URI 模板变量。

.withParameter() 返回一个新的 Hop 对象,该对象支持链式调用。您可以随意将任意数量的 .withParameter 调用串联起来。结果是一个单独的 Hop 定义。以下示例展示了一种方法
ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};

Map<String, Object> params = Collections.singletonMap("projection", "noImages");

EntityModel<Item> itemResource = traverson.//
    follow(rel("items").withParameters(params)).//
    follow("$._embedded.items[0]._links.self.href").//
    toObject(resourceParameterizedTypeReference);

您还可以加载整个参数 Map,通过使用 .withParameters(Map) 方法。

follow() 方法支持链式调用,这意味着您可以串联多个 hop,如前面的示例所示。您可以放入多个基于字符串的 rel 值(follow("items", "item")),或者一个带有特定参数的单个 hop。

6.1.1. EntityModel<T>CollectionModel<T>

到目前为止展示的示例演示了如何规避 Java 的类型擦除,并将单个 JSON 格式的资源转换为一个 EntityModel<Item> 对象。但是,如果您得到一个像 \_embedded HAL 集合那样的集合怎么办?您只需稍作调整即可实现,如下面的示例所示

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

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

与获取单个资源不同,这个示例将一个集合反序列化到 CollectionModel 中。

在使用支持超媒体的表示时,一个常见的任务是查找包含特定关系类型(relation type)的链接。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 后,使用该 configurer 来注册超媒体类型。
HypermediaWebClientConfigurer 的作用是向 WebClient.Builder 注册所有正确的编码器和解码器。要使用它,您需要将该 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("http://localhost/employees").accept(HAL_JSON) //
      .exchange() //
      .expectStatus().isOk() //
      .expectBody(new TypeReferences.CollectionModelType<EntityModel<Employee>>() {}) (3)
      .consumeWith(result -> {
        CollectionModel<EntityModel<Employee>> model = result.getResponseBody(); (4)

        // Assert against the hypermedia model.
        assertThat(model.getRequiredLink(IanaLinkRelations.SELF)).isEqualTo(Link.of("http://localhost/employees"));
        assertThat(model.getContent()).hasSize(2);
      });
}
1 注册您的配置类,该类使用 @EnableHypermediaSupport 以启用 HAL 支持。
2 使用 HypermediaWebTestClientConfigurer 来应用超媒体支持。
3 请求一个类型为 CollectionModel<EntityModel<Employee>> 的响应,使用 Spring HATEOAS 的 TypeReferences.CollectionModelType 帮助类。
4 在获取到 Spring HATEOAS 格式的“body”后,对其进行断言!
WebTestClient 是一个不可变的值类型(immutable value type),因此您不能原地修改它。HypermediaWebClientConfigurer 返回一个变异后的变体,您必须捕获它才能使用。

如果您使用 Spring Boot,还有其他选项,例如这样

示例 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 autowire 到 builder 中,并将 Spring HATEOAS 的 configurer 作为方法参数。
3 使用 HypermediaWebTestClientConfigurer 来注册对超媒体的支持。
4 表明您希望返回一个 EntityModel<Employee>,使用 TypeReferences

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

还有许多其他方法来构建测试用例。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 后,使用该 configurer 来应用超媒体类型。

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

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

通常,Spring Boot 已经不再推荐在应用上下文(application context)中注册 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 实例将能够使用超媒体进行交互。