# Spring Hateoas-参考文献

# 1.前言

# 1.1.迁移到 Spring Hateoas1.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实例。它还被转换为自绑定泛型类型,以允许向实例添加链接的方法返回实例本身。

  • LinkDiscovererAPI 已被移动到client包中。

  • LinkBuilderEntityLinksAPI 已被移动到server包中。

  • ControllerLinkBuilder已被移动到server.mvc中,并且不推荐将其替换为WebMvcLinkBuilder

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

  • VndError已被移动到mediatype.vnderror包中。

# 1.1.2.迁移脚本

你可以在应用程序根目录中找到a script (opens new window)以运行,该根目录将更新所有导入语句和静态方法引用,使其指向在我们的源代码存储库中移动的 Spring Hateoas 类型。只需下载它,从你的项目根目录运行它。默认情况下,它将检查所有 Java 源文件,并用新的 Hateoas 类型引用替换旧的 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.0m3 迁移到 1.0rc1

  • Link.andAffordance(…)可提供的细节已移至Affordances。要手动构建Affordance实例,现在使用Affordances.of(link).afford(…)。还请注意从Affordances公开的新AffordanceBuilder类型,以便流畅地使用。详见启示

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

  • HAL 窗体现在不呈现属性属性,如果它们的值符合规范中定义的默认值。也就是说,如果以前required显式地设置为false,那么我们现在省略required的条目。我们现在也只强制那些使用PATCH作为 HTTP 方法的模板不需要它们。

# 2.基本原理

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

# 2.1.链接

超媒体的基本思想是用超媒体元素来丰富资源的表示。最简单的形式是链接。它们指示客户机可以导航到特定的资源。相关资源的语义是在所谓的链接关系中定义的。你可能已经在 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 (opens new window)中定义的其他属性。你可以通过在Link实例上调用相应的 wither 方法来设置它们。

Building links in Spring MVCBuilding links in Spring WebFlux中找到有关如何创建指向 Spring MVC 和 Spring WebFlux 控制器的链接的更多信息。

# 2.2.URI 模板

对于 Spring HateoasLink,超文本引用不仅可以是一个 URI,而且根据RFC-6570 (opens new window)也可以是一个 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}");

# 2.3.链接关系

要表示目标资源与当前资源的关系,可以使用所谓的链接关系。 Spring Hateoas 提供了一个LinkRelation类型来轻松地创建基于String的实例。

# 2.3.1.IANA 链接关系

因特网分配号码管理局包含一组预定义的链接关系 (opens new window)。它们可以通过IanaLinkRelations引用。

例 6.使用 IANA 链接关系

Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);

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

# 表示模型

Spring 为了方便地创建富含超媒体的表示,Hateoas 提供了一组在其根上带有RepresentationModel的类。它基本上是一个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.为人员表示模型生成的 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);

# 3.服务器端支持

# 在 Spring MVC 中构建链接

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

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

@Controller
class PersonController {

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

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

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

  • 要创建一个绝对 URI,你需要查找协议、主机名、端口、 Servlet 基和其他值。这很麻烦,并且需要难看的手工字符串连接代码。

  • 你可能不希望在基础 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](http://localhost:8080/your-app),那么这正是你正在构建附加部分的 URI。构建器现在检查给定的控制器类的根映射,并以[localhost:8080/your-app/people](http://localhost:8080/your-app/people)结束。你还可以构建更多的嵌套链接。下面的示例展示了如何做到这一点:

Person person = new Person(1L, "Dave", "Matthews");
//                 /person                 /     1
Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel();
assertThat(link.getRel(), is(IanaLinkRelation.SELF.value()));
assertThat(link.getHref(), endsWith("/people/1"));

构建器还允许创建 URI 实例来构建(例如,响应标头值):

HttpHeaders headers = new HttpHeaders();
headers.setLocation(linkTo(PersonController.class).slash(person).toUri());

return new ResponseEntity<PersonModel>(headers, HttpStatus.CREATED);

# 3.1.1.建立指向方法的链接

你甚至可以构建指向方法的链接,或者创建虚拟控制器方法调用。第一种方法是将Method实例交给WebMvcLinkBuilder。下面的示例展示了如何做到这一点:

Method method = PersonController.class.getMethod("show", Long.class);
Link link = linkTo(method, 2L).withSelfRel();

assertThat(link.getHref()).endsWith("/people/2"));

