1. 前言
1.1. 迁移到春季 HATEOAS 1.0
对于 1.0,我们借此机会重新评估了我们为 0.x 分支所做的一些设计和包结构选择。 关于它的反馈数量非常多,主要版本的颠簸似乎是重构这些反馈的最自然的地方。
1.1.1. 变更
包结构的最大变化是由引入超媒体类型注册API驱动的,以支持Spring HATEOAS中的其他媒体类型。 这导致客户端和服务器 API(分别命名的包)以及包中的媒体类型实现的明确分离。mediatype
将代码库升级到新 API 的最简单方法是使用迁移脚本。 在我们跳到之前,以下是快速浏览的变化。
表示模型
/// 类组从未真正觉得命名合适。 毕竟,这些类型实际上并不表示资源,而是可以用超媒体信息和提供来丰富表示模型。 以下是新名称与旧名称的映射方式:ResourceSupportResourceResourcesPagedResources
- ResourceSupport现在是RepresentationModel
- Resource现在是EntityModel
- Resources现在是CollectionModel
- PagedResources现在是PagedModel
因此,已重命名为 和 其方法,并分别重命名为 和。 此外,名称更改也反映在 中包含的类中。ResourceAssemblerRepresentationModelAssemblertoResource(…)toResources(…)toModel(…)toCollectionModel(…)TypeReferences
- RepresentationModel.getLinks()现在公开一个实例(通过 ),因为它公开了额外的 API 以使用各种策略连接和合并不同的实例。 此外,它还已转换为自绑定泛型类型,以允许向实例添加链接的方法返回实例本身。LinksList<Link>Links
- API 已移至包中。LinkDiscovererclient
- 和 API 已移至包中。LinkBuilderEntityLinksserver
- ControllerLinkBuilder已移入并已弃用 替换。server.mvcWebMvcLinkBuilder
- RelProvider已重命名为并返回实例而不是 s。LinkRelationProviderLinkRelationString
- VndError已移至包。mediatype.vnderror
1.1.2. 迁移脚本
你可以找到一个从应用程序根目录运行的脚本,该脚本将更新所有导入语句和静态方法引用,这些引用将更新到我们的源代码存储库中移动的 Spring HATEOAS 类型。 只需下载它,从您的项目根目录运行它。 默认情况下,它将检查所有 Java 源文件,并将旧的 Spring HATEOAS 类型引用替换为新的引用。
例 1.迁移脚本的示例应用程序
$ ./migrate-to-1.0.shMigrating Spring HATEOAS references to 1.0 for files : *.javaAdapting ./src/main/java/……Done!请注意,脚本不一定能够完全修复所有更改,但它应该涵盖最重要的重构。
现在验证对你喜欢的 Git 客户端中的文件所做的更改,并根据需要提交。 如果您发现方法或类型引用未迁移,请在问题跟踪器中打开票证。
1.1.3. 从 1.0 M3 迁移到 1.0 RC1
- Link.andAffordance(…)获取启示详细信息已移至 。现在要手动构建实例,请使用 。另请注意公开的新类型,以便流畅使用。有关详细信息,请参阅提示。AffordancesAffordanceAffordances.of(link).afford(…)AffordanceBuilderAffordances
- AffordanceModelFactory.getAffordanceModel(…)现在接收和实例而不是 s 以允许非基于类型的实现。自定义媒体类型实现必须相应地进行调整。InputPayloadMetadataPayloadMetadataResolvableType
- HAL 窗体现在不会呈现属性属性,如果属性属性的值符合规范中定义为默认值的值。即,如果以前明确设置为 ,我们现在只需省略 . 我们现在也只强制它们对于用作 HTTP 方法的模板不是必需的。requiredfalserequiredPATCH
2. 基础知识
本节介绍Spring HATEOAS的基础知识及其基本的领域抽象。
2.1. 链接
超媒体的基本思想是用超媒体元素丰富资源的表示。 最简单的形式是链接。 它们指示客户端可以导航到某个资源。 相关资源的语义在所谓的链接关系中定义。 您可能已经在 HTML 文件的标头中看到了这一点:
例 2.HTML 文档中的链接
<link href="theme.css" rel="stylesheet" type="text/css" />如您所见,链接指向资源并指示它是一个样式表。 链接通常带有其他信息,例如资源指向的媒体类型将返回。 但是,链接的基本构建块是其引用和关系。theme.css
Spring HATEOAS 允许您通过其不可变的值类型使用链接。 它的构造函数同时采用超文本引用和链接关系,后者默认为 IANA 链接关系 . 在链接关系中阅读有关后者的更多信息。Linkself
例 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 中定义的其他属性。 您可以通过在实例上调用相应的 wither 方法来设置它们。Link
有关如何创建指向Spring MVC和Spring WebFlux控制器的链接的更多信息,请参阅Spring MVC中的构建链接和Spring WebFlux中的构建链接。
2.2. URI 模板
对于Spring HATEOAS,超文本引用不仅可以是URI,还可以是RFC-6570的URI模板。 URI 模板包含所谓的模板变量,并允许扩展这些参数。 这允许客户端将参数化模板转换为 URI,而无需知道最终 URI 的结构,它只需要知道变量的名称。Link
例 4.使用具有模板化 URI 的链接
Link link = Link.of("/{segment}/something{?parameter}");assertThat(link.isTemplated()).isTrue(); assertThat(link.getVariableNames()).contains("segment", "parameter"); Map<String, Object> values = new HashMap<>();values.put("segment", "path");values.put("parameter", 42);assertThat(link.expand(values).getHref()) .isEqualTo("/path/something?parameter=42");该实例指示它是模板化的,即它包含一个 URI 模板。Link
它公开模板中包含的参数。
它允许扩展参数。
可以手动构造 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 提供了一种类型,可以轻松创建基于它的实例。LinkRelationString
2.3.1. IANA 链接关系
互联网号码分配机构包含一组预定义的链路关系。 可以通过 引用它们。IanaLinkRelations
例 6.使用 IANA 链接关系
Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);assertThat(link.getRel()).isEqualTo(LinkRelation.of("next"));assertThat(IanaLinkRelation.isIanaRel(link.getRel())).isTrue();2.4. 表示模型
为了轻松创建超媒体丰富的表示形式,Spring HATEOAS 提供了一组根目录类。 它基本上是 s 集合的容器,并具有将这些添加到模型中的方便方法。 稍后可以将模型呈现为各种媒体类型格式,这些格式将定义超媒体元素在表示中的外观。 有关此内容的详细信息,请查看媒体类型。RepresentationModelLink
例 7.类层次结构RepresentationModel
class RepresentationModelclass EntityModelclass CollectionModelclass PagedModelEntityModel -|> RepresentationModelCollectionModel -|> RepresentationModelPagedModel -|> CollectionModel使用 a 的默认方法是创建它的子类以包含表示应该包含的所有属性,创建该类的实例,填充属性并使用链接丰富它。RepresentationModel
例 8.示例表示模型类型
class PersonModel extends RepresentationModel<PersonModel> { String firstname, lastname;}通用自键入对于让返回自身的实例是必要的。 模型类型现在可以像这样使用:RepresentationModel.add(…)
例 9.使用人员表示模型
PersonModel model = new PersonModel();model.firstname = "Dave";model.lastname = "Matthews";model.add(Link.of("https://myhost/people/42"));如果您从 Spring MVC 或 WebFlux 控制器返回了这样的实例,并且客户端发送了一个设置为 的标头,则响应将如下所示:Acceptapplication/hal+json
例 10.为人员表示模型生成的 HAL 表示
{ "_links" : { "self" : { "href" : "https://myhost/people/42" } }, "firstname" : "Dave", "lastname" : "Matthews"}2.4.1. 项目资源表示模型
对于由单个对象或概念支持的资源,存在便利类型。 无需为每个概念创建自定义模型类型,只需重用现有类型并将其实例包装到 .EntityModelEntityModel
例 11.用于包装现有对象EntityModel
Person person = new Person("Dave", "Matthews");EntityModel<Person> model = EntityModel.of(person);2.4.2. 集合资源表示模型
对于概念上属于集合的资源,可以使用 。 它的元素可以是简单的对象,也可以依次是实例。CollectionModelRepresentationModel
例 12.用于包装现有对象的集合CollectionModel
Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews"));CollectionModel<Person> model = CollectionModel.of(people);虽然 被约束为始终包含有效负载,因此允许推理唯一实例上的类型排列,但 的基础集合可以为空。 由于 Java 的类型擦除,我们实际上无法检测到 a 实际上是 a,因为我们看到的只是运行时实例和一个空集合。 可以将缺少的类型信息添加到模型中,方法是在构造时将其添加到空实例中,或者在基础集合可能为空的情况下作为回退:EntityModelCollectionModelCollectionModel<Person> model = CollectionModel.empty()CollectionModel<Person>CollectionModel.empty(Person.class)
Iterable<Person> people = repository.findAll();var model = CollectionModel.of(people).withFallbackType(Person.class);3. 服务器端支持
3.1. 在 Spring MVC 中构建链接
现在我们已经有了域词汇表,但主要的挑战仍然存在:如何创建实际的 URI,以不那么脆弱的方式包装到实例中。现在,我们将不得不到处复制 URI 字符串。这样做很脆弱且不可维护。Link
假设你已经实现了Spring MVC控制器,如下所示:
@Controllerclass PersonController { @GetMapping("/people") HttpEntity<PersonModel> showAll() { … } @GetMapping(value = "/{person}", method = RequestMethod.GET) HttpEntity<PersonModel> show(@PathVariable Long person) { … }}我们在这里看到两个约定。第一个是通过控制器方法的注释公开的集合资源,该集合的各个元素作为直接子资源公开。集合资源可能会在简单 URI(如所示)或更复杂的 URI(如 )处公开。假设您想链接到所有人的集合资源。遵循上面的方法会导致两个问题:@GetMapping/people/{id}/addresses
- 要创建绝对 URI,您需要查找协议、主机名、端口、servlet 基和其他值。这很麻烦,需要丑陋的手动字符串连接代码。
- 您可能不希望在基本 URI 之上连接 URI,因为这样您必须在多个位置维护信息。如果更改映射,则必须更改指向它的所有客户端。/people
Spring HATEOAS 现在提供了一个允许您通过指向控制器类来创建链接的功能。 以下示例演示如何执行此操作:WebMvcLinkBuilder
Link link = linkTo(PersonController.class).withRel("people");assertThat(link.getRel()).isEqualTo(LinkRelation.of("people"));assertThat(link.getHref()).endsWith("/people");它使用 Spring 的底层从当前请求中获取基本的 URI 信息。假设您的应用程序在 上运行,这正是您在其上构造其他部分的 URI。构建器现在检查给定控制器类的根映射,因此以 .您还可以构建更多嵌套链接。 以下示例演示如何执行此操作:WebMvcLinkBuilderServletUriComponentsBuilderlocalhost:8080/your-applocalhost:8080/your-app/people
Person person = new Person(1L, "Dave", "Matthews");// /person / 1Link 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. 构建指向方法的链接
您甚至可以构建指向方法的链接或创建虚拟控制器方法调用。 第一种方法是将实例交给 . 以下示例演示如何执行此操作:MethodWebMvcLinkBuilder
Method method = PersonController.class.getMethod("show", Long.class);Link link = linkTo(method, 2L).withSelfRel();assertThat(link.getHref()).endsWith("/people/2"));这仍然有点令人不满意,因为我们必须首先获得一个实例,该实例会引发异常并且通常非常麻烦。至少我们不会重复映射。更好的方法是在控制器代理上对目标方法进行虚拟方法调用,我们可以使用帮助程序创建该代理。 以下示例演示如何执行此操作:MethodmethodOn(…)
Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();assertThat(link.getHref()).endsWith("/people/2");methodOn(…)创建控制器类的代理,该代理记录方法调用,并在为方法的返回类型创建的代理中公开它。这允许我们想要获取映射的方法的流畅表达。但是,使用此技术可以获得的方法有一些限制:
- 返回类型必须能够代理,因为我们需要在其上公开方法调用。
- 通常忽略移交给方法的参数(通过 引用的参数除外,因为它们构成了 URI)。@PathVariable
控制请求参数的呈现
集合值请求参数实际上可以通过两种不同的方式具体化。 URI 模板规范列出了呈现它们的复合方式,该方式为每个值 () 重复参数名称,以及用逗号 () 分隔值的非复合方式。 Spring MVC 正确地解析了两种格式的集合。 默认情况下,呈现值默认为复合样式。 如果要以非复合样式呈现值,可以将注释与请求参数处理程序方法参数一起使用:param=value1¶m=value2param=value1,value2@NonComposite
@Controllerclass PersonController { @GetMapping("/people") HttpEntity<PersonModel> showAll( @NonComposite @RequestParam Collection<String> names) { … } }var values = List.of("Matthews", "Beauford");var link = linkTo(methodOn(PersonController.class).showAll(values)).withSelfRel(); assertThat(link.getHref()).endsWith("/people?names=Matthews,Beauford");我们使用注释来声明我们希望值以逗号分隔。@NonComposite
我们使用值列表调用该方法。
查看请求参数如何以预期格式呈现。
我们公开的原因是,渲染请求参数的复合方式被烘焙到 Spring 构建器的内部,我们只在 Spring HATEOAS 1.4 中引入了这种非复合样式。 如果我们今天从头开始,我们可能会默认使用该样式,而是让用户显式选择复合样式,而不是相反。@NonCompositeUriComponents
3.2. 在Spring WebFlux中构建链接
待办事项
3.3. 提示
环境的可负担性就是它所提供的......它提供或提供的东西,无论是好的还是坏的。动词“负担”在字典中找到,但名词“负担”不是。我已经编好了。
——詹姆斯·吉布森视觉感知的生态方法(第126页)
基于 REST 的资源不仅提供数据,还提供控制。 形成灵活服务的最后一个要素是有关如何使用各种控件的详细提示。 由于提示与链接相关联,Spring HATEOAS 提供了一个 API,用于根据需要将尽可能多的相关方法附加到链接。 就像您可以通过指向Spring MVC控制器方法来创建链接一样(有关详细信息,请参阅在Spring MVC中构建链接),您...
以下代码演示如何获取自链接并关联另外两个提示:
例 13.将启示连接到GET /employees/{id}
@GetMapping("/employees/{id}")public EntityModel<Employee> findOne(@PathVariable Integer id) { Class<EmployeeController> controllerClass = EmployeeController.class; // Start the affordance with the "self" link, i.e. this method. Link findOneLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel(); // 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))) .andAffordance(afford(methodOn(controllerClass).partiallyUpdateEmployee(null, id)))); }创建自链接。
将方法与链接关联。updateEmployeeself
将方法与链接关联。partiallyUpdateEmployeeself
使用 ,可以使用控制器的方法将 和 操作连接到操作。 想象一下,上面提供的相关方法看起来像这样:.andAffordance(afford(…))PUTPATCHGET
例 14。 响应的方法updateEmpoyeePUT /employees/{id}
@PutMapping("/employees/{id}")public ResponseEntity<?> updateEmployee( // @RequestBody EntityModel<Employee> employee, @PathVariable Integer id)例 15。 响应的方法partiallyUpdateEmployeePATCH /employees/{id}
@PatchMapping("/employees/{id}")public ResponseEntity<?> partiallyUpdateEmployee( // @RequestBody EntityModel<Employee> employee, @PathVariable Integer id)使用这些方法指向这些方法将导致Spring HATEOS分析请求正文和响应类型并捕获元数据,以允许不同的媒体类型实现使用该信息将其转换为输入和输出的描述。afford(…)
3.3.1. 手动构建提示
虽然这是为链接注册提示的主要方法,但可能需要手动构建其中的一些提示。 这可以通过使用 API 来实现:Affordances
例 16。使用 API 手动注册提示Affordances
var methodInvocation = methodOn(EmployeeController.class).all();var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) .afford(HttpMethod.POST) .withInputAndOutput(Employee.class) // .withName("createEmployee") // .andAfford(HttpMethod.GET) .withOutput(Employee.class) // .addParameters(// QueryParameter.optional("name"), // QueryParameter.optional("role")) // .withName("search") // .toLink();首先,从实例创建 的实例,创建用于描述提示的上下文。AffordancesLink
每个功能都从它应该支持的 HTTP 方法开始。然后,我们将类型注册为有效负载描述,并显式命名提示。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这有效地创建了与指向创建的指针相同的提示。EmployeeController.newEmployee(…)
构建下一个提示以反映指向 的指针发生的情况。在这里,我们定义为创建的响应的模型并显式注册 s。EmployeeController.search(…)EmployeeQueryParameter
提示由特定于媒体类型的提示模型提供支持,这些模型将常规提示元数据转换为特定表示形式。 请务必查看媒体类型部分中有关提示的部分,以查找有关如何控制该元数据公开的更多详细信息。
3.4. 转发标头处理
RFC-7239 转发标头最常用于应用程序位于代理后面、负载均衡器后面或云中的情况。 实际接收 Web 请求的节点是基础结构的一部分,并将请求转发到应用程序。
您的应用程序可能在 上运行,但对于外部世界而言,您应该在 (以及 Web 的标准端口 80) 上运行。 通过让代理包含额外的标头(许多人已经这样做了),Spring HATEOAS 可以正确生成链接,因为它使用 Spring Framework 功能来获取原始请求的基本 URI。localhost:8080reallycoolsite.com
任何可以基于外部输入更改根 URI 的内容都必须得到妥善保护。 这就是默认情况下禁用转发标头处理的原因。 您必须使其正常运行。 如果要部署到云或控制代理和负载均衡器的配置中,那么您肯定希望使用此功能。
要启用转发标头处理,您需要在应用程序中注册Spring for Spring MVC(详细信息在此处)或Spring WebFlux(详细信息在此处)。 在 Spring 引导应用程序中,这些组件可以简单地声明为 Spring bean,如此处所述。ForwardedHeaderFilterForwardedHeaderTransformer
例 17.注册一个ForwardedHeaderFilter
@BeanForwardedHeaderFilter forwardedHeaderFilter() { return new ForwardedHeaderFilter();}这将创建一个处理所有标头的 servlet 过滤器。 它将向 servlet 处理程序正确注册它。X-Forwarded-…
对于Spring WebFlux应用程序,反应式对应物是:ForwardedHeaderTransformer
例 18.注册一个ForwardedHeaderTransformer
@BeanForwardedHeaderTransformer forwardedHeaderTransformer() { return new ForwardedHeaderTransformer();}这将创建一个函数来转换反应式 Web 请求,处理标头。 它将向 WebFlux 正确注册它。X-Forwarded-…
如上所示的配置到位后,传递标头的请求将看到生成的链接中反映的标头: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" } }}3.5. 使用实体链接界面
EntityLinks并且它的各种实现目前没有为Spring WebFlux应用程序提供开箱即用。 SPI中定义的合约最初针对Spring Web MVC,不考虑Reactor Type。 开发支持响应式编程的类似合约仍在进行中。EntityLinks
到目前为止,我们已经通过指向 Web 框架实现(即 Spring MVC 控制器)创建了链接并检查了映射。 在许多情况下,这些类实质上是读取和写入由模型类支持的表示形式。
该接口现在公开一个 API,用于查找 或 基于模型类型。 这些方法实质上返回指向集合资源(如 )或项资源(如 )的链接。 以下示例演示如何使用:EntityLinksLinkLinkBuilder/people/people/1EntityLinks
EntityLinks links = …;LinkBuilder builder = links.linkFor(Customer.class);Link link = links.linkToItemResource(Customer.class, 1L);EntityLinks可通过在 Spring MVC 配置中激活的依赖注入获得。 这将导致注册的各种默认实现。 最基本的一个是检查 SpringMVC 控制器类。 如果要注册自己的实现,请查看此部分。@EnableHypermediaSupportEntityLinksControllerEntityLinksEntityLinks
3.5.1. 基于 Spring MVC 控制器的实体链接
激活实体链接功能会导致检查当前所有可用的 Spring MVC 控制器以进行注释。 注释公开控制器管理的模型类型。 除此之外,我们假设您遵守以下 URI 映射设置和约定:ApplicationContext@ExposesResourceFor(…)
- 声明控制器为其公开集合和项资源的实体类型的类型级别。@ExposesResourceFor(…)
- 表示集合资源的类级别基本映射。
- 一种附加方法级映射,用于扩展映射以将标识符追加为附加路径段。
以下示例显示了支持 -的控制器的实现:EntityLinks
@Controller@ExposesResourceFor(Order.class) @RequestMapping("/orders") class OrderController { @GetMapping ResponseEntity orders(…) { … } @GetMapping("{id}") ResponseEntity order(@PathVariable("id") … ) { … }}控制器指示它正在公开实体的集合和项资源。Order
其集合资源在/orders
该集合资源可以处理请求。在方便时为其他 HTTP 方法添加更多方法。GET
一种额外的控制器方法,用于处理从属资源,采用路径变量来公开项目资源,即单个 .Order
完成此操作后,当您在Spring MVC配置中启用时,您可以创建指向控制器的链接,如下所示:EntityLinks@EnableHypermediaSupport
@Controllerclass PaymentController { private final EntityLinks entityLinks; PaymentController(EntityLinks entityLinks) { this.entityLinks = entityLinks; } @PutMapping(…) ResponseEntity payment(@PathVariable Long orderId) { Link link = entityLinks.linkToItemResource(Order.class, orderId); … }}注入在您的配置中可用。EntityLinks@EnableHypermediaSupport
使用 API 通过实体类型而不是控制器类来构建链接。
如您所见,您可以引用管理实例的资源,而无需显式引用。OrderOrderController
3.5.2. 实体链接 API 详情
从根本上说,允许为实体类型的集合和项资源构建 s 和实例。 从 开始的方法将生成实例,供您使用其他路径段、参数等进行扩展和扩充。 从 开始的方法会生成完全准备好的实例。EntityLinksLinkBuilderLinklinkFor…LinkBuilderlinkToLink
虽然对于集合资源,提供实体类型就足够了,但指向项资源的链接将需要提供标识符。 这通常看起来像这样:
例 21。获取指向物料资源的链接
entityLinks.linkToItemResource(order, order.getId());如果您发现自己重复这些方法调用,则可以将标识符提取步骤拉出为可重用的,以便在不同的调用中重用:Function
Function<Order, Object> idExtractor = Order::getId; entityLinks.linkToItemResource(order, idExtractor);标识符提取是外部化的,以便可以将其保存在字段或常量中。
使用提取程序进行链接查找。
类型实体链接
由于控制器实现通常围绕实体类型进行分组,因此您经常会发现自己在整个控制器类中使用相同的提取器函数(有关详细信息,请参阅EntityLinks API)。 我们可以通过获取一次提供提取器的实例来进一步集中标识符提取逻辑,这样实际的查找就不必再处理提取了。TypedEntityLinks
例 22。使用类型化实体链接
class OrderController { private final TypedEntityLinks<Order> links; OrderController(EntityLinks entityLinks) { this.links = entityLinks.forType(Order::getId); } @GetMapping ResponseEntity<Order> someMethod(…) { Order order = … // lookup order Link link = links.linkToItemResource(order); }}注入实例。EntityLinks
指示您将查找具有特定标识符提取器函数的实例。Order
基于唯一实例查找项目资源链接。Order
3.5.3. 作为 SPI 的实体链接
由 创建的实例的类型又将选取所有其他可用作 bean 的实现。 它被注册为主 bean,因此当您通常注入时,它始终是唯一的注射候选者。 是将包含在安装程序中的默认实现,但用户可以自由实现和注册自己的实现。 使这些可用于实例进行注入是将您的实现注册为 Spring Bean 的问题。EntityLinks@EnableHypermediaSupportDelegatingEntityLinksEntityLinksApplicationContextEntityLinksControllerEntityLinksEntityLinks
例 23。声明自定义实体链接实现
@Configurationclass CustomEntityLinksConfiguration { @Bean MyEntityLinks myEntityLinks(…) { return new MyEntityLinks(…); }}这种机制的可扩展性的一个例子是Spring Data REST的RepositoryEntityLinks,它使用存储库映射信息来创建指向Spring Data存储库支持的资源的链接。 同时,它甚至公开了其他类型的资源的其他查找方法。 如果要使用这些,只需显式注入即可。RepositoryEntityLinks
3.6. 表示模型汇编器
由于从实体到表示模型的映射必须在多个位置使用,因此创建一个负责执行此操作的专用类是有意义的。转换包含非常自定义的步骤,但也包含一些样板步骤:
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(…)是您编写的代码,用于实例化给定对象的对象。它应该只关注设置属性,而不是填充。PersonModelPersonLinks
按照前面示例中的方式设置类可带来以下好处:
- 有几种方法可以让您创建资源的实例,并向其添加 rel。该链接的 href 由配置的控制器的请求映射加上实体的 ID(例如 )。createModelWithId(…)Linkself/people/1
- 资源类型通过反射实例化,并需要一个无参数构造函数。如果要使用专用构造函数或避免反射性能开销,可以覆盖 。instantiateModel(…)
然后,您可以使用汇编程序组装 或 . 以下示例创建一个实例:RepresentationModelCollectionModelCollectionModelPersonModel
Person person = new Person(…);Iterable<Person> people = Collections.singletonList(person);PersonModelAssembler assembler = new PersonModelAssembler();PersonModel model = assembler.toModel(person);CollectionModel<PersonModel> model = assembler.toCollectionModel(people);3.7. 表示模型处理器
有时,您需要在组装超媒体表示后对其进行调整和调整。
一个完美的例子是,当您有一个处理订单履行的控制器时,但您需要添加与付款相关的链接。
想象一下,让你的订购系统产生这种类型的超媒体:
{ "orderId" : "42", "state" : "AWAITING_PAYMENT", "_links" : { "self" : { "href" : "http://localhost/orders/999" } }}您希望添加一个链接,以便客户可以付款,但不想将有关您的详细信息混合到 这。 与其污染订购系统的细节,不如这样写:PaymentControllerOrderControllerRepresentationModelProcessor
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> { @Override public EntityModel<Order> process(EntityModel<Order> model) { model.add( Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) // .expand(model.getContent().getOrderId())); return model; }}此处理器将仅应用于对象。EntityModel<Order>
通过添加无条件链接来操作现有对象。EntityModel
返回 ,以便可以将其序列化为请求的媒体类型。EntityModel
将处理器注册到您的应用程序:
@Configurationpublic class PaymentProcessingApp { @Bean PaymentProcessor paymentProcessor() { return new PaymentProcessor(); }}现在,当您发出 的超媒体表示形式时,客户端会收到以下内容:Order
{ "orderId" : "42", "state" : "AWAITING_PAYMENT", "_links" : { "self" : { "href" : "http://localhost/orders/999" }, "payments" : { "href" : "/payments/42" } }}您会看到插入的插件作为此链接的关系。LinkRelation.of("payments")
URI 由处理器提供。
此示例非常简单,但您可以轻松:
- 使用 或 构建指向 .WebMvcLinkBuilderWebFluxLinkBuilderPaymentController
- 注入有条件地添加由状态驱动的其他链接(例如 )所需的任何服务。cancelamend
- 利用 Spring Security 等跨领域服务,根据当前用户的上下文添加、删除或修改链接。
此外,在此示例中,更改了提供的 .您还可以将其替换为另一个对象。请注意,API 要求返回类型等于输入类型。PaymentProcessorEntityModel<Order>
3.7.1. 处理空集合模型
为了找到要为实例调用的正确实例集,调用基础结构会对已注册的泛型声明执行详细分析。 例如,这包括检查底层集合的元素,因为在运行时,唯一的模型实例不会公开泛型信息(由于 Java 的类型擦除)。 这意味着,默认情况下,不会为空集合模型调用实例。 要仍允许基础设施正确推断有效负载类型,您可以从一开始就使用显式回退有效负载类型初始化空实例,或通过调用 . 有关详细信息,请参阅集合资源表示模型。RepresentationModelProcessorRepresentationModelRepresentationModelProcessorCollectionModelRepresentationModelProcessorCollectionModelCollectionModel.withFallbackType(…)
3.8. 使用 APILinkRelationProvider
构建链接时,通常需要确定要用于链接的关系类型。在大多数情况下,关系类型与(域)类型直接关联。我们封装了详细的算法,以查找 API 背后的关系类型,以便确定单个资源和集合资源的关系类型。查找关系类型的算法如下:LinkRelationProvider
当您使用 时,A 会自动显示为春豆。您可以通过实现接口并依次将它们公开为 Spring bean 来插入自定义提供程序。LinkRelationProvider@EnableHypermediaSupport
4. 媒体类型
4.1. HAL – 超文本应用程序语言
JSON 超文本应用程序语言或 HAL 是最简单的语言之一 以及最广泛采用的超媒体媒体类型,在不讨论特定的 Web 堆栈时采用。
这是Spring HATEOAS采用的第一个基于规范的媒体类型。
4.1.1. 构建 HAL 表示模型
从Spring HATEOAS 1.1开始,我们提供了一个专用的,允许通过HAL惯用API创建实例。 这些是它的基本假设:HalModelBuilderRepresentationModel
下面是所用 API 的示例:
// An ordervar order = new Order(…); // The customer who placed the ordervar customer = customer.findById(order.getCustomerId());var customerLink = Link.of("/orders/{id}/customer") .expand(order.getId()) .withRel("customer");var additional = …var model = HalModelBuilder.halModelOf(order) .preview(new CustomerSummary(customer)) .forLink(customerLink) .embed(additional) .link(Link.of(…, IanaLinkRelations.SELF)); .build();我们设置了一些域类型。在这种情况下,订单与下达它的客户有关系。
我们准备了一个指向资源的链接,该链接将公开客户详细信息
我们通过提供应该在子句中呈现的有效负载来开始构建预览。_embeddable
我们通过提供目标链接来结束预览。它透明地添加到对象中,其链接关系用作上一步中提供的对象的键。_links
可以添加其他对象以显示在 下。 列出它们的键派生自对象关系设置。它们可以通过或专用方式进行自定义(有关详细信息,请参阅使用 LinkRelationProvider API)。_embedded@RelationLinkRelationProvider
{ "_links" : { "self" : { "href" : "…" }, "customer" : { "href" : "/orders/4711/customer" } }, "_embedded" : { "customer" : { … }, "additional" : { … } }}明确提供的链接。self
通过 透明添加的链接。customer….preview(…).forLink(…)
提供的预览对象。
通过显式 . 添加的其他元素。….embed(…)
在 HAL 中也用于表示顶级集合。 它们通常分组在从对象类型派生的链接关系下。 即订单列表在 HAL 中如下所示:_embedded
{ "_embedded" : { "orders : [ … ] }}各个订单文件请点击此处。
Creating such a representation is as easy as this:
Collection<Order> orders = …;HalModelBuilder.emptyHalDocument() .embed(orders);That said, if the order is empty, there’s no way to derive the link relation to appear inside , so that the document will stay empty if the collection is empty._embedded
If you prefer to explicitly communicate an empty collection, a type can be handed into the overload of the method taking a . If the collection handed into the method is empty, this will cause a field rendered with its link relation derived from the given type.….embed(…)Collection
HalModelBuilder.emptyHalModel() .embed(Collections.emptyList(), Order.class); // or .embed(Collections.emptyList(), LinkRelation.of("orders"));will create the following, more explicit representation.
{ "_embedded" : { "orders" : [] }}4.1.2. 配置链接渲染
在 HAL 中,条目是 JSON 对象。属性名称是链接关系和 每个值都是一个链接对象或链接对象数组。_links
对于具有两个或多个链接的给定链接关系,规范在表示上是明确的:
例 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"}如果您希望自定义此策略,您所要做的就是将 Bean 注入到应用程序配置中。 有多种选择。HalConfiguration
例 27。全局 HAL 单链路呈现策略
@Beanpublic HalConfiguration globalPolicy() { return new HalConfiguration() // .withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); }通过将所有单链接关系渲染为数组来覆盖Spring HATEOAS的默认值。
如果您只想覆盖某些特定的链接关系,则可以创建如下所示的 Bean:HalConfiguration
例 28。基于链路关系的 HAL 单链路呈现策略
@Beanpublic HalConfiguration linkRelationBasedPolicy() { return new HalConfiguration() // .withRenderSingleLinksFor( // IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) .withRenderSingleLinksFor( // LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); }始终将链接关系呈现为数组。item
当只有一个链接时,将链接关系呈现为对象。prev
如果这些都不符合您的需求,则可以使用 Ant 样式的路径模式:
例 29。基于模式的 HAL 单链路呈现策略
@Beanpublic HalConfiguration patternBasedPolicy() { return new HalConfiguration() // .withRenderSingleLinksFor( // "http*", RenderSingleLinks.AS_ARRAY); }Render all link relations that start with as an array.http
基于模式的方法使用 Spring 的 .AntPathMatcher
所有这些枯萎可以结合起来形成一个全面的政策。请务必测试您的 API 广泛以避免意外。HalConfiguration
4.1.3. 链接标题国际化
HAL 为其链接对象定义一个属性。 这些标题可以通过使用 Spring 的资源包抽象和命名的资源包来填充,以便客户端可以直接在其 UI 中使用它们。 此捆绑包将自动设置,并在 HAL 链路序列化期间使用。titlerest-messages
要定义链接的标题,请使用密钥模板,如下所示:_links.$relationName.title
例 30。一个示例rest-messages.properties
_links.cancel.title=Cancel order_links.payment.title=Proceed to checkout这将产生以下 HAL 表示形式:
例 31。定义了链接标题的示例 HAL 文档
{ "_links" : { "cancel" : { "href" : "…" "title" : "Cancel order" }, "payment" : { "href" : "…" "title" : "Proceed to checkout" } }}4.1.4. 使用 APICurieProvider
Web 链接 RFC 描述了已注册和扩展链接关系类型。注册的rel是在链接关系类型的IANA注册机构注册的已知字符串。扩展 URI 可由不希望注册关系类型的应用程序使用。每个都是唯一标识关系类型的 URI。URI 可以序列化为紧凑 URI 或 Curie。例如,居里 代表链接关系类型 if 定义为 。如果使用居里,则响应作用域中必须存在基本 URI。relrelex:personsexample.com/rels/personsexexample.com/rels/{rel}
默认情况下创建的值是扩展关系类型,因此必须是 URI,这可能会导致大量开销。API 负责处理这个问题:它允许您将基本 URI 定义为 URI 模板,并定义代表该基本 URI 的前缀。如果存在 ,则在所有值前面加上居里前缀。此外,链接会自动添加到 HAL 资源。relRelProviderCurieProviderCurieProviderRelProviderrelcuries
以下配置定义默认居里提供程序:
@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}")); }}请注意,现在前缀会自动显示在未向 IANA 注册的所有 rel 值之前,如 中所示。客户可以使用该链接将居里解析为其完整形式。 以下示例演示如何执行此操作:ex:ex:orderscuries
{ "_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"}由于 API 的目的是允许自动创建居里,因此每个应用程序范围只能定义一个 Bean。CurieProviderCurieProvider
4.2. 哈尔形式
HAL-FORMS旨在向HAL媒体类型添加运行时FORM支持。
HAL形式“看起来像HAL”。但是,重要的是要记住,HAL-FORMS与HAL不同 - 两者 不应以任何方式被视为可互换。
— 迈克·阿蒙森哈尔形式规格
若要启用此媒体类型,请在代码中放置以下配置:
例 32。启用 HAL 表单的应用程序
@Configuration@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)public class HalFormsApplication {}每当客户端提供带有 的标头时,您都可以期待如下所示的内容:Acceptapplication/prs.hal-forms+json
例 33。HAL 表单示例文档
{ "firstName" : "Frodo", "lastName" : "Baggins", "role" : "ring bearer", "_links" : { "self" : { "href" : "http://localhost:8080/employees/1" } }, "_templates" : { "default" : { "method" : "put", "properties" : [ { "name" : "firstName", "required" : true }, { "name" : "lastName", "required" : true }, { "name" : "role", "required" : true } ] }, "partiallyUpdateEmployee" : { "method" : "patch", "properties" : [ { "name" : "firstName", "required" : false }, { "name" : "lastName", "required" : false }, { "name" : "role", "required" : false } ] } }}查看 HAL-FORMS 规范以了解 _templates 属性的详细信息。 阅读有关提示 API 的信息,以使用此额外的元数据扩充控制器。
至于单项()和聚合根集合(),Spring HATEOS渲染它们 与 HAL 文档相同。EntityModelCollectionModel
4.2.1. 定义 HAL-FORMS 元数据
HAL-FORMS允许描述每个表单字段的标准。 Spring HATEOAS 允许通过塑造输入和输出类型的模型类型并在其上使用注释来自定义这些类型。
每个模板都将定义以下属性:
表 1.模板属性
属性
描述
contentType
服务器应接收的媒体类型。仅当控制器方法指向公开属性或在设置提示时显式定义媒体类型时,才包含。@RequestMapping(consumes = "…")
method
提交模板时要使用的 HTTP 方法。
target
要将表单提交到的目标 URI。仅当提供目标与声明它的链接不同时,才会呈现。
title
显示模板时的人类可读标题。
properties
所有属性将与表格一起提交(见下文)。
每个属性都将定义以下属性:
表 2.属性属性
属性
描述
readOnly
如果属性没有 setter 方法,则设置为 。如果存在,请在访问器或字段上显式使用 Jackson's。默认情况下不呈现,因此默认为 .true@JsonProperty(Access.READ_ONLY)false
regex
可以通过在字段或类型上使用 JSR-303 的注释来自定义。在后者的情况下,该模式将用于声明为该特定类型的每个属性。默认情况下不呈现。@Pattern
required
可以使用 JSR-303 进行定制。默认情况下不呈现,因此默认为 .使用 as 方法的模板将自动将所有属性设置为“不需要”。@NotNullfalsePATCH
max
属性允许的最大值。源自 Hibernate Validator 或 JSR-303 和注释。@Range@Max@DecimalMax
maxLength
属性允许的最大长度值。派生自Hibernate验证器的注释。@Length
min
属性允许的最小值。源自 Hibernate Validator 或 JSR-303 和注释。@Range@Min@DecimalMin
minLength
属性允许的最小长度值。派生自Hibernate验证器的注释。@Length
options
提交表单时用于从中选择值的选项。有关详细信息,请参阅定义属性的 HAL-FORMS 选项。
prompt
呈现表单输入时要使用的用户可读提示。有关详细信息,请参阅属性提示。
placeholder
用户可读的占位符,用于提供预期格式的示例。定义这些的方法遵循属性提示,但使用后缀 。_placeholder
type
派生自显式批注、JSR-303 验证批注或属性类型的 HTML 输入类型。@InputType
对于无法手动注释的类型,可以通过应用程序上下文中存在的 Bean 注册自定义模式。HalFormsConfiguration
@Configurationclass CustomConfiguration { @Bean HalFormsConfiguration halFormsConfiguration() { HalFormsConfiguration configuration = new HalFormsConfiguration(); configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}"); }}此设置将导致类型的表示模型属性的 HAL-FORMS 模板属性声明具有值 的字段。CreditCardNumberregex[0-9]{16}
为属性定义 HAL 窗体选项
对于其值应与某个值超集匹配的属性,HAL-FORMS 在属性定义中定义子文档。 可用于特定属性的选项可以通过 来描述,该 获取指向类型属性的指针和将 a 转换为实例的 creator 函数。optionsHalFormsConfigurationwithOptions(…)PropertyMetadataHalFormsOptions
@Configurationclass CustomConfiguration { @Bean HalFormsConfiguration halFormsConfiguration() { HalFormsConfiguration configuration = new HalFormsConfiguration(); configuration.withOptions(Order.class, "shippingMethod" metadata -> HalFormsOptions.inline("FedEx", "DHL")); }}了解我们如何设置选项值以及作为属性选择的选项。 或者,可以指向动态提供值的远程资源。 有关选项设置的更多约束,请参阅 规范或 的 Javadoc。FedExDHLOrder.shippingMethodHalFormsOptions.remote(…)HalFormsOptions
4.2.2. 表单属性的国际化
HAL-FORMS 包含用于人工解释的属性,例如模板的标题或属性提示。 这些可以使用 Spring 的资源包支持和 Spring HATEOAS 默认配置的资源包来定义和国际化。rest-messages
模板标题
要定义模板标题,请使用以下模式:。请注意,在 HAL-FORMS 中,模板的名称是如果它是唯一的模板。 这意味着,通常必须使用提示所描述的本地或完全限定的输入类型名称来限定密钥。_templates.$affordanceName.titledefault
例 34。定义 HAL-FORMS 模板标题
_templates.default.title=Some title _templates.putEmployee.title=Create employee Employee._templates.default.title=Create employee com.acme.Employee._templates.default.title=Create employee使用 as 键的标题的全局定义。default
使用实际提示名称作为键的游戏的全局定义。除非在创建提示时显式定义,否则默认为创建提示时指向的方法的名称。
要应用于名为 的所有类型的本地定义的标题。Employee
使用完全限定类型名称的标题定义。
使用实际提示名称的密钥优先于默认密钥。
属性提示
属性提示也可以通过Spring HATEOS自动配置的资源包来解决。 这些键可以全局定义、本地定义或完全限定,并且需要连接到实际属性键:rest-messages._prompt
例 35。定义属性提示email
firstName._prompt=Firstname Employee.firstName._prompt=Firstname com.acme.Employee.firstName._prompt=Firstname所有命名的属性都将呈现“名字”,与声明它们的类型无关。firstName
命名类型中的属性将提示为“名字”。firstNameEmployee
的属性将收到分配“名字”的提示。firstNamecom.acme.Employee
4.2.3. 一个完整的例子
让我们看一些结合了上述所有定义和自定义属性的示例代码。 A 表示客户可能如下所示:RepresentationModel
class CustomerRepresentation extends RepresentationModel<CustomerRepresentation> { String name; LocalDate birthdate; @Pattern(regex = "[0-9]{16}") String ccn; @Email String email; }我们定义一个类型的属性。birthdateLocalDate
我们希望遵守正则表达式。ccn
我们使用 JSR-303 注释定义为电子邮件。email@Email
请注意,此类型不是域类型。 它旨在捕获各种可能无效的输入,以便可以立即拒绝字段的潜在错误值。
让我们继续看一下控制器如何使用该模型:
@Controllerclass CustomerController { @PostMapping("/customers") EntityModel<?> createCustomer(@RequestBody CustomerRepresentation payload) { // … } @GetMapping("/customers") CollectionModel<?> getCustomers() { CollectionModel<?> model = …; CustomerController controller = methodOn(CustomerController.class); model.add(linkTo(controller.getCustomers()).withSelfRel() .andAfford(controller.createCustomer(null))); return ResponseEntity.ok(model); }}控制器方法声明为使用上面定义的表示模型将请求正文绑定到 (如果 颁发给 )。POST/customers
准备模型的请求,添加指向该模型的链接,并在指向映射到的控制器方法的链接上声明提示。 这将导致构建一个提示模型,该模型(取决于最终要呈现的媒体类型)将转换为特定于媒体类型的格式。GET/customersselfPOST
接下来,让我们添加一些额外的元数据,使表单更易于人类访问:
中声明的其他属性。rest-messages.properties
CustomerRepresentation._template.createCustomer.title=Create customer CustomerRepresentation.ccn._prompt=Credit card number CustomerRepresentation.ccn._placeholder=1234123412341234我们为通过指向方法创建的模板定义显式标题。createCustomer(…)
我们显式地为模型的属性提供提示和占位符。ccnCustomerRepresentation
如果客户端现在使用 标头发出请求,则响应 HAL 文档将扩展到 HAL-FORMS 文档,以包含以下定义:GET/customersAcceptapplication/prs.hal-forms+json_templates
{ …, "_templates" : { "default" : { "title" : "Create customer", "method" : "post", "properties" : [ { "name" : "name", "required" : true, "type" : "text" } , { "name" : "birthdate", "required" : true, "type" : "date" } , { "name" : "ccn", "prompt" : "Credit card number", "placeholder" : "1234123412341234" "required" : true, "regex" : "[0-9]{16}", "type" : "text" } , { "name" : "email", "prompt" : "Email", "required" : true, "type" : "email" } ] } }}将公开名为的模板。它的名称是因为它是唯一定义的模板,规范要求使用该名称。 如果附加了多个模板(通过声明其他提示),则每个模板都将以它们指向的方法命名。defaultdefault
模板标题派生自资源包中定义的值。请注意,根据随请求发送的标头和可用性,可能会返回不同的值。Accept-Language
属性的值派生自提供从中派生的方法的映射。method
属性的值派生自属性的类型。 这同样适用于属性,但会导致 .typetextStringbirthdatedate
属性的提示符和占位符也派生自资源包。ccn
属性的声明作为模板属性的属性公开。@Patternccnregex
属性上的注释已转换为相应的值。@Emailemailtype
例如,HAL 资源管理器会考虑 HAL-FORMS 模板,它会自动从这些描述中呈现 HTML 表单。