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
Comments 的 Cookie,该集合的各个元素公开为 Direct Sub 资源。集合资源可能以简单的 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
instance 添加到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
使用 request 参数处理程序方法参数进行注释:
@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 builder 中,我们只在 Spring HATEOAS 1.4 中引入了这种非复合样式。
如果我们今天从头开始,我们可能会默认使用该样式,而是让用户明确选择使用复合样式,而不是相反。 |
3.3. 可供性
环境的可供性是它所提供的......它提供或提供的东西,无论是好是坏。动词“to afford”在字典中找到,但名词“affordance”没有。我编造的。
,视觉感知的生态方法(第 126 页)
基于 REST 的资源不仅提供数据,还提供控制。 形成灵活服务的最后一个要素是有关如何使用各种控件的详细可供性。 由于可供性与链接相关联,因此 Spring HATEOAS 提供了一个 API,可以根据需要将任意数量的相关方法附加到链接。 正如您可以通过指向 Spring MVC 控制器方法来创建链接一样(有关详细信息,请参阅在 Spring MVC 中构建链接),您...
以下代码演示了如何获取自链接并关联另外两个可供性:
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 | 创建自链接。 |
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 方法开始。然后,我们将类型注册为有效负载描述,并显式命名可供性。后者可以省略,默认名称将从 HTTP 方法和输入类型名称派生。这有效地创建了与指向EmployeeController.newEmployee(…) 创建。 |
3 | 构建下一个可供性以反映指向的指针正在发生的情况EmployeeController.search(…) .在这里,我们定义Employee 作为创建并显式注册的响应的模型QueryParameter s. |
可供性由媒体类型特定可供性模型支持,这些模型将一般可供性元数据转换为特定表示形式。 请务必查看媒体类型部分中有关可供性的部分,以查找有关如何控制该元数据的曝光的更多详细信息。
3.4. 转发标头处理
RFC-7239 转发标头最常用于应用程序位于代理后面、负载均衡器后面或云中。 实际接收 Web 请求的节点是基础结构的一部分,并将请求转发到应用程序。
您的应用程序可能在localhost:8080
,但对于外界来说,你应该处于reallycoolsite.com
(以及网络的标准端口 80)。
通过让代理包含额外的标头(许多人已经这样做了),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-…
标头将看到那些反映在生成的链接中的链接:
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
通过激活依赖注入@EnableHypermediaSupport
在您的 Spring MVC 配置中。
这将导致各种默认实现EntityLinks
正在注册。
最根本的一条是ControllerEntityLinks
检查 SpringMVC 控制器类。
如果您想注册自己的实现EntityLinks
,请查看此部分。
3.5.1. 基于 Spring MVC 控制器的 EntityLinks
激活实体链接功能会导致当前ApplicationContext
要检查@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 . |
有了这个,当您启用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
实例到实体类型的集合和项目资源。
以 开头的方法linkFor…
将产生LinkBuilder
实例,以便通过其他路径段、参数等进行扩展和扩充。
以 开头的方法linkTo
生产充分准备Link
实例。
虽然对于集合资源,提供实体类型就足够了,但指向项目资源的链接需要提供标识符。 这通常如下所示:
entityLinks.linkToItemResource(order, order.getId());
如果您发现自己重复这些方法调用,则可以将标识符提取步骤提取到可重用的Function
在不同的调用中重复使用:
Function<Order, Object> idExtractor = Order::getId; (1)
entityLinks.linkToItemResource(order, idExtractor); (2)
1 | 标识符提取被外部化,以便它可以保存在字段或常量中。 |
2 | 使用提取器的链接查找。 |
类型实体链接
由于控制器实现通常围绕实体类型进行分组,因此您经常会发现自己在整个控制器类中使用相同的提取器函数(有关详细信息,请参阅 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. 作为 SPI 的 EntityLinks
这EntityLinks
实例创建者@EnableHypermediaSupport
是类型DelegatingEntityLinks
反过来又会接取所有其他EntityLinks
在ApplicationContext
.
它被注册为主 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(…) 是您编写的用于实例化PersonModel 对象给定一个Person 对象。 它应该只关注设置属性,而不是填充Links . |
像前面的示例中一样设置类可带来以下好处:
-
有少数
createModelWithId(…)
方法,允许您创建资源的实例并具有Link
与self
添加到其中。该链接的 href 由配置的控制器的请求映射加上实体的 ID(例如,/people/1
). -
资源类型通过反射实例化,并期望无参数构造函数。如果要使用专用构造函数或避免反射性能开销,可以重写
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 对象,通过添加无条件链接。 |
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. 处理空集合模型
要找到合适的RepresentationModelProcessor
实例调用RepresentationModel
实例,调用基础结构对RepresentationModelProcessor
s 注册。
为CollectionModel
instances,这包括检查底层集合的元素,因为在运行时,唯一的模型实例不会公开泛型信息(由于 Java 的类型擦除)。
这意味着,默认情况下,RepresentationModelProcessor
不会为空集合模型调用实例。
若要仍允许基础结构正确推断有效负载类型,可以初始化空CollectionModel
实例从一开始就具有显式回退有效负载类型,或通过调用CollectionModel.withFallbackType(…)
.
有关详细信息,请参阅集合资源表示模型。
3.8. 使用LinkRelationProvider
应用程序接口
构建链接时,通常需要确定要用于链接的关系类型。在大多数情况下,关系类型与(域)类型直接关联。我们封装了详细的算法,以查找LinkRelationProvider
API 用于确定单个资源和集合资源的关系类型。查找关系类型的算法如下:
-
如果类型用
@Relation
,我们使用注释中配置的值。 -
如果没有,我们默认使用不大写的简单类名加上附加的
List
用于收藏rel
. -
如果 EVO 变化器 JAR 在类路径中,我们使用单个资源的复数形式
rel
由复数算法提供。 -
@Controller
类的注释@ExposesResourceFor
(有关详细信息,请参阅使用 EntityLinks 接口)透明地查找注释中配置的类型的关系类型,以便您可以使用LinkRelationProvider.getItemResourceRelFor(MyController.class)
并获取公开的域类型的关系类型。
一个LinkRelationProvider
在使用@EnableHypermediaSupport
.您可以通过实现接口并依次将它们公开为 Spring Bean 来插入自定义提供程序。