SPRING实战(3)、超媒体与Spring HATEOAS之一

翟丰茂
2023-12-01

超媒体作为应用状态引擎(Hypermedia as the Engine of Application State,HATEOAS)是一种创建自描述API的方式。API所返回的资源中会包含相关资源的链接,客户端只需要了解最少的API URL信息就能导航整个API。如果API启用了超媒体功能,那么API将会描述自己的URL,从而减轻客户端对其进行硬编码的痛苦。这种特殊风格的HATEOAS被称为HAL(超文本应用语言,Hypertext Application Language),这是一种在JSON响应中嵌入超链接的简单通用格式。

Spring HATEOAS项目为Spring提供了超链接的支持。它提供了一些类和资源装配器(assembler),在Spring MVC控制器返回资源之前能够为其添加连接。

SpringBoot配置:Spring HATEOAS依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
<dependency>

Spring HATEOAS官方文档开头是这样描述该项目的:Spring HATEOAS提供了一些API,以简化在使用Spring特别是Spring MVC时遵循HATEOAS原理的REST表示形式的过程。 它试图解决的核心问题是链接创建和表示组装。

This project provides some APIs to ease creating REST representations that follow the HATEOAS principle when working with Spring and especially Spring MVC. The core problem it tries to address is link creation and representation assembly.

Spring HATEOAS提供了两个主要的类型来表示超链接资源:Resource和Resources。Resource代表一个资源,而Resources代表资源的集合。当从Spring MVC REST控制器返回时,它们所携带的链接将会包含到客户端所接收到的JSON(或XML)中。

在未引入Spring HATEOAS时,调用服务返回的往往是对象(Object )或对象数组List<Object>。引入该依赖后,为返回结果添加超链接需要返回一个RepresentationModel或EntityModel或CollectionModel或PagedModel。

这里需要注意的是,在1.0版本后,模型发生了一个很大的变化。在之前使用的模型名称是:ResourceSupport、Resource、Resources、PagedResources 。从1.0开始对应改变:

  • ResourceSupport is now RepresentationModel

  • Resource is now EntityModel

  • Resources is now CollectionModel

  • PagedResources is now PagedModel

官方更名给出的解释是:1.0之前的这个组命名(ResourceSupport / Resource / Resources / PagedResources),称之为资源,但是命名不准确,因为,这些类型实际上并不表示资源,而是表示模型,仅仅是可以通过超媒体信息和提供的内容加以丰富的模型。

Spring HATEOAS以链接构建者(link builder)的方式为我们提供了帮助。在Spring HATEOAS中,最有用的链接构建者是WebMvcLinkBuilder或WebFluxLinkBuilder。这个链接构建者非常智能,它能自动探知主机名是什么,这样就能避免对其进行硬编码。同时,它还提供了流畅的API,允许我们相对于控制器的基础URL构建连接。

URI模板

  • 使用带有模板化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模板
UriTemplate template = UriTemplate.of("/{segment}/something") .with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM); assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");

为了指示目标资源与当前的关系,使用了一个所谓的链接关系。Spring HATEOAS提供了一个链接关系类型( LinkRelation),可以轻松地创建基于字符串的实例。Internet指定数字权限包含一组预定义的链接关系。它们可以通过IanaLinkRelations被引用。

为了轻松创建丰富的超媒体表示形式,Spring HATEOAS提供了一组RepresentationModel以其根为基础的类。 RepresentationModel类层次结构:

lass RepresentationModel
class EntityModel
class CollectionModel
class PagedModel

EntityModel -|> RepresentationModel
CollectionModel -|> RepresentationModel
PagedModel -|> CollectionModel

通常不建议继承RepresentationModel,直接使用Spring HATEOAS提供的实现类更加方便;而且多数情况下可以满足我们的需求。对于由单个对象或概念支持的资源,EntityModel;集合的资源,可以使用CollectionModel。

class PersonModel extends RepresentationModel<PersonModel> { 
String firstname, lastname;
 }

PersonModel model = new PersonModel(); 
model.firstname = "Dave"; 
model.lastname = "Matthews"; 
model.add(Link.of("https://myhost/people/42"));

