3. 服务器端支持
3.1. 在 Spring MVC 中构建链接
现在我们已经有了域词汇表,但主要挑战仍然存在:如何创建实际的 URI,以一种不那么脆弱的方式包装到实例中。现在,我们不得不到处复制 URI 字符串。这样做很脆弱且无法维护。Link
假设你有如下实现 Spring MVC 控制器:
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll() { … }
@GetMapping(value = "/{person}", method = RequestMethod.GET)
HttpEntity<PersonModel> show(@PathVariable Long person) { … }
}
我们在这里看到两个约定。第一个是通过 controller 方法的注释公开的集合资源,该集合的各个元素作为直接 sub 资源公开。集合资源可能在简单的 URI(如刚才所示)或更复杂的 URI (如) 中公开。假设您要链接到所有人的集合资源。遵循上述方法将导致两个问题:@GetMapping
/people/{id}/addresses
-
要创建绝对 URI,您需要查找协议、主机名、端口、Servlet 基和其他值。这很麻烦,并且需要丑陋的手动字符串连接代码。
-
您可能不想在基本 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。构建器现在检查给定控制器类的根映射,因此最终得到 .你也可以构建更多的嵌套链接。
以下示例显示了如何执行此操作:WebMvcLinkBuilder
ServletUriComponentsBuilder
localhost:8080/your-app
localhost:8080/your-app/people
Person person = new Person(1L, "Dave", "Matthews");
// /person / 1
Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel();
assertThat(link.getRel(), is(IanaLinkRelation.SELF.value()));
assertThat(link.getHref(), endsWith("/people/1"));
构建器还允许创建要构建的 URI 实例(例如,响应标头值):
HttpHeaders headers = new HttpHeaders();
headers.setLocation(linkTo(PersonController.class).slash(person).toUri());
return new ResponseEntity<PersonModel>(headers, HttpStatus.CREATED);
3.1.1. 构建指向方法的链接
您甚至可以构建指向方法的链接或创建虚拟控制器方法调用。
第一种方法是将实例交给 .
以下示例显示了如何执行此操作:Method
WebMvcLinkBuilder
Method method = PersonController.class.getMethod("show", Long.class);
Link link = linkTo(method, 2L).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2"));
这仍然有点令人不满意,因为我们必须先获取一个实例,这会引发异常并且通常非常麻烦。至少我们没有重复映射。更好的方法是在控制器代理上对目标方法进行虚拟方法调用,我们可以使用帮助程序创建该代理。
以下示例显示了如何执行此操作:Method
methodOn(…)
Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2");
methodOn(…)
创建 Controller 类的代理,该代理记录方法调用,并在为方法的返回类型创建的代理中公开它。这允许我们想要获取映射的方法的 Fluent 表达式。但是,使用此技术可以获得的方法存在一些限制:
-
返回类型必须能够代理,因为我们需要公开其上的方法调用。
-
传递给方法的参数通常被忽略(通过 . 引用的参数除外,因为它们构成了 URI)。
@PathVariable
控制请求参数的呈现
集合值请求参数实际上可以通过两种不同的方式实现。
URI 模板规范列出了呈现它们的复合方式,该方式重复每个值的参数名称 () ,以及用逗号 () 分隔值的非复合方式。
Spring MVC 正确解析了这两种格式的集合。
默认情况下,呈现值默认为复合样式。
如果希望以非复合样式呈现值,可以将注释与 request 参数 handler method 参数一起使用:param=value1¶m=value2
param=value1,value2
@NonComposite
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll(
@NonComposite @RequestParam Collection<String> names) { … } (1)
}
var values = List.of("Matthews", "Beauford");
var link = linkTo(methodOn(PersonController.class).showAll(values)).withSelfRel(); (2)
assertThat(link.getHref()).endsWith("/people?names=Matthews,Beauford"); (3)
1 | 我们使用 annotation 来声明我们希望以逗号分隔的方式呈现值。@NonComposite |
2 | 我们使用值列表调用该方法。 |
3 | 查看 request 参数如何以预期格式呈现。 |
我们公开的原因是,渲染请求参数的复合方式已经融入到 Spring 构建器的内部结构中,我们只在 Spring HATEOAS 1.4 中引入了这种非复合样式。
如果我们今天从头开始,我们可能会默认使用该样式,宁愿让用户显式选择使用复合样式,而不是相反。@NonComposite UriComponents |
3.3. 功能
环境的可供性就是它所提供的......它提供或提供什么,无论是好的还是坏的。动词“to afford”可以在字典中找到,但名词“affordance”却没有。我编的。
) 视觉感知的生态学方法(第 126 页)
基于 REST 的资源不仅提供数据,还提供控件。 形成灵活服务的最后一个要素是有关如何使用各种控件的详细功能。 因为功能与链接相关联,所以 Spring HATEOAS 提供了一个 API,可以根据需要将任意数量的相关方法附加到链接。 就像你可以通过指向 Spring MVC 控制器方法来创建链接一样(有关详细信息,请参阅在 Spring MVC 中构建链接),你......
以下代码显示了如何采用 self link 并关联另外两个功能:
GET /employees/{id}
@GetMapping("/employees/{id}")
public EntityModel<Employee> findOne(@PathVariable Integer id) {
Class<EmployeeController> controllerClass = EmployeeController.class;
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel(); (1)
// Return the affordance + a link back to the entire collection resource.
return EntityModel.of(EMPLOYEES.get(id), //
findOneLink //
.andAffordance(afford(methodOn(controllerClass).updateEmployee(null, id))) (2)
.andAffordance(afford(methodOn(controllerClass).partiallyUpdateEmployee(null, id)))); (3)
}
1 | 创建 self link。 |
2 | 将方法与链接关联。updateEmployee self |
3 | 将方法与链接关联。partiallyUpdateEmployee self |
使用 ,您可以使用控制器的方法将 和 操作连接到操作。
想象一下上面提供的相关方法如下所示:.andAffordance(afford(…))
PUT
PATCH
GET
updateEmpoyee
PUT /employees/{id}
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee( //
@RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
partiallyUpdateEmployee
PATCH /employees/{id}
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee( //
@RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
使用这些方法指向这些方法将导致 Spring HATEOAS 分析请求正文和响应类型并捕获元数据,以允许不同的媒体类型实现使用该信息将其转换为输入和输出的描述。afford(…)
3.3.1. 手动构建功能
虽然这是为链接注册功能的主要方法,但可能需要手动构建其中一些功能。
这可以通过使用 API 来实现:Affordances
Affordances
var methodInvocation = methodOn(EmployeeController.class).all();
var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) (1)
.afford(HttpMethod.POST) (2)
.withInputAndOutput(Employee.class) //
.withName("createEmployee") //
.andAfford(HttpMethod.GET) (3)
.withOutput(Employee.class) //
.addParameters(//
QueryParameter.optional("name"), //
QueryParameter.optional("role")) //
.withName("search") //
.toLink();
1 | 首先,从实例创建一个实例 ,创建用于描述视觉功能的上下文。Affordances Link |
2 | 每个功能都从它应该支持的 HTTP 方法开始。然后,我们将类型注册为 payload description,并显式命名提供。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这实际上会创建与指向 created 的指针相同的功能。EmployeeController.newEmployee(…) |
3 | 下一个提示旨在反映指向 的指针所发生的情况。在这里,我们定义为创建的响应的模型,并显式注册 s。EmployeeController.search(…) Employee QueryParameter |
视觉提示由特定于媒体类型的视觉元素模型提供支持,这些模型将一般视觉元素元数据转换为特定的表示形式。 请务必查看 Media types 部分中有关功能的选项部分,以了解有关如何控制该元数据的公开的更多详细信息。
3.4. 转发标头处理
RFC-7239 转发标头最常用于应用程序位于代理后面、负载均衡器后面或云中。 实际接收 Web 请求的节点是基础设施的一部分,并将请求转发到您的应用程序。
您的应用程序可能正在 上运行,但对于外部世界来说,您应该处于 (并且位于 Web 的标准端口 80 上)。
通过让代理包含额外的 Headers(许多人已经这样做了),Spring HATEOAS 可以正确生成链接,因为它使用 Spring Framework 功能来获取原始请求的基本 URI。localhost:8080
reallycoolsite.com
必须妥善保护任何可以基于外部输入更改根 URI 的内容。 因此,默认情况下,转发标头处理处于禁用状态。 您必须启用它才能运行。 如果要部署到云或控制代理和负载均衡器的配置中,那么您肯定需要使用此功能。 |
要启用转发 Headers 处理,您需要在应用程序中为 Spring MVC 注册 Spring 的 Spring 的 Mvc (详情在这里) 或 Spring WebFlux的 Spring 的 Headers 处理。
在 Spring Boot 应用程序中,这些组件可以简单地声明为 Spring bean,如此处所述。ForwardedHeaderFilter
ForwardedHeaderTransformer
ForwardedHeaderFilter
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
这将创建一个处理所有标头的 servlet 过滤器。
它将向 servlet 处理程序正确注册它。X-Forwarded-…
对于 Spring WebFlux 应用程序,反应式对应物是:ForwardedHeaderTransformer
ForwardedHeaderTransformer
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
这将创建一个函数来转换反应式 Web 请求,处理标头。
它会在 WebFlux 中正确注册它。X-Forwarded-…
如上所示的配置就位后,传递标头的请求将看到这些标头反映在生成的链接中:X-Forwarded-…
X-Forwarded-…
curl -v localhost:8080/employees \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Host: example.com' \
-H 'X-Forwarded-Port: 9001'
{
"_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 接口
EntityLinks 并且它的各种实现目前不是为 Spring WebFlux 应用程序提供的开箱即用的。
SPI 中定义的 Contract 最初是针对 Spring Web MVC,不考虑 Reactor 类型。
开发支持反应式编程的类似合约仍在进行中。EntityLinks |
到目前为止,我们已经通过指向 Web 框架实现(即 Spring MVC 控制器)创建了链接,并检查了映射。 在许多情况下,这些类本质上是读取和写入由 model 类支持的表示。
该接口现在公开了一个 API,用于根据模型类型查找 OR。
这些方法实质上返回指向集合资源(如 )或项资源(如 )的链接。
以下示例演示如何使用:EntityLinks
Link
LinkBuilder
/people
/people/1
EntityLinks
EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);
EntityLinks
可以通过在 Spring MVC 配置中激活来通过依赖项注入获得。
这将导致各种默认实现被注册。
最基本的是检查 SpringMVC 控制器类。
如果要注册自己的 实现,请查看 此部分。@EnableHypermediaSupport
EntityLinks
ControllerEntityLinks
EntityLinks
3.5.1. 基于 Spring MVC 控制器的 EntityLinks
激活实体链接功能会导致检查当前所有可用的 Spring MVC 控制器的注释。
注释公开了控制器管理的模型类型。
除此之外,我们假定您遵守以下 URI 映射设置和约定:ApplicationContext
@ExposesResourceFor(…)
-
一个类型级别,声明控制器为其公开集合和项资源的实体类型。
@ExposesResourceFor(…)
-
表示集合资源的类级别基映射。
-
一个附加的方法级别映射,用于扩展映射以附加标识符作为附加路径段。
以下示例显示了支持 -的控制器的实现:EntityLinks
@Controller
@ExposesResourceFor(Order.class) (1)
@RequestMapping("/orders") (2)
class OrderController {
@GetMapping (3)
ResponseEntity orders(…) { … }
@GetMapping("{id}") (4)
ResponseEntity order(@PathVariable("id") … ) { … }
}
1 | 控制器指示它正在公开实体的 collection 和 item 资源。Order |
2 | 它的集合资源在/orders |
3 | 该集合资源可以处理请求。在您方便时为其他 HTTP 方法添加更多方法。GET |
4 | 一个额外的控制器方法,用于处理从属资源,该方法采用 path 变量来公开 item 资源,即单个 .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 | Inject 在您的配置中提供。EntityLinks @EnableHypermediaSupport |
2 | 使用 API 通过使用实体类型而不是控制器类来构建链接。 |
如您所见,您可以引用 resources managing instances 而不显式引用。Order
OrderController
3.5.2. EntityLinks API 详解
从根本上说,允许构建 s 和 instances 来集合和监控实体类型的资源。
以 开头 的方法 将生成实例,供您使用其他路径段、参数等进行扩展和增强。
以 开头的方法生成完全准备好的实例。EntityLinks
LinkBuilder
Link
linkFor…
LinkBuilder
linkTo
Link
虽然对于集合资源,提供实体类型就足够了,但指向 item 资源的链接需要提供标识符。 这通常如下所示:
entityLinks.linkToItemResource(order, order.getId());
如果您发现自己重复了这些方法调用,则可以将标识符提取步骤提取到可重用的 API 中,以便在不同的调用中重复使用:Function
Function<Order, Object> idExtractor = Order::getId; (1)
entityLinks.linkToItemResource(order, idExtractor); (2)
1 | 标识符提取已外部化,因此可以保存在字段或 constant 中。 |
2 | 使用提取器进行链接查找。 |
类型化EntityLinks
由于控制器实现通常围绕实体类型进行分组,因此您经常会发现自己在整个控制器类中使用相同的提取器函数(有关详细信息,请参阅详细的 EntityLinks API)。
我们可以通过获取一次提供提取器的实例来更加集中标识符提取逻辑,这样实际的查找就根本不需要处理提取了。TypedEntityLinks
class OrderController {
private final TypedEntityLinks<Order> links;
OrderController(EntityLinks entityLinks) { (1)
this.links = entityLinks.forType(Order::getId); (2)
}
@GetMapping
ResponseEntity<Order> someMethod(…) {
Order order = … // lookup order
Link link = links.linkToItemResource(order); (3)
}
}
1 | 注入实例。EntityLinks |
2 | 指示您将查找具有特定标识符提取器函数的实例。Order |
3 | 根据唯一实例查找 item 资源链接。Order |
3.5.3. EntityLinks 作为 SPI
创建的实例 is 类型将依次选取 .
它被注册为 primary bean,因此在一般情况下,当您注入时,它始终是唯一的注入候选者。 是将包含在设置中的默认实现,但用户可以自由实施和注册自己的实现。
使这些可用于实例进行注入是将实现注册为 Spring bean 的问题。EntityLinks
@EnableHypermediaSupport
DelegatingEntityLinks
EntityLinks
ApplicationContext
EntityLinks
ControllerEntityLinks
EntityLinks
@Configuration
class CustomEntityLinksConfiguration {
@Bean
MyEntityLinks myEntityLinks(…) {
return new MyEntityLinks(…);
}
}
这种机制的可扩展性的一个示例是 Spring Data REST 的RepositoryEntityLinks
,它使用存储库映射信息创建指向 Spring Data 存储库支持的资源的链接。
同时,它甚至为其他类型的资源公开了额外的查找方法。
如果你想使用这些,只需显式注入。RepositoryEntityLinks
3.6. 表示模型组装器
由于必须在多个位置使用从实体到表示模型的映射,因此创建一个负责执行此操作的专用类是有意义的。转换包含非常自定义的步骤,但也包含一些样板步骤:
-
模型类的实例化
-
添加一个带有 of 的链接,该链接指向要呈现的资源。
rel
self
Spring HATEOAS 现在提供了一个基类,有助于减少您需要编写的代码量。
以下示例演示如何使用它:RepresentationModelAssemblerSupport
class PersonModelAssembler extends RepresentationModelAssemblerSupport<Person, PersonModel> {
public PersonModelAssembler() {
super(PersonController.class, PersonModel.class);
}
@Override
public PersonModel toModel(Person person) {
PersonModel resource = createResource(person);
// … do further mapping
return resource;
}
}
createResource(…) 是您编写的代码,用于在给定对象的情况下实例化对象。它应该只关注设置属性,而不是填充 。PersonModel Person Links |
像前面的例子一样设置类可以为您带来以下好处:
-
有一些方法允许您创建资源的实例,并为其添加 rel 的 a。该链接的 href 由配置的控制器的请求映射加上实体的 ID(例如,)确定。
createModelWithId(…)
Link
self
/people/1
-
资源类型由反射实例化,并需要一个 no-arg 构造函数。如果要使用专用构造函数或避免反射性能开销,可以覆盖 .
instantiateModel(…)
然后,您可以使用汇编器组装 a 或 .
以下示例创建一个 of 实例:RepresentationModel
CollectionModel
CollectionModel
PersonModel
Person person = new Person(…);
Iterable<Person> people = Collections.singletonList(person);
PersonModelAssembler assembler = new PersonModelAssembler();
PersonModel model = assembler.toModel(person);
CollectionModel<PersonModel> model = assembler.toCollectionModel(people);
3.7. 表示模型处理器
有时,您需要在超媒体表示组合完成后对其进行调整和调整。
一个完美的例子是,当您有一个处理订单履行的控制器,但您需要添加与付款相关的链接时。
想象一下,你的订购系统正在生成这种类型的超媒体:
{
"orderId" : "42",
"state" : "AWAITING_PAYMENT",
"_links" : {
"self" : {
"href" : "http://localhost/orders/999"
}
}
}
您希望添加一个链接以便客户可以付款,但不想混合有关您的详细信息
这。
与其污染你的订购系统的细节,你可以写这样一个:PaymentController
OrderController
RepresentationModelProcessor
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> { (1)
@Override
public EntityModel<Order> process(EntityModel<Order> model) {
model.add( (2)
Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) //
.expand(model.getContent().getOrderId()));
return model; (3)
}
}
1 | 此处理器将仅应用于对象。EntityModel<Order> |
2 | 通过添加无条件链接来操作现有对象。EntityModel |
3 | 返回 ,以便将其序列化为请求的媒体类型。EntityModel |
将处理器注册到您的应用程序中:
@Configuration
public class PaymentProcessingApp {
@Bean
PaymentProcessor paymentProcessor() {
return new PaymentProcessor();
}
}
现在,当您发出 an 的超媒体重新表示时,客户端会收到以下内容:Order
{
"orderId" : "42",
"state" : "AWAITING_PAYMENT",
"_links" : {
"self" : {
"href" : "http://localhost/orders/999"
},
"payments" : { (1)
"href" : "/payments/42" (2)
}
}
}
1 | 您会看到 Plugged in 作为此链接的关系。LinkRelation.of("payments") |
2 | URI 由处理器提供。 |
这个例子非常简单,但你可以很容易地:
-
使用 或 构建指向 .
WebMvcLinkBuilder
WebFluxLinkBuilder
PaymentController
-
注入有条件地添加由 state 驱动的其他链接(例如 , )所需的任何服务。
cancel
amend
-
利用 Spring Security 等横切服务,根据当前用户的上下文添加、删除或修改链接。
此外,在此示例中,会更改提供的 .您还可以将其替换为另一个对象。请注意,API 要求返回类型等于输入类型。PaymentProcessor
EntityModel<Order>
3.7.1. 处理空集合模型
为了找到要为实例调用的正确实例集,调用基础设施将对已注册的 s 的泛型声明执行详细分析。
例如,这包括检查底层集合的元素,因为在运行时,唯一的模型实例不会公开泛型信息(由于 Java 的类型擦除)。
这意味着,默认情况下,不会为空集合模型调用实例。
要仍然允许基础设施正确推断有效负载类型,您可以从一开始就使用显式回退有效负载类型初始化空实例,或者通过调用来注册它。
有关详细信息,请参阅 集合资源表示模型 。RepresentationModelProcessor
RepresentationModel
RepresentationModelProcessor
CollectionModel
RepresentationModelProcessor
CollectionModel
CollectionModel.withFallbackType(…)
3.8. 使用 APILinkRelationProvider
在构建链接时,通常需要确定要用于链接的关系类型。在大多数情况下,关系类型与 (域) 类型直接关联。我们封装了详细的算法来查找 API 背后的关系类型,该 API 允许您确定单个资源和集合资源的关系类型。查找关系类型的算法如下:LinkRelationProvider
-
如果类型使用 进行批注,则使用批注中配置的值。
@Relation
-
如果不是,我们默认使用未大写的简单类名加上为集合附加的 。
List
rel
-
如果 EVO 变形器 JAR 在 Classpath 中,则我们使用复数化算法提供的单个资源的复数形式。
rel
-
@Controller
annotation with 的类(有关详细信息,请参阅使用 EntityLinks 接口)透明地查找 annotation 中配置的类型的关系类型,以便您可以使用和获取公开的域类型的关系类型。@ExposesResourceFor
LinkRelationProvider.getItemResourceRelFor(MyController.class)
当您使用 A 时,它会自动公开为 Spring bean。您可以通过实现接口并依次将它们公开为 Spring bean 来插入自定义提供程序。LinkRelationProvider
@EnableHypermediaSupport