这仍然有点令人不满意,因为我们必须首先获得一个Method实例,该实例抛出一个异常,并且通常非常麻烦。至少我们不会重复映射。一种更好的方法是在控制器代理上对目标方法进行虚拟方法调用,我们可以使用methodOn(…)助手来创建该方法。下面的示例展示了如何做到这一点:

Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();

assertThat(link.getHref()).endsWith("/people/2");

methodOn(…)创建控制器类的代理,该代理记录方法调用,并在为方法的返回类型创建的代理中公开它。这允许对我们想要获得的映射的方法进行 Fluent 表达式。然而,在使用这种技术可以获得的方法上有一些限制:

  • 返回类型必须能够代理,因为我们需要在其上公开方法调用。

  • 传递到方法中的参数通常被忽略(通过@PathVariable引用的参数除外,因为它们构成了 URI)。

# 3.2.在 Spring WebFlux 中构建链接

TODO

# 3.3.启示

环境所能提供的就是它所提供的……它所提供的或提供的,不管是好是坏。词典中有动词“to afford”,但名词“affordance”没有。这是我编的。

——James J.Gibson

视觉感知的生态学方法(第 126 页)

基于 REST 的资源不仅提供数据,还提供控制。形成灵活服务的最后一个要素是关于如何使用各种控件的详细说明启示。 Spring 由于提供与链接相关联,Hateoas 提供了一个 API,以便根据需要将尽可能多的相关方法附加到链接上。正如你可以通过指向 Spring MVC 控制器方法来创建链接一样(有关详细信息,请参见 Building links in 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.响应updateEmpoyeePUT /employees/{id}方法

@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.人工构建可供参考的功能

虽然注册链接的主要方法是提供支持,但可能有必要手动构建其中的一些。这可以通过使用AffordancesAPI 来实现:

例 16.使用AffordancesAPI 手动注册启示

var methodInvocation = methodOn(EmployeeController.class).all();

var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) (1)

    .afford(HttpMethod.POST) (2)
    .withInputAndOutput(Employee.class) //
    .withName("createEmployee") //

    .andAfford(HttpMethod.GET) (3)
    .withOutput(Employee.class) //
    .addParameters(//
        QueryParameter.optional("name"), //
        QueryParameter.optional("role")) //
    .withName("search") //

    .toLink();
1 首先,从一个Link实例创建Affordances的实例,创建用于描述提供的上下文。
2 每一个启示都是从它应该支持的 HTTP 方法开始的。然后,我们将一种类型注册为有效负载描述,并显式地给出它的名称。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这有效地创建了与指向EmployeeController.newEmployee(…)的指针相同的启示。
3 构建下一个启示是为了反映指向EmployeeController.search(…)的指针所发生的情况。这里我们将Employee定义为创建并显式注册QueryParameters 的响应的模型。

启示由特定于媒体类型的启示模型支持,该模型将一般的启示元数据转换为特定的表示形式。请务必检查媒体类型部分中有关启示的部分,以找到有关如何控制该元数据的公开的更多详细信息。

# 3.4.转发头处理

RFC-7239 转发头 (opens new window)通常用于应用程序位于代理、负载均衡器或云中。实际接收 Web 请求的节点是基础设施的一部分,向前是对应用程序的请求。

你的应用程序可能运行在localhost:8080上,但是对于外部世界来说,你应该运行在reallycoolsite.com上(以及 Web 的标准端口 80 上)。通过使代理包括额外的头(许多人已经这样做了), Spring Hateoas 可以在使用 Spring 框架功能来获得原始请求的基本 URI 时正确地生成链接。

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

要启用转发头处理,你需要在应用程序中注册 Spring MVC 的ForwardedHeaderFilter(detailshere (opens new window))或 Spring WebFlux 的ForwardedHeaderTransformer(detailshere (opens new window))。在 Spring 引导应用程序中,这些组件可以简单地声明为 Spring bean,如here (opens new window)所述。

例 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 应用程序提供开箱即用的。
EntityLinksSPI 中定义的契约最初是针对 Spring Web MVC 的,并不考虑反应器类型。
开发支持反应式编程的类似契约仍在进行中。

到目前为止,我们已经通过指向 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 配置中激活@EnableHypermediaSupport,通过依赖注入可以使用EntityLinks。这将导致EntityLinks的各种默认实现被注册。最基本的是ControllerEntityLinks,它检查 SpringMVC 控制器类。如果你想注册你自己的EntityLinks的实现,请查看本节