返回结构:

{
  "_links" : {
    "self" : {
      "href" : "https://myhost/people/42"
    }
  },
  "firstname" : "Dave",
  "lastname" : "Matthews"

可以直接使用EntityModel,用法如下:

Person person = new Person("Dave", "Matthews"); 
EntityModel<Person> model = EntityModel.of(person);

同样集合可以直接使用CollectionModel包装现有对象的集合:

Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews"));
CollectionModel<Person> model = CollectionModel.of(people);

 

Spring HATEOAS现在提供了一个WebMvcLinkBuilder,可以解决Spring MVC通过@GetMapping设置URI的一些固有弊端。

遵循Spring MVC通过@GetMapping设置将导致两个问题:

  • 要创建绝对URI,您需要查找协议,主机名,端口,servlet基和其他值。这很麻烦,并且需要难看的手动字符串连接代码。
  • 在基本URI的顶部进行串联,将不得不在多个位置维护信息。如果更改映射,则必须更改所有指向该映射的客户端。

调用实例:

@GetMapping("/employees")
ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {

   List<EntityModel<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
         .map(employee -> new EntityModel<>(employee, 
               linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), 
               linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) 
         .collect(Collectors.toList());

   return ResponseEntity.ok( //
         new CollectionModel<>(employees, //
               linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
}
@GetMapping("/employees/{id}")
ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {

   return repository.findById(id) 
         .map(employee -> new EntityModel<>(employee, 
               linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), 
               linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) 
         .map(ResponseEntity::ok) 
         .orElse(ResponseEntity.notFound().build());
}
{
    "_embedded":{
        "employees":[
            {
                "id":1,
                "firstName":"Frodo",
                "lastName":"Baggins",
                "role":"ring bearer",
                "_links":{
                    "self":{
                        "href":"
http://localhost/employees/1"
                    },
                    "employees":{
                        "href":"
http://localhost/employees"
                    }
                }

            },
            {
                "id":2,
                "firstName":"Bilbo",
                "lastName":"Baggins",
                "role":"burglar",
                "_links":{
                    "self":{
                        "href":"
http://localhost/employees/2"
                    },
                    "employees":{
                        "href":"
http://localhost/employees"
                    }
                }

            }
        ]
    },
    "_links":{
        "self":{
            "href":"
http://localhost/employees"
        }
    }

}

说明:

  • linkTo:可以通过methodOn创建指向控制器方法的指针。提交一个虚拟的方法调用结果。
  • withRel():入参为String或者LinkRelation实例LinkRelation用于定义链接关系的接口。可用于实现基于spec的链接关系以及自定义链接关系。
  • withSelfRel():使用默认的self Link关系创建当前构建器实例构建的{@link Link}。
  • methodOn():创建控制器类的代理,该代理类记录方法调用,并将其公开在为方法的返回类型创建的代理中。这使得我们想要获得映射的方法能够流畅表达。但是,使用此技术可以获得的方法受到一些限制:
    • 返回类型必须能够代理,因为我们需要公开对其的方法调用。

    • 传递给方法的参数通常被忽略(通过引用的参数除外@PathVariable,因为它们组成了URI)。

    可以通过Affordance为相同的uri关联到selref上、用例如下:

    @GetMapping("/employees/{id}")
    ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
    
       return repository.findById(id)
             .map(employee -> new EntityModel<>(employee,
                   linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
                         .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
                         .andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
                   linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
             .map(ResponseEntity::ok) //
             .orElse(ResponseEntity.notFound().build());
    }

    其中:

    @PutMapping("/employees/{id}") public ResponseEntity<?> updateEmployee( // @RequestBody EntityModel<Employee> employee, @PathVariable Integer id){}

    @DeleteMapping("/employees/{id}")
    ResponseEntity<?> deleteEmployee(@PathVariable long id) {}

    通常情况,注册连接是主要的方式,但是也有可能需要手工创建连接,此时使用AffordancesAPI手动注册能力可以满足需求。

     

     

 类似资料: