From 88237aaa9f5b999064c0976a7ad55957f1077f52 Mon Sep 17 00:00:00 2001 From: ZhangKai Date: Tue, 8 Mar 2022 17:42:54 +0800 Subject: [PATCH] =?UTF-8?q?#25=20spring=20hateoas=20=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=AE=A1=E6=A0=B8=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/config.js | 11 + docs/spring-hateoas/README.md | 1 + docs/spring-hateoas/spring-hateoas.md | 2012 +++++++++++++++++++++++++ 3 files changed, 2024 insertions(+) create mode 100644 docs/spring-hateoas/README.md create mode 100644 docs/spring-hateoas/spring-hateoas.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 61d29f2..935c029 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -510,6 +510,17 @@ module.exports = { initialOpenGroupIndex: 0 // 可选的, 默认值是 0 } ], + '/spring-hateoas/': [ + { + title: 'Spring HATEOAS', + sidebarDepth: 2, + collapsable: false, + children: [ + "/spring-hateoas/spring-hateoas.md", + ], + initialOpenGroupIndex: 0 // 可选的, 默认值是 0 + } + ], // fallback '/': [{ diff --git a/docs/spring-hateoas/README.md b/docs/spring-hateoas/README.md new file mode 100644 index 0000000..6db3426 --- /dev/null +++ b/docs/spring-hateoas/README.md @@ -0,0 +1 @@ +# Spring HATEOAS \ No newline at end of file diff --git a/docs/spring-hateoas/spring-hateoas.md b/docs/spring-hateoas/spring-hateoas.md new file mode 100644 index 0000000..f8ff109 --- /dev/null +++ b/docs/spring-hateoas/spring-hateoas.md @@ -0,0 +1,2012 @@ +# Spring Hateoas-参考文献 + +# 1.前言 + +### 1.1.迁移到 Spring Hateoas1.0 + +对于 1.0,我们利用这个机会重新评估了我们为 0.x 分支所做的一些设计和包结构选择。对此有大量的反馈,主要版本的提升似乎是重构这些内容的最自然的地方。 + +#### 1.1.1.变化 + +包结构中最大的变化是引入了超媒体类型注册 API,以支持 Spring Hateoas 中的其他媒体类型。这导致客户机和服务器 API(分别命名的包)以及包`mediatype`中的媒体类型实现的明确分离。 + +使你的代码库升级到新 API 的最简单的方法是使用[迁移脚本](#migrate-to-1.0.script)。在我们开始讨论这一问题之前,我们先来简单了解一下这些变化。 + +##### 表示模型 + +`ResourceSupport`/`Resource`/`Resources`/`PagedResources`这组类从来没有真正感觉到合适的名称。毕竟,这些类型实际上并不表示资源,而是表示模型,这些模型可以用超媒体信息和启示来丰富。以下是新名字与旧名字的对应方式: + +* `ResourceSupport`现在是`RepresentationModel` + +* `Resource`现在是`EntityModel` + +* `Resources`现在是`CollectionModel` + +* `PagedResources`现在是`PagedModel` + +因此,`ResourceAssembler`已更名为`RepresentationModelAssembler`,其方法`toResource(…)`和`toResources(…)`已分别更名为`toModel(…)`和`toCollectionModel(…)`。此外,名称更改已反映在`TypeReferences`中包含的类中。 + +* `RepresentationModel.getLinks()`现在公开了一个`Links`实例(在`List`上),因为它公开了额外的 API 来使用各种策略连接和合并不同的`Links`实例。它还被转换为自绑定泛型类型,以允许向实例添加链接的方法返回实例本身。 + +* `LinkDiscoverer`API 已被移动到`client`包中。 + +* `LinkBuilder`和`EntityLinks`API 已被移动到`server`包中。 + +* `ControllerLinkBuilder`已被移动到`server.mvc`中,并且不推荐将其替换为`WebMvcLinkBuilder`。 + +* `RelProvider`已重命名为`LinkRelationProvider`,并返回`LinkRelation`实例,而不是`String`s。 + +* `VndError`已被移动到`mediatype.vnderror`包中。 + +#### 1.1.2.迁移脚本 + +你可以在应用程序根目录中找到[a script](https://github.com/spring-projects/spring-hateoas/tree/master/etc)以运行,该根目录将更新所有导入语句和静态方法引用,使其指向在我们的源代码存储库中移动的 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`类型,以便流畅地使用。详见[启示](#server.affordances)。 + +* `AffordanceModelFactory.getAffordanceModel(…)`现在接收`InputPayloadMetadata`和`PayloadMetadata`实例,而不是`ResolvableType`s,以允许非基于类型的实现。定制的媒体类型实现必须相应地进行调整。 + +* HAL 窗体现在不呈现属性属性,如果它们的值符合规范中定义的默认值。也就是说,如果以前`required`显式地设置为`false`,那么我们现在省略`required`的条目。我们现在也只强制那些使用`PATCH`作为 HTTP 方法的模板不需要它们。 + +## 2.基本原理 + +本节介绍了 Spring Hateoas 的基础知识及其基本的领域抽象。 + +### 2.1.链接 + +超媒体的基本思想是用超媒体元素来丰富资源的表示。最简单的形式是链接。它们指示客户机可以导航到特定的资源。相关资源的语义是在所谓的链接关系中定义的。你可能已经在 HTML 文件的头中看到了这一点: + +例 2.HTML 文档中的链接 + +``` + +``` + +如你所见,该链接指向一个资源`theme.css`,并指示它是一个样式表。链接通常带有额外的信息,例如指向的资源将返回的媒体类型。然而,链接的基本组成部分是它的引用和关系。 + +Spring Hateoas 允许你通过其不可变的`Link`值类型处理链接。它的构造函数接受超文本引用和链接关系,后者默认为 IANA 链接关系`self`。在[链接关系](#fundamentals.link-relations)中阅读有关后者的更多信息。 + +例 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](https://tools.ietf.org/html/rfc8288)中定义的其他属性。你可以通过在`Link`实例上调用相应的 wither 方法来设置它们。 + +在[ Building links in Spring MVC](#server.link-builder.webmvc)和[Building links in Spring WebFlux](#server.link-builder.webflux)中找到有关如何创建指向 Spring MVC 和 Spring WebFlux 控制器的链接的更多信息。 + +### 2.2.URI 模板 + +对于 Spring Hateoas`Link`,超文本引用不仅可以是一个 URI,而且根据[RFC-6570](https://tools.ietf.org/html/rfc6570)也可以是一个 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 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 链接关系 + +因特网分配号码管理局包含一组[预定义的链接关系](https://www.iana.org/assignments/link-relations/link-relations.xhtml)。它们可以通过`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`的类。它基本上是一个`Link`s 集合的容器,并且有方便的方法将它们添加到模型中。模型稍后可以呈现为各种媒体类型格式,这些格式将定义超媒体元素在表示中的外观。有关此的更多信息,请查看[媒体类型](#mediatypes)。 + +例 7.`RepresentationModel`类层次结构 + +diagram classes + +使用`RepresentationModel`的默认方法是创建它的一个子类,以包含表示应该包含的所有属性,创建该类的实例,填充属性并用链接丰富它。 + +例 8.样本表示模型类型 + +``` +class PersonModel extends RepresentationModel { + + 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 model = EntityModel.of(person); +``` + +#### 2.4.2.集合资源表示模型 + +对于概念上是集合的资源,可以使用`CollectionModel`。它的元素可以是简单对象,也可以是`RepresentationModel`实例。 + +例 12.使用`CollectionModel`包装现有对象的集合 + +``` +Collection people = Collections.singleton(new Person("Dave", "Matthews")); +CollectionModel model = CollectionModel.of(people); +``` + +## 3.服务器端支持 + +### 在 Spring MVC 中构建链接 + +现在我们已经有了域词汇表,但主要的挑战仍然是:如何创建实际的 URI,并以一种不那么脆弱的方式包装到`Link`实例中。现在,我们将不得不到处复制 URI 字符串。这样做是脆弱和不可维护的。 + +假设你的 Spring MVC 控制器实现如下: + +``` +@Controller +class PersonController { + + @GetMapping("/people") + HttpEntity showAll() { … } + + @GetMapping(value = "/{person}", method = RequestMethod.GET) + HttpEntity 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(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](#server.link-builder.webmvc)),你… + +下面的代码展示了如何使用**自我**链接并关联另外两个启示: + +例 13.连接到`GET /employees/{id}`的启示 + +``` +@GetMapping("/employees/{id}") +public EntityModel findOne(@PathVariable Integer id) { + + Class controllerClass = EmployeeController.class; + + // Start the affordance with the "self" link, i.e. this method. + Link findOneLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel(); (1) + + // Return the affordance + a link back to the entire collection resource. + return EntityModel.of(EMPLOYEES.get(id), // + findOneLink // + .andAffordance(afford(methodOn(controllerClass).updateEmployee(null, id))) (2) + .andAffordance(afford(methodOn(controllerClass).partiallyUpdateEmployee(null, id)))); (3) +} +``` + +|**1**|创建**自我**链接。| +|-----|--------------------------------------------------------------------| +|**2**|将`updateEmployee`方法与`self`链接关联起来。| +|**3**|将`partiallyUpdateEmployee`方法与`self`链接关联起来。| + +使用`.andAffordance(afford(…​))`,你可以使用控制器的方法将`PUT`和`PATCH`操作连接到`GET`操作。假设上面的相关方法**提供**看起来是这样的: + +例 14.响应`updateEmpoyee`的`PUT /employees/{id}`方法 + +``` +@PutMapping("/employees/{id}") +public ResponseEntity updateEmployee( // + @RequestBody EntityModel employee, @PathVariable Integer id) +``` + +例 15.响应`PATCH /employees/{id}`的`partiallyUpdateEmployee`方法 + +``` +@PatchMapping("/employees/{id}") +public ResponseEntity partiallyUpdateEmployee( // + @RequestBody EntityModel employee, @PathVariable Integer id) +``` + +指向那些使用`afford(…)`方法的方法将导致 Spring Hateoas 分析请求主体和响应类型并捕获元数据,以允许不同的媒体类型实现使用该信息将其转换为输入和输出的描述。 + +#### 3.3.1.人工构建可供参考的功能 + +虽然注册链接的主要方法是提供支持,但可能有必要手动构建其中的一些。这可以通过使用`Affordances`API 来实现: + +例 16.使用`Affordances`API 手动注册启示 + +``` +var methodInvocation = methodOn(EmployeeController.class).all(); + +var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) (1) + + .afford(HttpMethod.POST) (2) + .withInputAndOutput(Employee.class) // + .withName("createEmployee") // + + .andAfford(HttpMethod.GET) (3) + .withOutput(Employee.class) // + .addParameters(// + QueryParameter.optional("name"), // + QueryParameter.optional("role")) // + .withName("search") // + + .toLink(); +``` + +|**1**|首先,从一个`Link`实例创建`Affordances`的实例,创建用于描述提供的上下文。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|每一个启示都是从它应该支持的 HTTP 方法开始的。然后,我们将一种类型注册为有效负载描述,并显式地给出它的名称。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这有效地创建了与指向`EmployeeController.newEmployee(…)`的指针相同的启示。| +|**3**|构建下一个启示是为了反映指向`EmployeeController.search(…)`的指针所发生的情况。这里我们将`Employee`定义为创建并显式注册`QueryParameter`s 的响应的模型。| + +启示由特定于媒体类型的启示模型支持,该模型将一般的启示元数据转换为特定的表示形式。请务必检查[媒体类型](#mediatypes)部分中有关启示的部分,以找到有关如何控制该元数据的公开的更多详细信息。 + +### 3.4.转发头处理 + +[RFC-7239 转发头](https://tools.ietf.org/html/rfc7239)通常用于应用程序位于代理、负载均衡器或云中。实际接收 Web 请求的节点是基础设施的一部分,*向前*是对应用程序的请求。 + +你的应用程序可能运行在`localhost:8080`上,但是对于外部世界来说,你应该运行在`reallycoolsite.com`上(以及 Web 的标准端口 80 上)。通过使代理包括额外的头(许多人已经这样做了), Spring Hateoas 可以在使用 Spring 框架功能来获得原始请求的基本 URI 时正确地生成链接。 + +| |任何可以根据外部输入改变根 URI 的内容都必须得到适当的保护,
这就是为什么,默认情况下,转发头处理是**已禁用**。
你必须使其能够运行。
如果你要部署到云或控制代理和负载均衡器的配置中,那么你肯定想要使用此功能。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要启用转发头处理,你需要在应用程序中注册 Spring MVC 的`ForwardedHeaderFilter`(details[here](https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#filters-forwarded-headers))或 Spring WebFlux 的`ForwardedHeaderTransformer`(details[here](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-forwarded-headers))。在 Spring 引导应用程序中,这些组件可以简单地声明为 Spring bean,如[here](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-embedded-container-servlets-filters-listeners-beans)所述。 + +例 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 接口 + +| |`EntityLinks`及其各种实现目前还没有为 Spring WebFlux 应用程序提供开箱即用的。
`EntityLinks`SPI 中定义的契约最初是针对 Spring Web MVC 的,并不考虑反应器类型。
开发支持反应式编程的类似契约仍在进行中。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +到目前为止,我们已经通过指向 Web 框架实现(即 Spring MVC 控制器)创建了链接,并检查了映射。在许多情况下,这些类本质上是由模型类支持的读写表示。 + +现在,`EntityLinks`接口公开了一个 API,以便根据模型类型查找`Link`或`LinkBuilder`。这些方法本质上返回指向集合资源(例如`/people`)或项资源(例如`/people/1`)的链接。下面的示例展示了如何使用`EntityLinks`: + +``` +EntityLinks links = …; +LinkBuilder builder = links.linkFor(Customer.class); +Link link = links.linkToItemResource(Customer.class, 1L); +``` + +通过在 Spring MVC 配置中激活`@EnableHypermediaSupport`,通过依赖注入可以使用`EntityLinks`。这将导致`EntityLinks`的各种默认实现被注册。最基本的是`ControllerEntityLinks`,它检查 SpringMVC 控制器类。如果你想注册你自己的`EntityLinks`的实现,请查看[本节](#server.entity-links.spi)。 + +#### 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`实例。 + +#### 3.5.2.详细介绍 EntityLinks API + +从根本上说,`EntityLinks`允许将`LinkBuilder`s 和`Link`实例构建为实体类型的集合和项资源。以`linkFor…`开头的方法将产生`LinkBuilder`实例,供你扩展和增加额外的路径段、参数等。方法以`linkTo`为起点,生产制备充分的`Link`实例。 + +虽然对于集合资源来说,提供一个实体类型就足够了,但是指向项资源的链接将需要提供一个标识符。这通常看起来是这样的: + +例 21.获取到项目资源的链接 + +``` +entityLinks.linkToItemResource(order, order.getId()); +``` + +如果你发现自己重复这些方法调用,则可以将标识符提取步骤拉出到一个可重用的`Function`中,以便在不同的调用中重用: + +``` +Function idExtractor = Order::getId; (1) + +entityLinks.linkToItemResource(order, idExtractor); (2) +``` + +|**1**|标识符的提取是外部化的,因此它可以保存在一个域或常数中。| +|-----|----------------------------------------------------------------------------------------| +|**2**|使用抽取器进行链接查找。| + +##### typedentylinks + +由于控制器实现通常是围绕实体类型进行分组的,因此你经常会发现自己在整个控制器类中使用相同的提取器函数(有关详细信息,请参见[详细介绍 EntityLinks API](#server.entity-links.api))。我们可以通过获得一个`TypedEntityLinks`实例来集中标识符提取逻辑,从而一次提供提取器,这样实际的查找就完全不必再处理提取了。 + +例 22.使用 typedentitylinks + +``` +class OrderController { + + private final TypedEntityLinks links; + + OrderController(EntityLinks entityLinks) { (1) + this.links = entityLinks.forType(Order::getId); (2) + } + + @GetMapping + ResponseEntity someMethod(…) { + + Order order = … // lookup order + + Link link = links.linkToItemResource(order); (3) + } +} +``` + +|**1**|注入一个`EntityLinks`实例。| +|-----|------------------------------------------------------------------------------------------------| +|**2**|表示你将使用特定的标识符提取器函数查找`Order`实例。| +|**3**|基于唯一的`Order`实例查找项目资源链接。| + +#### 3.5.3.作为 SPI 的 EntityLink + +由`@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. 添加一个链接,其`rel`的`self`指向呈现的资源。 + +Spring Hateoas 现在提供了一个`RepresentationModelAssemblerSupport`基类,它有助于减少你需要编写的代码量。下面的示例展示了如何使用它: + +``` +class PersonModelAssembler extends RepresentationModelAssemblerSupport { + + 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(…)`。 + +然后可以使用汇编程序来组装`RepresentationModel`或`CollectionModel`。下面的示例创建了`CollectionModel`的`PersonModel`实例: + +``` +Person person = new Person(…); +Iterable people = Collections.singletonList(person); + +PersonModelAssembler assembler = new PersonModelAssembler(); +PersonModel model = assembler.toModel(person); +CollectionModel model = assembler.toCollectionModel(people); +``` + +### 3.7.表示模型处理器 + +有时,你需要在超媒体表示[assembled](#server.representation-model-assembler)之后对其进行调整。 + +一个完美的例子是,你有一个处理订单履行的控制器,但你需要添加与付款相关的链接。 + +想象一下,让你的订购系统产生这种类型的超媒体: + +``` +{ + "orderId" : "42", + "state" : "AWAITING_PAYMENT", + "_links" : { + "self" : { + "href" : "http://localhost/orders/999" + } + } +} +``` + +你希望添加一个链接,以便客户端可以进行付款,但不想将有关`PaymentController`的详细信息混入`OrderController`中。 + +你可以这样写`RepresentationModelProcessor`,而不是污染你的订购系统的细节: + +``` +public class PaymentProcessor implements RepresentationModelProcessor> { (1) + + @Override + public EntityModel process(EntityModel model) { + + model.add( (2) + Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) // + .expand(model.getContent().getOrderId())); + + return model; (3) + } +} +``` + +|**1**|此处理器将只应用于`EntityModel`对象。| +|-----|-------------------------------------------------------------------------------| +|**2**|通过添加一个无条件链接来操作现有的`EntityModel`对象。| +|**3**|返回`EntityModel`,以便将其序列化为所请求的媒体类型。| + +在应用程序中注册处理器: + +``` +@Configuration +public class PaymentProcessingApp { + + @Bean + PaymentProcessor paymentProcessor() { + return new PaymentProcessor(); + } +} +``` + +现在,当你发出`Order`的超媒体重发消息时,客户机将接收到以下内容: + +``` +{ + "orderId" : "42", + "state" : "AWAITING_PAYMENT", + "_links" : { + "self" : { + "href" : "http://localhost/orders/999" + }, + "payments" : { (1) + "href" : "/payments/42" (2) + } + } +} +``` + +|**1**|你可以看到插入的`LinkRelation.of("payments")`作为此链接的关系。| +|-----|-----------------------------------------------------------------------------| +|**2**|URI 由处理器提供。| + +这个例子很简单,但你可以很容易地: + +* 使用`WebMvcLinkBuilder`或`WebFluxLinkBuilder`构建到`PaymentController`的动态链接。 + +* 插入有条件地添加由状态驱动的其他链接(例如`cancel`,`amend`)所需的任何服务。 + +* 利用诸如 Spring Security 之类的交叉服务,根据当前用户的上下文添加、删除或修改链接。 + +同样,在这个示例中,`PaymentProcessor`改变了提供的`EntityModel`。你还可以用另一个对象替换它。请注意,API 要求返回类型等于输入类型。 + +### 3.8.使用`LinkRelationProvider`API + +在构建链接时,通常需要确定要为该链接使用的关系类型。在大多数情况下,关系类型与(域)类型直接关联。我们封装了详细的算法来查找`LinkRelationProvider`API 背后的关系类型,该 API 允许你确定单个资源和集合资源的关系类型。查找关系类型的算法如下: + +1. 如果类型是用`@Relation`注释的,那么我们使用在注释中配置的值。 + +2. 如果不是,那么对于集合`rel`,我们默认使用未大写的简单类名加上附加的`List`。 + +3. 如果[EVO 折弯机](https://github.com/atteo/evo-inflector)JAR 在 Classpath 中,则使用由多元化算法提供的单个资源`rel`的复数形式。 + +4. `@Controller`注释带`@ExposesResourceFor`的 classes(详见[使用 EntityLinks 接口](#server.entity-links))透明地查找注释带中所配置类型的关系类型,这样就可以使用`LinkRelationProvider.getItemResourceRelFor(MyController.class)`并获取所公开的域类型的关系类型。 + +当你使用`@EnableHypermediaSupport`时,`LinkRelationProvider`会自动暴露为 Spring Bean。你可以通过实现接口并将它们依次以 Spring bean 的形式公开来插入自定义提供程序。 + +## 4.媒体类型 + +### 4.1.HAL-超文本应用程序语言 + +[JSON 超文本应用程序语言](https://tools.ietf.org/html/draft-kelly-json-hal-08)或 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`进行定制(有关详细信息,请参见[使用`LinkRelationProvider`API](#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 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 对象。属性名是[链接关系](#fundamentals.link-relations),每个值都是[链接对象或链接对象数组](https://tools.ietf.org/html/draft-kelly-json-hal-07#section-4.1.1)。 + +对于具有两个或多个链接的给定的链接关系,规范在表示上是明确的: + +例 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" + } + } +} +``` + +#### 使用`CurieProvider`API + +[网络链接 RFC](https://tools.ietf.org/html/rfc8288#section-2.1)描述了注册和扩展链接关系类型。已注册的 REL 是用[IANA 链接关系类型注册表](https://www.iana.org/assignments/link-relations/link-relations.xhtml)注册的众所周知的字符串。扩展`rel`URI 可以被不希望注册关系类型的应用程序使用。每个 URI 都是唯一标识关系类型的 URI。`rel`URI 可以序列化为紧凑的 URI 或[Curie](https://www.w3.org/TR/curie)。例如,一个`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,这可能会导致大量的开销。`CurieProvider`API 负责这一点:它允许你将一个基本 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" +} +``` + +由于`CurieProvider`API 的目的是允许自动创建居里,因此每个应用程序范围只能定义一个`CurieProvider` Bean。 + +### 4.2.HAL-表格 + +[HAL-FORMS](https://rwcbook.github.io/hal-forms/)旨在向[HAL 媒体类型](#mediatypes.hal)添加运行时表单支持。 + +> > > > +> HAL-形式“看起来像 HAL。”然而,重要的是要记住,HAL 形式与 HAL 是不一样的——两者 +> 不应被认为在任何方面都是可以互换的。 +> > > > + +——Mike Amundsen + +> HAL-表格规格 + +要启用此媒体类型,请在代码中加入以下配置: + +例 32.HAL-支持表单的应用程序 + +``` +@Configuration +@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) +public class HalFormsApplication { + +} +``` + +每当客户机提供带有`application/prs.hal-forms+json`的`Accept`头时,你可以预期这样的情况: + +例 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-表格规格](https://rwcbook.github.io/hal-forms/)以了解**\_ 模板**属性的详细信息。阅读有关[Affordances API](#server.affordances)的信息,以使用这些额外的元数据来增强控制器。 + +至于单项(`EntityModel`)和聚合根集合(`CollectionModel`), Spring Hateoas 将它们以相同的方式呈现为[HAL 文件](#mediatypes.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.Employee`的`firstName`属性将得到分配的“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 的问题细节](https://tools.ietf.org/html/rfc7807)是一种媒体类型,用于在 HTTP 响应中包含机器可读的错误详细信息,以避免需要为 HTTP API 定义新的错误响应格式。 + +HTTP Problem Details 定义了一组 JSON 属性,这些属性携带额外的信息来向 HTTP 客户机描述错误详细信息。在[RFC 文档](https://tools.ietf.org/html/rfc7807#section-3.1)的相关部分中找到有关这些属性的更多详细信息。 + +你可以通过在 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 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](http://amundsen.com/media-types/collection/format/)是在 IANA 批准的媒体类型`application/vnd.collection+json`中注册的 JSON 规范。 + +> > > > +> [Collection+JSON](http://amundsen.com/media-types/collection/)是一种基于 JSON 的读/写超媒体类型,旨在支持 +> 简单集合的管理和查询。 +> > > > + +——Mike Amundsen + +> 集合 +JSON 规范 + +Collection+JSON 提供了一种统一的方式来表示单个项目资源以及集合。要启用此媒体类型,请在代码中加入以下配置: + +例 37.支持 Collection+JSON 的应用程序 + +``` +@Configuration +@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON) +public class CollectionJsonApplication { + +} +``` + +此配置将使你的应用程序响应具有`Accept`头`application/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`中提取属性并将它们转换为…| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在呈现资源集合时,文档几乎是相同的,只是`items`JSON 数组中会有多个条目,每个条目对应一个条目。 + +Spring Hateoas 更具体地说将: + +* 将整个集合的`self`链接放入顶层`href`属性。 + +* 将`CollectionModel`链接(减去`self`)放入顶层`links`。 + +* 每个条目级别`href`将包含来自`CollectionModel.content`集合的每个条目的相应`self`链接。 + +* 每个条目级别`links`将包含来自`CollectionModel.content`的每个条目的所有其他链接。 + +### 4.5.UBER-交换表示的统一基础 + +[UBER](https://rawgit.com/uber-hypermedia/specification/master/uber-hypermedia.html)是一种实验性的 JSON 规范 + +> > > > +> UBER 的文档格式是一种最小的读/写超媒体类型,旨在支持简单的状态传输和 ad-hoc +> 基于超媒体的转换。 +> > > > + +——Mike Amundsen + +> UBER 规范 + +UBER 提供了一种统一的方式来表示单个项目资源以及集合。要启用此媒体类型,请在代码中加入以下配置: + +例 39.启用 UBER+JSON 的应用程序 + +``` +@Configuration +@EnableHypermediaSupport(type = HypermediaType.UBER) +public class UberApplication { + +} +``` + +此配置将使你的应用程序使用`Accept`头`application/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" + } ] + } ] + } +} +``` + +这种媒体类型和规范本身都还在开发中。如果你在使用它时遇到问题,请随意[开一张票](https://github.com/spring-projects/spring-hateoas/issues)。 + +| |**UBER 媒体类型**与乘车共享公司**UBERTechnologies Inc.。**没有任何关联。| +|---|-----------------------------------------------------------------------------------------------------------| + +### 4.6.ALP-应用程序级配置文件语义 + +[ALPS](https://tools.ietf.org/html/draft-amundsen-richardson-foster-alps-01)是一种媒体类型,用于提供有关另一资源的基于配置文件的元数据。 + +> > > > +> 一份阿尔卑斯山的文档可以作为一个配置文件来使用 +> 用应用程序解释文档的应用程序语义- +> 不可知的媒体类型(如 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.基于社区的媒体类型 + +由于[创建自己的媒体类型的能力](#mediatypes.custom),有几个社区领导的努力,以建立额外的媒体类型。 + +#### 4.7.1.JSON:API + +* [规格](https://jsonapi.org) + +* 媒体类型名称:`application/vnd.api+json` + +* 最新版本 + + * [参考文献](https://toedter.github.io/spring-hateoas-jsonapi/release/reference/index.html) + + * [API 文档](https://toedter.github.io/spring-hateoas-jsonapi/release/api/index.html) + +* 当前快照 + + * [参考文献](https://toedter.github.io/spring-hateoas-jsonapi/snapshot/reference/index.html) + + * [API 文档](https://toedter.github.io/spring-hateoas-jsonapi/snapshot/api/index.html) + +* [Source](https://github.com/toedter/spring-hateoas-jsonapi) + +* 项目负责人:[Kai Toedter](https://github.com/toedter) + +Maven 坐标 + +``` + + com.toedter + spring-hateoas-jsonapi + {see project page for current version} + +``` + +Gradle 坐标 + +``` +implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}' +``` + +如果你想要发布快照,请访问项目页面了解更多详细信息。 + +#### 4.7.2.警报器 + +* [规格](https://github.com/kevinswiber/siren) + +* 媒体类型名称:`application/vnd.siren+json` + +* [参考文献](https://spring-hateoas-siren.ingogriebsch.de) + +* [javadocs](https://spring-hateoas-siren.ingogriebsch.de/apidocs) + +* [Source](https://github.com/ingogriebsch/spring-hateoas-siren) + +* 项目负责人:[Ingo Griebsch](https://github.com/ingogriebsch) + +Maven 坐标 + +``` + + de.ingogriebsch.hateoas + spring-hateoas-siren + {see project page for current version} + compile + +``` + +Gradle 坐标 + +``` +implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}' +``` + +### 4.8.注册自定义媒体类型 + +Spring Hateoas 允许你通过 SPI 集成自定义媒体类型。这种实现的基本要素是: + +1. 某种形式的 Jackson`ObjectMapper`定制。在最简单的情况下,这是一个 Jackson`Module`实现。 + +2. 一个`LinkDiscoverer`实现,使客户端支持能够检测表示中的链接。 + +3. 一小部分基础设施配置,它将允许 Spring Hateoas 找到自定义实现并获取它。 + +#### 4.8.1.自定义媒体类型配置 + +Spring Hateoas 通过扫描应用程序上下文以查找`HypermediaMappingInformation`接口的任何实现来获取自定义的媒体类型实现。每个媒体类型都必须实现这个接口,以便: + +* 应用于[`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 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 模块通常声明`Serializer`和`Deserializer`实现用于表示模型类型`RepresentationModel`、`EntityModel`、`CollectionModel`和`PagedModel`。如果需要对 Jackson`ObjectMapper`进行进一步的自定义(如自定义`HandlerInstantiator`),则可以替代地覆盖`configureObjectMapper(…)`。 + +| |以前的参考文档版本已经提到实现`MediaTypeConfigurationProvider`接口并将其注册为`spring.factories`。
这是不必要的。
此 SPI 仅用于 Spring Hateoas 提供的开箱即用媒体类型。
仅实现`HypermediaMappingInformation`接口将它注册为 Spring Bean 是所有需要的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.8.2.建议 + +实现媒体类型表示的首选方法是提供与预期格式匹配的类型层次结构,并且可以按原样由 Jackson 序列化。在`Serializer`和`Deserializer`为`RepresentationModel`注册的实现中,将实例转换为特定于媒体类型的模型类型,然后为这些类型查找 Jackson 序列化器。 + +默认情况下支持的媒体类型使用与第三方实现相同的配置机制。因此,值得研究[the`mediatype`包](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](https://tools.ietf.org/html/draft-kelly-json-hal)以及默认呈现。使用注释将触发以下操作: + +* 它注册了必要的 Jackson 模块,以超媒体特定的格式呈现`EntityModel`和`CollectionModel`。 + +* 如果 JsonPath 位于 Classpath 上,它会自动注册一个`LinkDiscoverer`实例,以便通过它们的`rel`在普通的 JSON 表示中查找链接(参见[使用`LinkDiscoverer`实例](#client.link-discoverer))。 + +* 默认情况下,它启用[实体链接](#fundamentals.obtaining-links.entity-links)并自动获取`EntityLinks`实现,并将它们捆绑到`DelegatingEntityLinks`实例中,你可以自动连接这些实例。 + +* 它会自动拾取`RelProvider`中的所有`ApplicationContext`实现,并将它们捆绑到可以自动连接的`DelegatingRelProvider`中。它在 Spring MVC 控制器以及域类型上注册要考虑`@Relation`的提供者。如果[EVO 折弯机](https://github.com/atteo/evo-inflector)在 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 函式库](https://blog.codecentric.de/en/2013/11/traverson/)。下面的示例展示了如何使用它: + +``` +Map 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> resourceParameterizedTypeReference = new ParameterizedTypeReference>() {}; + +EntityModel 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> resourceParameterizedTypeReference = new ParameterizedTypeReference>() {}; + +Map params = Collections.singletonMap("projection", "noImages"); + +EntityModel 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`vs.`CollectionModel` + +到目前为止展示的示例演示了如何避开 Java 的类型擦除,并将单个 JSON 格式的资源转换为`EntityModel`对象。但是,如果你得到了一个类似`\_embedded`HAL 集合的集合,该怎么办?你只需做一个小的调整就可以做到这一点,如下例所示: + +``` +CollectionModelType collectionModelType = + TypeReferences.CollectionModelType() {}; + +CollectionModel 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>() {}) (3) + .consumeWith(result -> { + CollectionModel> 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>`的响应。| +|**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() {}) (4) + .consumeWith(result -> { + // assert against this EntityModel! + }); + } +} +``` + +|**1**|这是 Spring boot 的测试注释,它将为这个测试类配置`WebTestClient.Builder`。| +|-----|-------------------------------------------------------------------------------------------------------------------| +|**2**|将 AutoWire Spring boot 的`WebTestClient.Builder`转换为`builder`和 Spring Hateoas 的配置器作为方法参数。| +|**3**|使用`HypermediaWebTestClientConfigurer`注册对超媒体的支持。| +|**4**|使用`TypeReferences`返回要`EntityModel`的信号。| + +同样,你可以使用与前面的示例类似的断言。 + +还有许多其他方法来设计测试用例。`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`实例将能够使用超媒体进行交互。 \ No newline at end of file -- GitLab