# 3.5.1.基于 Spring MVC 控制器的实体链接

激活实体链接功能将检查当前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实例。

从根本上说,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 使用抽取器进行链接查找。

由于控制器实现通常是围绕实体类型进行分组的,因此你经常会发现自己在整个控制器类中使用相同的提取器函数(有关详细信息,请参见详细介绍 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实例查找项目资源链接。

@EnableHypermediaSupport创建的EntityLinks实例的类型为DelegatingEntityLinks,它将在ApplicationContext中以 bean 的形式获取所有其他EntityLinks实现。它被注册为 primary Bean,因此当你通常注入EntityLinks时,它始终是唯一的注入候选。ControllerEntityLinks是将包含在设置中的默认实现,但是用户可以自由地实现和注册自己的实现。使那些可用于EntityLinks实例可用于注入是将你的实现注册为 Spring Bean 的问题。

例 23.声明自定义 EntityLinks 实现

@Configuration
class CustomEntityLinksConfiguration {

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

这种机制的可扩展性的一个例子是 Spring data rest 的[RepositoryEntityLinks](https://github.com/ Spring-projects/ Spring-data-rest/blob/3a0cba94a2cc8739375ECF 24086da2f7c3bbf038/ Spring-data-rest-webmvc/mvc/main/main/java/org/repositorframework/data/data/repositforemframework/rest/spramework/sprint/springmvc/springmvc/support/support/sup 同时,它甚至还公开了其他类型资源的其他查找方法。如果你想要利用这些,只需显式地注入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(…​)是你编写的代码,用于实例化给定PersonModel对象的Person对象。它应该只关注于设置属性,而不是填充Links

正如我们在前面的示例中所做的那样,将类设置为你提供了以下好处:

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

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

然后可以使用汇编程序来组装RepresentationModelCollectionModel。下面的示例创建了CollectionModelPersonModel实例:

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.表示模型处理器

有时,你需要在超媒体表示assembled之后对其进行调整。

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

想象一下,让你的订购系统产生这种类型的超媒体:

{
  "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.8.使用LinkRelationProviderAPI

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

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

  2. 如果不是,那么对于集合rel,我们默认使用未大写的简单类名加上附加的List

  3. 如果EVO 折弯机 (opens new window)JAR 在 Classpath 中,则使用由多元化算法提供的单个资源rel的复数形式。

  4. @Controller注释带@ExposesResourceFor的 classes(详见使用 EntityLinks 接口)透明地查找注释带中所配置类型的关系类型,这样就可以使用LinkRelationProvider.getItemResourceRelFor(MyController.class)并获取所公开的域类型的关系类型。

当你使用@EnableHypermediaSupport时,LinkRelationProvider会自动暴露为 Spring Bean。你可以通过实现接口并将它们依次以 Spring bean 的形式公开来插入自定义提供程序。

# 4.媒体类型

# 4.1.HAL-超文本应用程序语言

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

这是 Spring Hateoas 采用的第一种基于规范的媒体类型。

# 4.1.1.HAL 表示模型的构建

从 Spring Hateoas1.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进行定制(有关详细信息,请参见[使用LinkRelationProviderAPI](#server.rel-provider))。
{
  "_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" : {
    "orders : [
      … (1)
    ]
  }
}
1 个人订单文件放在这里。

创建这样的表示非常简单:

Collection<Order> orders = …;

HalModelBuilder.emptyHalDocument()
  .embed(orders);

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

如果你希望显式地通信一个空集合,则可以将一个类型传递到….embed(…)方法的重载中,并接受Collection。如果交给方法的集合是空的,这将导致呈现一个字段,其链接关系是从给定类型派生的。

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

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

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

# 4.1.2.配置链接呈现

在 HAL 中,_links条目是一个 JSON 对象。属性名是链接关系,每个值都是链接对象或链接对象数组 (opens new window)

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

例 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 通过将所有单链接关系呈现为数组,从而覆盖 Hateoas 的默认设置。

如果你只希望覆盖某些特定的链接关系,那么可以创建一个HalConfiguration Bean,如下所示:

例 28.基于链路关系的 HAL 单链路呈现策略

@Bean
public HalConfiguration linkRelationBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1)
      .withRenderSingleLinksFor( //
          LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2)
}
1 总是将item链接关系呈现为数组。
2 当只有一个链接时,将prev链接关系作为对象呈现。

如果这两种方法都不符合你的需求,那么你可以使用 Ant 样式的路径模式:

例 29.基于模式的 HAL 单链路呈现策略

@Bean
public HalConfiguration patternBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          "http*", RenderSingleLinks.AS_ARRAY); (1)
}
1 将所有以http开头的链接关系呈现为一个数组。
基于模式的方法使用 Spring 的AntPathMatcher

所有这些HalConfiguration威瑟斯可以组合成一个全面的政策。一定要广泛地测试你的 API,以避免出现意外。

# 4.1.3.链接标题国际化

HAL 为其链接对象定义了title属性。这些标题可以通过使用 Spring 的资源包抽象和名为rest-messages的资源包来填充,以便客户可以直接在其 UIS 中使用它们。这个包将被自动设置,并在 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"
    }
  }
}

# 使用CurieProviderAPI

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

由默认RelProvider创建的rel值是扩展关系类型,因此必须是 URI,这可能会导致大量的开销。CurieProviderAPI 负责这一点:它允许你将一个基本 URI 定义为一个 URI 模板和一个代表该基本 URI 的前缀。如果存在CurieProvider,则RelProvider前置所有带有居里前缀的rel值。此外,curies链接被自动地添加到 HAL 资源中。

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

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

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

注意,现在ex:前缀自动出现在所有未注册到 IANA 的 REL 值之前,如ex:orders。客户机可以使用curies链接将居里分解为完整形式。下面的示例展示了如何做到这一点:

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

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

# 4.2.HAL-表格

HAL-FORMS (opens new window)旨在向HAL 媒体类型添加运行时表单支持。

HAL-形式“看起来像 HAL。”然而,重要的是要记住,HAL 形式与 HAL 是不一样的——两者 不应被认为在任何方面都是可以互换的。

——Mike Amundsen

HAL-表格规格

要启用此媒体类型,请在代码中加入以下配置:

例 32.HAL-支持表单的应用程序

@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {

}

每当客户机提供带有application/prs.hal-forms+jsonAccept头时,你可以预期这样的情况:

例 33.HAL-表格样本文件

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "role" : "ring bearer",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/employees/1"
    }
  },
  "_templates" : {
    "default" : {
      "method" : "put",
      "contentType" : "",
      "properties" : [ {
        "name" : "firstName",
        "required" : true
      }, {
        "name" : "lastName",
        "required" : true
      }, {
        "name" : "role",
        "required" : true
      } ]
    },
    "partiallyUpdateEmployee" : {
      "method" : "patch",
      "contentType" : "",
      "properties" : [ {
        "name" : "firstName",
        "required" : false
      }, {
        "name" : "lastName",
        "required" : false
      }, {
        "name" : "role",
        "required" : false
      } ]
    }
  }
}

查看HAL-表格规格 (opens new window)以了解**_ 模板**属性的详细信息。阅读有关Affordances API的信息,以使用这些额外的元数据来增强控制器。

至于单项(EntityModel)和聚合根集合(CollectionModel), Spring Hateoas 将它们以相同的方式呈现为HAL 文件

# 4.2.1.定义 HAL-表单元数据

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

Attribute 说明
readOnly 如果属性没有 setter 方法,则设置为true。如果存在这种情况,则在访问器或字段上显式地使用 Jackson 的@JsonProperty(Access.READ_ONLY)。默认情况下不呈现,因此默认为false
regex 可以通过在字段或类型上使用 JSR-303 的@Pattern注释来定制。在后一种情况下,该模式将用于声明为该特定类型的每个属性。默认情况下不呈现。
required 可以通过使用 JSR-303 的@NotNull进行自定义。默认情况下不呈现,因此默认为false。使用PATCH作为方法的模板将自动将所有属性设置为不需要的。

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

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

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

此设置将使类型CreditCardNumber的表示模型属性的 HAL-Forms 模板属性声明一个regex字段,其值[0-9]{16}

# 4.2.2.表单属性的国际化

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

# 模板标题

要定义模板标题,请使用以下模式:_templates.$affordanceName.title。注意,在 HAL 表单中,模板的名称是default,如果它是唯一的一个。这意味着你通常必须使用 Affordance 描述的本地或完全限定输入类型名称来限定密钥。

例 34.定义 HAL-表单模板标题

_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 时显式定义,否则默认为$httpMethod + $simpleInputTypeName
3 将本地定义的标题应用于所有名为Employee的类型。
4 使用完全限定类型名的标题定义。
与默认的密钥相比,使用实际的 Affaundance 名称的密钥享有优先权。
# 属性提示

