3. 服务器端支持
3.1. 在 Spring MVC 中构建链接
现在我们已经有了域词汇表,但主要挑战仍然存在:如何创建要包装的实际 URILink
实例。现在,我们不得不到处复制 URI 字符串。这样做很脆弱且无法维护。
假设你有如下实现 Spring MVC 控制器:
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll() { … }
@GetMapping("/{person}")
HttpEntity<PersonModel> show(@PathVariable Long person) { … }
}
我们在这里看到两个约定。第一个是通过@GetMapping
Controller方法的注释,其中该集合的各个元素作为直接子资源公开。集合资源可能在简单的 URI(如刚才所示)或更复杂的 URI(如/people/{id}/addresses
).假设您要链接到所有人的集合资源。遵循上述方法将导致两个问题:
-
要创建绝对 URI,您需要查找协议、主机名、端口、Servlet 基和其他值。这很麻烦,并且需要丑陋的手动字符串连接代码。
-
您可能不想连接
/people
,因为您必须在多个位置维护信息。如果更改映射,则必须更改指向它的所有客户端。
Spring HATEOAS 现在提供了一个WebMvcLinkBuilder
这允许你通过指向控制器类来创建链接。
以下示例显示了如何执行此作:
Link link = linkTo(PersonController.class).withRel("people");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("people"));
assertThat(link.getHref()).endsWith("/people");
这WebMvcLinkBuilder
使用 Spring 的ServletUriComponentsBuilder
从当前请求中获取基本 URI 信息。假设您的应用程序在localhost:8080/your-app
,这正是您正在构建其他部分的 URI。构建器现在检查给定的控制器类的根映射,因此最终得到localhost:8080/your-app/people
.你也可以构建更多的嵌套链接。
以下示例显示了如何执行此作:
Person person = new Person(1L, "Dave", "Matthews");
// /person / 1
Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel();
assertThat(link.getRel(), is(IanaLinkRelation.SELF.value()));
assertThat(link.getHref(), endsWith("/people/1"));
构建器还允许创建要构建的 URI 实例(例如,响应标头值):
HttpHeaders headers = new HttpHeaders();
headers.setLocation(linkTo(PersonController.class).slash(person).toUri());
return new ResponseEntity<PersonModel>(headers, HttpStatus.CREATED);
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
instance,它会引发异常,并且通常非常麻烦。至少我们没有重复映射。更好的方法是在控制器代理上对目标方法进行虚拟方法调用,我们可以使用methodOn(…)
助手。
以下示例显示了如何执行此作:
Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2");
methodOn(…)
创建 Controller 类的代理,该代理记录方法调用,并在为方法的返回类型创建的代理中公开它。这允许我们想要获取映射的方法的 Fluent 表达式。但是,使用此技术可以获得的方法存在一些限制:
-
返回类型必须能够代理,因为我们需要公开其上的方法调用。
-
传递给方法的参数通常被忽略(除了通过
@PathVariable
,因为它们构成了 URI)。
控制请求参数的呈现
集合值请求参数实际上可以通过两种不同的方式实现。
URI 模板规范列出了呈现它们的复合方式,该方式重复每个值的参数名称 (param=value1¶m=value2
) 和用逗号 (param=value1,value2
).
Spring MVC 正确解析了这两种格式的集合。
默认情况下,呈现值默认为复合样式。
如果您希望以非复合样式呈现值,可以使用@NonComposite
带有请求参数 handler method 参数的注释:
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll(
@NonComposite @RequestParam Collection<String> names) { … } (1)
}
var values = List.of("Matthews", "Beauford");
var link = linkTo(methodOn(PersonController.class).showAll(values)).withSelfRel(); (2)
assertThat(link.getHref()).endsWith("/people?names=Matthews,Beauford"); (3)
1 | 我们使用@NonComposite 注解来声明我们希望值以逗号分隔。 |
2 | 我们使用值列表调用该方法。 |
3 | 查看 request 参数如何以预期格式呈现。 |
我们曝光的原因@NonComposite 是渲染请求参数的复合方式被融入到 Spring 的UriComponents builder 中,我们只在 Spring HATEOAS 1.4 中引入了这种非复合样式。
如果我们今天从头开始,我们可能会默认使用该样式,宁愿让用户显式选择使用复合样式,而不是相反。 |
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)
使用afford(…)
方法将导致 Spring HATEOAS 分析请求正文和响应类型并捕获元数据,以允许不同的媒体类型实现使用该信息将其转换为输入和输出的描述。
3.3.1. 手动构建功能
虽然这是为链接注册功能的主要方法,但可能需要手动构建其中一些功能。
这可以通过使用Affordances
应用程序接口:
Affordances
用于手动注册提示的 APIvar 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 方法和输入类型名称派生。这有效地创建了与指向EmployeeController.newEmployee(…) 创建。 |
3 | 下一个提示旨在反映指针指向EmployeeController.search(…) .在这里,我们定义Employee 作为响应的模型并显式注册QueryParameter s. |
视觉提示由特定于媒体类型的视觉元素模型提供支持,这些模型将一般视觉元素元数据转换为特定的表示形式。 请务必查看 Media types 部分中有关功能的选项部分,以了解有关如何控制该元数据的公开的更多详细信息。
3.4. 转发标头处理
RFC-7239 转发标头最常用于应用程序位于代理后面、负载均衡器后面或云中。 实际接收 Web 请求的节点是基础设施的一部分,并将请求转发到您的应用程序。
您的应用程序可能正在 上运行localhost:8080
,但对外部世界来说,您应该处于reallycoolsite.com
(在 Web 的标准端口 80 上)。
通过让代理包含额外的 Headers(许多人已经这样做了),Spring HATEOAS 可以正确生成链接,因为它使用 Spring Framework 功能来获取原始请求的基本 URI。
必须妥善保护任何可以基于外部输入更改根 URI 的内容。 因此,默认情况下,转发标头处理处于禁用状态。 您必须启用它才能运行。 如果要部署到云或控制代理和负载均衡器的配置中,那么您肯定需要使用此功能。 |
要启用转发标头处理,您需要注册 Spring 的ForwardedHeaderFilter
对于 Spring MVC(详情请见此处)或ForwardedHeaderTransformer
对于应用程序中的 Spring WebFlux(详细信息在这里)。
在 Spring Boot 应用程序中,这些组件可以简单地声明为 Spring bean,如此处所述。
ForwardedHeaderFilter
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
这将创建一个 servlet 过滤器,用于处理所有X-Forwarded-…
头。
它将向 servlet 处理程序正确注册它。
对于 Spring WebFlux 应用程序,反应式对应项是ForwardedHeaderTransformer
:
ForwardedHeaderTransformer
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
这将创建一个函数来转换反应式 Web 请求,处理X-Forwarded-…
头。
它会在 WebFlux 中正确注册它。
有了上面所示的配置,一个传递X-Forwarded-…
headers 将看到那些反映在生成的链接中:
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 应用程序提供的开箱即用的。
在EntityLinks SPI 最初针对 Spring Web MVC,不考虑 Reactor 类型。
开发支持反应式编程的类似合约仍在进行中。 |
到目前为止,我们已经通过指向 Web 框架实现(即 Spring MVC 控制器)创建了链接,并检查了映射。 在许多情况下,这些类本质上是读取和写入由 model 类支持的表示。
这EntityLinks
interface 现在公开了一个 API 来查找Link
或LinkBuilder
基于模型类型。
这些方法实质上返回指向集合资源(例如/people
) 或项资源(例如/people/1
).
以下示例演示如何使用EntityLinks
:
EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);
EntityLinks
可通过激活@EnableHypermediaSupport
在你的 Spring MVC 配置中。
这将导致EntityLinks
正在注册。
最基本的是ControllerEntityLinks
检查 SpringMVC 控制器类。
如果您想注册自己的EntityLinks
,请查看此部分。
3.5.1. 基于 Spring MVC 控制器的 EntityLinks
激活实体链接功能会导致当前ApplicationContext
进行检查@ExposesResourceFor(…)
注解。
注释公开了控制器管理的模型类型。
除此之外,我们假定您遵守以下 URI 映射设置和约定:
-
A 类型级别
@ExposesResourceFor(…)
声明控制器为其公开 collection 和 item 资源的实体类型。 -
表示集合资源的类级别基映射。
-
一个附加的方法级别映射,用于扩展映射以附加标识符作为附加路径段。
以下示例显示了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 | 该集合资源可以处理GET 请求。在您方便时为其他 HTTP 方法添加更多方法。 |
4 | 一个额外的控制器方法,用于处理从属资源,该方法采用 path 变量来公开 item 资源,即单个Order . |
有了这个功能,当您启用EntityLinks
@EnableHypermediaSupport
在 Spring MVC 配置中,你可以创建指向控制器的链接,如下所示:
@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 | 注入EntityLinks 提供者@EnableHypermediaSupport 在您的配置中。 |
2 | 使用 API 通过使用实体类型而不是控制器类来构建链接。 |
如您所见,您可以参考 资源管理Order
实例而不引用OrderController
明确地。
3.5.2. EntityLinks API 详解
从 根本上EntityLinks
允许构建LinkBuilder
s 和Link
实例添加到实体类型的 collection 和 item 资源。
以linkFor…
将产生LinkBuilder
实例,以便您使用其他路径段、参数等进行扩展和扩充。
以linkTo
产品充分准备Link
实例。
虽然对于集合资源,提供实体类型就足够了,但指向 item 资源的链接需要提供标识符。 这通常如下所示:
entityLinks.linkToItemResource(order, order.getId());
如果您发现自己重复了这些方法调用,则可以将标识符提取步骤拉出到可重用的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 | 根据 sole 查找 item 资源链接Order 实例。 |
3.5.3. EntityLinks 作为 SPI
这EntityLinks
实例创建者@EnableHypermediaSupport
属于 类型DelegatingEntityLinks
这将反过来拾取所有其他EntityLinks
实现在ApplicationContext
.
它被注册为 primary bean,因此在你注入时它总是唯一的 injection 候选者EntityLinks
通常。ControllerEntityLinks
是将包含在设置中的默认实现,但用户可以自由实施和注册自己的实现。
使这些函数可供EntityLinks
instance available for injection 是将您的实现注册为 Spring bean 的问题。
@Configuration
class CustomEntityLinksConfiguration {
@Bean
MyEntityLinks myEntityLinks(…) {
return new MyEntityLinks(…);
}
}
此机制的可扩展性的一个示例是 Spring Data REST 的RepositoryEntityLinks
,它使用存储库映射信息创建指向 Spring Data 存储库支持的资源的链接。
同时,它甚至为其他类型的资源公开了额外的查找方法。
如果您想使用这些,只需注入RepositoryEntityLinks
明确地。
3.6. 表示模型组装器
由于必须在多个位置使用从实体到表示模型的映射,因此创建一个负责执行此作的专用类是有意义的。转换包含非常自定义的步骤,但也包含一些样板步骤:
-
模型类的实例化
-
添加带有
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 . |
像前面的例子一样设置类可以为您带来以下好处:
-
有少数
createModelWithId(…)
方法,这些方法允许您创建资源的实例,并具有Link
rel 为self
添加到它。该链接的 href 由配置的控制器的请求映射加上实体的 ID(例如/people/1
). -
资源类型由反射实例化,并需要一个 no-arg 构造函数。如果您想使用专用构造函数或避免反射性能开销,您可以覆盖
instantiateModel(…)
.
然后,您可以使用汇编器将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 object 通过添加无条件链接。 |
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") 插入为此链接的 relation。 |
2 | URI 由处理器提供。 |
这个例子非常简单,但你可以很容易地:
-
用
WebMvcLinkBuilder
或WebFluxLinkBuilder
要构建一个指向PaymentController
. -
注入有条件地添加其他链接所需的任何服务(例如
cancel
,amend
),这些 API 的 API 由 state 驱动。 -
利用 Spring Security 等横切服务,根据当前用户的上下文添加、删除或修改链接。
此外,在此示例中,PaymentProcessor
更改提供的EntityModel<Order>
.您还可以将其替换为另一个对象。请注意,API 要求返回类型等于输入类型。
3.7.1. 处理空集合模型
要找到合适的RepresentationModelProcessor
实例来调用RepresentationModel
实例中,调用基础设施会对RepresentationModelProcessor
已注册。
为CollectionModel
实例,这包括检查底层集合的元素,因为在运行时,唯一的模型实例不会公开泛型信息(由于 Java 的类型擦除)。
这意味着,默认情况下,RepresentationModelProcessor
不会为空集合模型调用实例。
要仍然允许基础设施正确推断有效负载类型,您可以初始化 emptyCollectionModel
实例,或者通过调用CollectionModel.withFallbackType(…)
.
有关详细信息,请参阅 集合资源表示模型 。
3.8. 使用LinkRelationProvider
应用程序接口
在构建链接时,通常需要确定要用于链接的关系类型。在大多数情况下,关系类型与 (域) 类型直接关联。我们封装了详细的算法来查找LinkRelationProvider
用于确定单个资源和集合资源的关系类型的 API。查找关系类型的算法如下:
-
如果类型带有
@Relation
,我们将使用注解中配置的值。 -
如果不是,我们默认使用未大写的简单类名加上附加的
List
对于系列rel
. -
如果 EVO 变形器 JAR 在类路径中,则我们使用单个资源的复数形式
rel
由复数算法提供。 -
@Controller
批注@ExposesResourceFor
(有关详细信息,请参阅使用 EntityLinks 接口)透明地查找注解中配置的类型的关系类型,以便您可以使用LinkRelationProvider.getItemResourceRelFor(MyController.class)
并获取 domain type 的 relation type。
一个LinkRelationProvider
在使用@EnableHypermediaSupport
.您可以通过实现接口并依次将它们公开为 Spring bean 来插入自定义提供程序。