3. 服务端支持
3.1. 在 Spring MVC 中构建链接
现在我们已经建立了领域词汇,但主要挑战仍然存在:如何以更稳健的方式创建实际要封装到Link实例中的 URI。目前,我们不得不在各处重复 URI 字符串。这样做既脆弱又难以维护。
假设您的 Spring MVC 控制器实现如下:
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll() { … }
@GetMapping("/{person}")
HttpEntity<PersonModel> show(@PathVariable Long person) { … }
}
我们在这里看到两种约定。第一种是通过控制器方法的 @GetMapping 注解公开的集合资源,该集合的各个元素作为直接的子资源公开。集合资源可以暴露在简单的 URI(如上所示)或更复杂的 URI(例如 /people/{id}/addresses)。假设您希望链接到所有人的集合资源。采用上述方法会导致两个问题:
-
要创建绝对 URI,您需要查找协议、主机名、端口、Servlet 基础路径以及其他值。这非常繁琐,并且需要编写笨拙的手动字符串拼接代码。
-
您可能不希望将
/people拼接到基础 URI 的顶部,因为那样您就必须在多个地方维护该信息。如果您更改了映射,那么您就必须更改所有指向它的客户端。
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 实例,这会抛出异常且通常相当繁琐。至少我们无需重复映射。更好的方法是在控制器代理上对目标方法进行一次虚拟方法调用,我们可以使用 methodOn(…) 辅助工具来创建该代理。
以下示例展示了如何实现这一点:
Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2");
methodOn(…) 会创建控制器类的代理,该代理记录方法调用,并将其暴露在为方法返回类型创建的代理中。这使得我们可以流畅地表达希望获取映射的方法。然而,使用此技术可获取的方法存在一些限制:
-
返回类型必须能够被代理,因为我们需要在其上暴露方法调用。
-
传入方法的参数通常被忽略(除了通过
@PathVariable引用的那些,因为它们构成了 URI)。
控制请求参数的渲染
集合类型的请求参数实际上可以通过两种不同的方式实例化。
URI 模板规范列出了渲染它们的复合方式,即为每个值重复参数名称(param=value1¶m=value2),以及非复合方式,即使用逗号分隔各个值(param=value1,value2)。
Spring MVC 能够正确地从这两种格式中解析出集合。
默认情况下,渲染值采用复合风格。
如果您希望以非复合风格渲染值,可以在请求参数的处理器方法参数上使用 @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 | 我们使用 @NonComposite 注解来声明我们希望值以逗号分隔的方式渲染。 |
| 2 | 我们使用值列表来调用该方法。 |
| 3 | 查看请求参数如何以预期格式呈现。 |
我们之所以暴露 @NonComposite,是因为渲染请求参数的复合方式已内置于 Spring 的 UriComponents 构建器的内部实现中,而我们直到 Spring HATEOAS 1.4 才引入了这种非复合风格。
如果今天从头开始设计,我们可能会默认采用那种非复合风格,并让用户显式选择启用复合风格,而不是反过来。 |
3.3. 功能
环境的供能性(affordances)是指它所提供的……无论是好是坏,它所给予或配备的东西。动词“to afford”可以在词典中找到,但名词
视觉感知的生态方法(第 126 页)
基于 REST 的资源不仅提供数据,还提供控制能力。 构建灵活服务的最后一个要素是关于如何使用各种控制能力的详细功能提示(affordances)。 由于功能提示与链接相关联,Spring HATEOAS 提供了一个 API,允许为链接附加任意数量的相关方法。 正如您可以通过指向 Spring MVC 控制器方法来创建链接一样(详见在 Spring MVC 中构建链接),您……
以下代码展示了如何获取一个 self 链接并关联另外两个功能:
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 链接。 |
| 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 API 来实现:
Affordances API 手动注册功能特性(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 | 首先,您需要从 Link 实例创建一个 Affordances 实例,以构建描述功能可供性(affordances)的上下文。 |
| 2 | 每个功能(affordance)都以它应支持的 HTTP 方法开始。随后,我们将一个类型注册为负载描述,并显式命名该功能。后者可以省略,此时将从 HTTP 方法和输入类型名称派生出默认名称。这实际上创建了与指向 EmployeeController.newEmployee(…) 所创建的功能相同的功能。 |
| 3 | 下一个功能旨在反映指针指向 EmployeeController.search(…) 时发生的情况。在此,我们将 Employee 定义为所创建响应的模型,并显式注册 QueryParameter。 |
功能由特定于媒体类型的功能模型支持,这些模型将通用功能元数据转换为特定的表示形式。 请务必查看 媒体类型 部分中关于功能的内容,以获取更多有关如何控制该元数据暴露的详细信息。
3.4. 转发请求头处理
RFC-7239 转发头最常用于您的应用位于代理、负载均衡器之后或部署在云环境中时。 实际接收 Web 请求的节点属于基础设施部分,并将请求转发给您的应用程序。
您的应用程序可能运行在 localhost:8080 上,但对于外部世界而言,您应当位于 reallycoolsite.com(并且使用 Web 的标准端口 80)。
通过让代理包含额外的头部信息(许多代理已经这样做),Spring HATEOAS 可以正确生成链接,因为它利用 Spring Framework 的功能来获取原始请求的基础 URI。
| 任何可能基于外部输入更改根 URI 的内容都必须得到妥善保护。 因此,默认情况下,转发标头处理功能是禁用的。 您必须启用它才能使其正常运行。 如果您要部署到云端或部署到您能控制代理和负载均衡器的配置环境中,那么您肯定希望使用此功能。 |
要启用转发标头处理,您需要在应用程序中为 Spring MVC 注册 Spring 的 ForwardedHeaderFilter(详情见 此处),或为 Spring WebFlux 注册 ForwardedHeaderTransformer(详情见 此处)。
在 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-… 个标头的请求将在生成的链接中看到这些标头被反映出来:
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 控制器)创建了链接,并检查了映射关系。 在许多情况下,这些类本质上会读取和写入由模型类支持的表示形式。
EntityLinks 接口现在提供了一个 API,用于根据模型类型查找 Link 或 LinkBuilder。
这些方法本质上返回指向集合资源(例如 /people)或单项资源(例如 /people/1)的链接。
以下示例展示了如何使用 EntityLinks:
EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);
EntityLinks 可通过在您的 Spring MVC 配置中激活 @EnableHypermediaSupport,经由依赖注入获得。
这将导致注册多种 EntityLinks 的默认实现。
其中最基础的是 ControllerEntityLinks,它会检查 SpringMVC 控制器类。
如果您想注册自己的 EntityLinks 实现,请参阅 本节。
3.5.1. 基于 Spring MVC 控制器的 EntityLinks
启用实体链接功能会导致检查当前 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 和 Link 实例,以集合和项资源的形式表示实体类型。
以 linkFor… 开头的方法将生成 LinkBuilder 实例,供您扩展并添加额外的路径段、参数等。
以 linkTo 开头的方法会生成完全准备好的 Link 实例。
虽然对于集合资源而言,提供实体类型已足够,但指向单项资源的链接则需要提供一个标识符。 这通常看起来像这样:
entityLinks.linkToItemResource(order, order.getId());
如果您发现自己重复调用这些方法,可以将标识符提取步骤提取到一个可复用的Function中,以便在不同的调用中重复使用:
Function<Order, Object> idExtractor = Order::getId; (1)
entityLinks.linkToItemResource(order, idExtractor); (2)
| 1 | 标识符提取已被外部化,因此可以将其保存在字段或常量中。 |
| 2 | 使用提取器进行链接查找。 |
TypedEntityLinks
由于控制器实现通常围绕实体类型进行分组,您经常会发现在整个控制器类中重复使用相同的提取器函数(详见 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 | 根据唯一的 Order 实例查找项资源链接。 |
3.5.3. EntityLinks 作为 SPI
由@EnableHypermediaSupport创建的EntityLinks实例类型为DelegatingEntityLinks,它将依次选取ApplicationContext中作为 Bean 可用的所有其他EntityLinks实现。
它被注册为主 Bean,因此当您在一般情况下注入EntityLinks时,它始终是唯一的注入候选者。
ControllerEntityLinks是设置中包含的默认实现,但用户可以自由实现并注册自己的实现。
要使这些实现对EntityLinks实例可用于注入,只需将您的实现注册为 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(…) 是您编写的代码,用于根据给定的 Person 对象实例化一个 PersonModel 对象。它应仅专注于设置属性,而不应填充 Links。 |
按照前一个示例中的方式设置类,可以为您带来以下好处:
-
有少数几个
createModelWithId(…)方法,可让您创建资源实例,并为其添加一个 rel 为self的Link。该链接的 href 由配置的控制器请求映射加上实体的 ID 确定(例如:/people/1)。 -
资源类型通过反射实例化,并需要一个无参构造函数。如果您想使用专用的构造函数或避免反射带来的性能开销,可以重写
instantiateModel(…)。
然后您可以使用汇编器来汇编一个 RepresentationModel 或一个 CollectionModel。
以下示例创建了 PersonModel 个实例的 CollectionModel:
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();
}
}
现在,当您发布一个 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<Order>。您也有能力将其替换为另一个对象。但请注意,API 要求返回类型必须与输入类型一致。
3.7.1. 处理空集合模型
为了找到调用 RepresentationModel 实例时应使用的正确 RepresentationModelProcessor 实例集,调用基础设施会对已注册的 RepresentationModelProcessor 的泛型声明进行详细分析。
对于 CollectionModel 实例,这包括检查底层集合的元素,因为在运行时,唯一的模型实例不会暴露泛型信息(由于 Java 的类型擦除)。
这意味着,默认情况下,不会为空的集合模型调用 RepresentationModelProcessor 实例。
为了使基础设施仍能正确推断负载类型,您可以从一开始就用显式的回退负载类型初始化空的 CollectionModel 实例,或者通过调用 CollectionModel.withFallbackType(…) 来注册它。
有关详细信息,请参阅 集合资源表示模型。
3.8. 使用LinkRelationProviderAPI
在构建链接时,通常需要确定用于该链接的关系类型。在大多数情况下,关系类型直接与(领域)类型相关联。我们将查找关系类型的详细算法封装在一个 LinkRelationProvider API 背后,该 API 允许您确定单个资源和集合资源的关系类型。查找关系类型的算法如下:
-
如果类型使用了
@Relation进行注解,我们将使用注解中配置的值。 -
否则,我们默认使用未首字母大写的简单类名,并为集合
rel附加一个List。 -
如果 EVO inflector JAR 位于类路径中,我们将使用由复数算法提供的单个资源
rel的复数形式。 -
@Controller个使用@ExposesResourceFor注解的类(详见 使用 EntityLinks 接口)会透明地查找该注解中配置类型对应的关系类型,以便您可以使用LinkRelationProvider.getItemResourceRelFor(MyController.class)获取所暴露领域类型的关系类型。
当您使用@EnableHypermediaSupport时,LinkRelationProvider会自动暴露为 Spring Bean。您可以通过实现该接口并将其依次暴露为 Spring Bean 来插入自定义提供者。