还可以通过由 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”的提示。

同时定义了模板标题和属性提示的样例文档将如下所示:

例 36.带有国际化模板标题和属性提示的示例 HAL 表单文档

{
  …,
  "_templates" : {
    "default" : {
      "title" : "Create employee",
      "method" : "put",
      "contentType" : "",
      "properties" : [ {
        "name" : "firstName",
        "prompt" : "Firstname",
        "required" : true
      }, {
        "name" : "lastName",
        "prompt" : "Lastname",
        "required" : true
      }, {
        "name" : "role",
        "prompt" : "Role",
        "required" : true
      } ]
    }
  }
}

# 4.3.HTTP 问题详细信息

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

HTTP Problem Details 定义了一组 JSON 属性,这些属性携带额外的信息来向 HTTP 客户机描述错误详细信息。在RFC 文档 (opens new window)的相关部分中找到有关这些属性的更多详细信息。

你可以通过在 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 (opens new window)是在 IANA 批准的媒体类型application/vnd.collection+json中注册的 JSON 规范。

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

——Mike Amundsen

集合 +JSON 规范

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

例 37.支持 Collection+JSON 的应用程序

@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {

}

此配置将使你的应用程序响应具有Acceptapplication/vnd.collection+json的请求,如下所示。

下面的规范示例显示了一个单独的项目:

例 38.集合 +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中提取属性并将它们转换为…

在呈现资源集合时,文档几乎是相同的,只是itemsJSON 数组中会有多个条目,每个条目对应一个条目。

Spring Hateoas 更具体地说将:

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

  • CollectionModel链接(减去self)放入顶层links

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

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

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

UBER (opens new window)是一种实验性的 JSON 规范

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

——Mike Amundsen

UBER 规范

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

例 39.启用 UBER+JSON 的应用程序

@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {

}

此配置将使你的应用程序使用Acceptapplication/vnd.amundsen-uber+json响应请求,如下所示:

例 40.UBER 样本文件

{
  "uber" : {
    "version" : "1.0",
    "data" : [ {
      "rel" : [ "self" ],
      "url" : "/employees/1"
    }, {
      "name" : "employee",
      "data" : [ {
        "name" : "role",
        "value" : "ring bearer"
      }, {
        "name" : "name",
        "value" : "Frodo"
      } ]
    } ]
  }
}

这种媒体类型和规范本身都还在开发中。如果你在使用它时遇到问题,请随意开一张票 (opens new window)

UBER 媒体类型与乘车共享公司**UBERTechnologies Inc.。**没有任何关联。

# 4.6.ALP-应用程序级配置文件语义

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

一份阿尔卑斯山的文档可以作为一个配置文件来使用 用应用程序解释文档的应用程序语义- 不可知的媒体类型(如 HTML、HAL、Collection+JSON、Siren、 等)。这增加了整个配置文件文档的可重用性。 媒体类型。

——Mike Amundsen

阿尔卑斯山规范

阿尔卑斯山不需要特殊的激活。相反,你“构建”一个Alps记录,并从 Spring MVC 或 Spring WebFlux Web 方法返回它,如下所示:

例 41.创建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:

例 42.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 框架的消息包和MessageSource接口。这使你能够将这些值委托给特定于区域的消息包,甚至使元数据国际化。

# 4.7.基于社区的媒体类型

由于创建自己的媒体类型的能力,有几个社区领导的努力,以建立额外的媒体类型。

# 4.7.1.JSON:API

Maven 坐标

<dependency>
    <groupId>com.toedter</groupId>
    <artifactId>spring-hateoas-jsonapi</artifactId>
    <version>{see project page for current version}</version>
</dependency>

Gradle 坐标

implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'

如果你想要发布快照,请访问项目页面了解更多详细信息。

# 4.7.2.警报器

Maven 坐标

<dependency>
    <groupId>de.ingogriebsch.hateoas</groupId>
    <artifactId>spring-hateoas-siren</artifactId>
    <version>{see project page for current version}</version>
    <scope>compile</scope>
</dependency>

Gradle 坐标

implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'

# 4.8.注册自定义媒体类型

Spring Hateoas 允许你通过 SPI 集成自定义媒体类型。这种实现的基本要素是:

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

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

  3. 一小部分基础设施配置,它将允许 Spring Hateoas 找到自定义实现并获取它。

# 4.8.1.自定义媒体类型配置

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

  • 应用于[WebClient](#client.web-client),[WebTestClient](#client.web-test-client),或[RestTemplate](#client.rest-template)实例。

  • 支持从 Spring Web MVC 和 Spring WebFlux 控制器中提供该媒体类型的服务。

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

@Configuration
public class MyMediaTypeConfiguration implements HypermediaMappingInformation {

  @Override
  public List<MediaType> getMediaTypes() {
    return MediaType.parse("application/vnd-acme-media-type") (1)
  }

  @Override
  public Module getJacksonModule() {
    return new Jackson2MyMediaTypeModule(); (2)
  }

  @Bean
  MyLinkDiscoverer myLinkDiscoverer() {
    return new MyLinkDiscoverer(); (3)
  }
}
1 配置类返回它所支持的媒体类型。这适用于服务器端和客户端场景。
2 它重写getJacksonModule()以提供自定义序列化器来创建特定于媒体类型的表示。
3 它还声明了用于进一步客户端支持的自定义LinkDiscoverer实现。

Jackson 模块通常声明SerializerDeserializer实现用于表示模型类型RepresentationModelEntityModelCollectionModelPagedModel。如果需要对 JacksonObjectMapper进行进一步的自定义(如自定义HandlerInstantiator),则可以替代地覆盖configureObjectMapper(…)

以前的参考文档版本已经提到实现MediaTypeConfigurationProvider接口并将其注册为spring.factories
这是不必要的。
此 SPI 仅用于 Spring Hateoas 提供的开箱即用媒体类型。
仅实现HypermediaMappingInformation接口将它注册为 Spring Bean 是所有需要的。

# 4.8.2.建议

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

默认情况下支持的媒体类型使用与第三方实现相同的配置机制。因此,值得研究[themediatype包](https://github.com/ Spring-projects/ Spring-hateoas/tree/master/SRC/main/java/org/springframework/hateoas/mediatype)中的实现。请注意,内置的媒体类型实现保持其配置类包的私有,因为它们是通过@EnableHypermediaSupport激活的。定制实现可能应该将这些公开,以确保用户可以从他们的应用程序包中导入这些配置类。

# 5.配置

本节描述如何配置 Spring Hateoas。

# 5.1.使用@EnableHypermediaSupport

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

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

  • 如果 JsonPath 位于 Classpath 上,它会自动注册一个LinkDiscoverer实例,以便通过它们的rel在普通的 JSON 表示中查找链接(参见[使用LinkDiscoverer实例](#client.link-discoverer))。

  • 默认情况下,它启用实体链接并自动获取EntityLinks实现,并将它们捆绑到DelegatingEntityLinks实例中,你可以自动连接这些实例。

  • 它会自动拾取RelProvider中的所有ApplicationContext实现,并将它们捆绑到可以自动连接的DelegatingRelProvider中。它在 Spring MVC 控制器以及域类型上注册要考虑@Relation的提供者。如果EVO 折弯机 (opens new window)在 Classpath 上,则集合rel值是通过使用在库中实现的多元化算法派生的(参见[[[spis.rel-provider]])。

# 5.1.1.显式地启用对专用 Web 堆栈的支持

默认情况下,@EnableHypermediaSupport将反射地检测你正在使用的 Web 应用程序堆栈,并将其钩入为这些组件注册的 Spring 组件,以支持超媒体表示。然而,在某些情况下,你只需要明确地激活对特定堆栈的支持。例如,如果你的 Spring 基于 WebMVC 的应用程序使用 WebFlux’WebClient发出请求,而其中一个不应该与超媒体元素一起工作,那么你可以通过在配置中显式声明 WebMVC 来限制所启用的功能:

例 43.显式地激活对特定 Web 堆栈的超媒体支持

@EnableHypermediaSupport(…, stacks = WebStack.WEBMVC)
class MyHypermediaConfiguration { … }

# 6.客户端支持

本节描述 Spring Hateoas 对客户的支持。

# 6.1.Traverson

Spring Hateoas 为客户端服务遍历提供了一个 API。它的灵感来自Traverson JavaScript 函式库 (opens new window)。下面的示例展示了如何使用它:

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头的媒体类型进行配置,可以设置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>对象。但是,如果你得到了一个类似\_embeddedHAL 集合的集合,该怎么办?你只需做一个小的调整就可以做到这一点,如下例所示:

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

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

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

# 6.2.使用LinkDiscoverer实例

在使用启用超媒体的表示时,一个常见的任务是在其中找到具有特定关系类型的链接。 Spring Hateoas 提供了基于的接口的实现方式,用于呈现或开箱即用的默认表示或 HAL。当使用@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,如下所示:

例 44.自己配置WebClient

@Bean
WebClient.Builder hypermediaWebClient(HypermediaWebClientConfigurer configurer) { (1)
 return configurer.registerHypermediaTypes(WebClient.builder()); (2)
}
1 在你的@Configuration类中,获取HypermediaWebClientConfigurer Bean Spring 仇恨寄存器的副本。
2 创建WebClient.Builder后,使用配置器注册超媒体类型。
什么HypermediaWebClientConfigurer它用WebClient.Builder注册所有正确的编码器和解码器。要使用它,
你需要将构建器插入到应用程序的某个地方,并运行build()方法来生成WebClient

如果你正在使用 Spring boot,还有另一种方法:WebClientCustomizer

例 45.让 Spring 引导配置事物

@Bean (4)
WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) { (1)
    return webClientBuilder -> { (2)
        configurer.registerHypermediaTypes(webClientBuilder); (3)
    };
}
1 在创建 Spring Bean 时,请求 Spring Hateoas 的 Bean 的副本。
2 使用 Java8lambda 表达式来定义WebClientCustomizer
3 在函数调用内部,应用registerHypermediaTypes方法。
4 将整个事情作为 Spring Bean 返回,以便 Spring 引导可以捡起它并将其应用到其自动配置的WebClient.Builder Bean。

在此阶段,每当你需要一个具体的WebClient时,只需将WebClient.Builder注入到你的代码中,并使用build()WebClient实例将能够使用超媒体进行交互。

# 6.4.配置WebTestClient实例

在使用启用超媒体的表示时,一个常见的任务是使用WebTestClient运行各种测试。

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

例 46.在使用 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 使用 Spring Hateoas 的TypeReferences.CollectionModelType助手请求CollectionModel<EntityModel<Employee>>的响应。
4 在得到 Spring Hateoas 格式的“身体”后,断言它!
WebTestClient是一种不可变值类型,因此你无法在适当的位置更改它。HypermediaWebClientConfigurer返回一个变异的
变量,然后必须捕获该变量才能使用它。

如果你正在使用 Spring 引导,还有其他选项,例如:

例 47.在使用 Spring 引导时配置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 将 AutoWire Spring boot 的WebTestClient.Builder转换为builder和 Spring Hateoas 的配置器作为方法参数。
3 使用HypermediaWebTestClientConfigurer注册对超媒体的支持。
4 使用TypeReferences返回要EntityModel<Employee>的信号。

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

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

# 6.5.配置 RESTTemplate 实例

如果你想创建自己的RestTemplate副本,并将其配置为说超媒体语言,则可以使用HypermediaRestTemplateConfigurer:

例 48.配置RestTemplate自己

/**
 * Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}.
 */
@Bean
RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { (1)
	return configurer.registerHypermediaTypes(new RestTemplate()); (2)
}
1 在你的@Configuration类中,获取一份HypermediaRestTemplateConfigurer Bean Spring 仇恨寄存器的副本。
2 在创建RestTemplate之后,使用配置器应用超媒体类型。

你可以自由地将此模式应用于所需的RestTemplate的任何实例,无论是创建已注册的 Bean,还是在你定义的服务中。

如果你正在使用 Spring boot,还有另一种方法。

通常, Spring 引导已经偏离了在应用程序上下文中注册RestTemplate Bean 的概念。

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

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

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

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

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

例 49.让 Spring 引导配置事物

@Bean (4)
RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { (1)
    return restTemplate -> { (2)
        configurer.registerHypermediaTypes(restTemplate); (3)
    };
}
1 在创建 Spring Bean 时,请求 Spring Hateoas 的HypermediaRestTemplateConfigurer Bean 的副本。
2 使用 Java8lambda 表达式来定义RestTemplateCustomizer
3 在函数调用内部,应用registerHypermediaTypes方法。
4 将整个事情作为 Spring Bean 返回,以便 Spring 引导可以将其拾起并将其应用到其自动配置的RestTemplateBuilder

在此阶段,每当你需要一个具体的RestTemplate时,只需将RestTemplateBuilder注入到你的代码中,并使用build()RestTemplate实例将能够使用超媒体进行交互。