SPRING實戰(3)、超媒體與Spring HATEOAS之一

超媒體作爲應用狀態引擎(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手動註冊能力可以滿足需求。

     

     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章