本專案提供了一些 API,以便在使用 Spring,特別是 Spring MVC 時,更容易建立遵循 HATEOAS 原則的 REST 表示。它試圖解決的核心問題是連結建立和表示組裝。

© 2012-2021 原始作者。

您可以為自己使用和分發此文件的副本,前提是您不對這些副本收取任何費用,並且每份副本(無論是印刷版還是電子版)都包含此版權宣告。

1. 前言

1.1. 遷移到 Spring HATEOAS 1.0

對於 1.0 版本,我們藉此機會重新評估了 0.x 分支的一些設計和包結構選擇。我們收到了大量的反饋,而主要版本升級似乎是重構這些內容的最佳時機。

1.1.1. 變更

包結構最大的變化是由超媒體型別註冊 API 的引入驅動的,旨在支援 Spring HATEOAS 中的附加媒體型別。這導致客戶端和伺服器 API(分別命名的包)以及包 mediatype 中的媒體型別實現之間的明確分離。

將程式碼庫升級到新 API 的最簡單方法是使用遷移指令碼。在我們深入瞭解之前,這裡快速瀏覽一下這些變化。

表示模型

ResourceSupport/Resource/Resources/PagedResources 這組類的名稱從未真正感到恰當。畢竟,這些型別實際上並未體現資源,而是可以富含超媒體資訊和可供性的表示模型。以下是新名稱與舊名稱的對映方式

  • ResourceSupport 現在是 RepresentationModel

  • Resource 現在是 EntityModel

  • Resources 現在是 CollectionModel

  • PagedResources 現在是 PagedModel

因此,ResourceAssembler 已重新命名為 RepresentationModelAssembler,其方法 toResource(…)toResources(…) 已分別重新命名為 toModel(…)toCollectionModel(…)。此外,名稱更改已反映在 TypeReferences 中包含的類中。

  • RepresentationModel.getLinks() 現在公開一個 Links 例項(而不是 List<Link>),因為它公開了附加 API,可以使用各種策略連線和合並不同的 Links 例項。此外,它已被轉換為自繫結泛型型別,以允許將連結新增到例項的方法返回例項本身。

  • LinkDiscoverer API 已移至 client 包。

  • LinkBuilderEntityLinks API 已移至 server 包。

  • ControllerLinkBuilder 已移至 server.mvc 並已棄用,由 WebMvcLinkBuilder 取代。

  • RelProvider 已重新命名為 LinkRelationProvider,並返回 LinkRelation 例項而不是 String

  • VndError 已移至 mediatype.vnderror 包。

1.1.2. 遷移指令碼

您可以在您的應用程式根目錄中找到一個指令碼,該指令碼將更新我們原始碼倉庫中已移動的 Spring HATEOAS 型別的匯入語句和靜態方法引用。只需下載該指令碼,從您的專案根目錄執行它即可。預設情況下,它將檢查所有 Java 原始檔,並將舊的 Spring HATEOAS 型別引用替換為新的引用。

示例 1. 遷移指令碼的示例應用
$ ./migrate-to-1.0.sh

Migrating Spring HATEOAS references to 1.0 for files : *.java

Adapting ./src/main/java/…
…

Done!

請注意,該指令碼不一定能完全修復所有更改,但它應該涵蓋最重要的重構。

現在,在您喜歡的 Git 客戶端中驗證對檔案所做的更改,並酌情提交。如果您發現方法或型別引用未遷移,請在我們的問題跟蹤器中開啟一個工單。

1.1.3. 從 1.0 M3 遷移到 1.0 RC1

  • 接受可供性詳細資訊的 Link.andAffordance(…) 已移至 Affordances。要手動構建 Affordance 例項,現在請使用 Affordances.of(link).afford(…)。另請注意 Affordances 中公開的新 AffordanceBuilder 型別,以便於流暢使用。有關詳細資訊,請參閱可供性

  • AffordanceModelFactory.getAffordanceModel(…) 現在接收 InputPayloadMetadataPayloadMetadata 例項,而不是 ResolvableType,以允許非基於型別的實現。自定義媒體型別實現必須相應地進行調整。

  • HAL Forms 現在不再渲染屬性屬性,如果其值符合規範中定義的預設值。也就是說,如果之前 required 明確設定為 false,我們現在只是省略 required 的條目。我們現在也只對使用 PATCH 作為 HTTP 方法的模板強制將其設定為非必需。

2. 基本原理

本節涵蓋 Spring HATEOAS 及其基本領域抽象。

超媒體的基本思想是用超媒體元素豐富資源的表示。最簡單的形式是連結。它們指示客戶端可以導航到某個資源。相關資源的語義在所謂的連結關係中定義。您可能已經在 HTML 檔案的頭部看到過此內容

示例 2. HTML 文件中的連結
<link href="theme.css" rel="stylesheet" type="text/css" />

如您所見,該連結指向資源 theme.css,並指示它是一個樣式表。連結通常攜帶附加資訊,例如指向的資源將返回的媒體型別。但是,連結的基本組成部分是其引用和關係。

Spring HATEOAS 允許您透過其不可變的 Link 值型別處理連結。其建構函式接受超文字引用和連結關係,後者預設為 IANA 連結關係 self。有關後者的更多資訊,請閱讀連結關係

示例 3. 使用連結
Link link = Link.of("/something");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);

link = Link.of("/something", "my-rel");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("my-rel"));

Link 公開了 RFC-8288 中定義的其他屬性。您可以透過在 Link 例項上呼叫相應的 wither 方法來設定它們。

有關如何在 Spring MVC 和 Spring WebFlux 控制器中建立指向它們的連結的更多資訊,請參閱在 Spring MVC 中構建連結在 Spring WebFlux 中構建連結

2.2. URI 模板

對於 Spring HATEOAS Link,超文字引用不僅可以是 URI,還可以是符合 RFC-6570 的 URI 模板。URI 模板包含所謂的模板變數,並允許擴充套件這些引數。這允許客戶端將引數化模板轉換為 URI,而無需瞭解最終 URI 的結構,它只需要瞭解變數的名稱。

示例 4. 使用帶模板 URI 的連結
Link link = Link.of("/{segment}/something{?parameter}");
assertThat(link.isTemplated()).isTrue(); (1)
assertThat(link.getVariableNames()).contains("segment", "parameter"); (2)

Map<String, Object> values = new HashMap<>();
values.put("segment", "path");
values.put("parameter", 42);

assertThat(link.expand(values).getHref()) (3)
    .isEqualTo("/path/something?parameter=42");
1 Link 例項指示它已模板化,即它包含一個 URI 模板。
2 它公開了模板中包含的引數。
3 它允許擴充套件引數。

URI 模板可以手動構建,模板變數可以在以後新增。

示例 5. 使用 URI 模板
UriTemplate template = UriTemplate.of("/{segment}/something")
  .with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM);

assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");

為了指示目標資源與當前資源的關係,使用了所謂的連結關係。Spring HATEOAS 提供了一個 LinkRelation 型別,可以輕鬆建立基於 String 的例項。

網際網路號碼分配機構包含一組預定義的連結關係。它們可以透過 IanaLinkRelations 引用。

示例 6. 使用 IANA 連結關係
Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);

assertThat(link.getRel()).isEqualTo(LinkRelation.of("next"));
assertThat(IanaLinkRelation.isIanaRel(link.getRel())).isTrue();

2.4. 表示模型

為了方便地建立富含超媒體的表示,Spring HATEOAS 提供了一組以 RepresentationModel 為根的類。它本質上是 Link 集合的容器,並具有方便的方法將這些連結新增到模型中。模型以後可以渲染成各種媒體型別格式,這些格式將定義超媒體元素在表示中的外觀。有關更多資訊,請參閱媒體型別

示例 7. RepresentationModel 類層次結構
diagram classes

使用 RepresentationModel 的預設方式是建立它的子類,以包含表示應該包含的所有屬性,建立該類的例項,填充屬性並用連結豐富它。

示例 8. 示例表示模型型別
class PersonModel extends RepresentationModel<PersonModel> {

  String firstname, lastname;
}

泛型自型別化是必要的,以便讓 RepresentationModel.add(…) 返回其自身的例項。現在可以像這樣使用模型型別

示例 9. 使用人員表示模型
PersonModel model = new PersonModel();
model.firstname = "Dave";
model.lastname = "Matthews";
model.add(Link.of("https://myhost/people/42"));

如果您從 Spring MVC 或 WebFlux 控制器返回此類例項,並且客戶端傳送了設定為 application/hal+jsonAccept 頭部,則響應將如下所示

示例 10. 為人員表示模型生成的 HAL 表示
{
  "_links" : {
    "self" : {
      "href" : "https://myhost/people/42"
    }
  },
  "firstname" : "Dave",
  "lastname" : "Matthews"
}

2.4.1. 實體資源表示模型

對於由單個物件或概念支援的資源,存在一個便捷的 EntityModel 型別。您無需為每個概念建立自定義模型型別,只需重用一個已存在的型別並將其例項包裝到 EntityModel 中即可。

示例 11. 使用 EntityModel 包裝現有物件
Person person = new Person("Dave", "Matthews");
EntityModel<Person> model = EntityModel.of(person);

2.4.2. 集合資源表示模型

對於概念上是集合的資源,可以使用 CollectionModel。其元素可以是簡單物件,也可以是 RepresentationModel 例項。

示例 12. 使用 CollectionModel 包裝現有物件的集合
Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews"));
CollectionModel<Person> model = CollectionModel.of(people);

雖然 EntityModel 始終受限於包含有效負載,因此允許在單個例項上推斷型別排列,但 CollectionModel 的底層集合可能為空。由於 Java 的型別擦除,我們實際上無法檢測到 CollectionModel<Person> model = CollectionModel.empty() 實際上是一個 CollectionModel<Person>,因為我們只看到執行時例項和一個空集合。可以透過在構造時透過 CollectionModel.empty(Person.class) 將缺失的型別資訊新增到空例項,或者在底層集合可能為空的情況下將其作為備用

Iterable<Person> people = repository.findAll();
var model = CollectionModel.of(people).withFallbackType(Person.class);

3. 伺服器端支援

現在我們已經有了領域詞彙,但主要的挑戰仍然存在:如何以一種不易出錯的方式建立要包裝到 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 基礎和其他值。這很麻煩,需要難看的手動字串連線程式碼。

  • 您可能不希望在基本 URI 之上連線 /people,因為那樣您將不得不在多個地方維護資訊。如果您更改對映,那麼您必須更改所有指向它的客戶端。

Spring HATEOAS 現在提供了一個 WebMvcLinkBuilder,它允許您透過指向控制器類來建立連結。以下示例展示瞭如何實現

import static org.sfw.hateoas.server.mvc.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);

您甚至可以構建指向方法或建立虛擬控制器方法呼叫的連結。第一種方法是將 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&param=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”在詞典中可以找到,但名詞“affordance”沒有。我創造了它。

— James J. Gibson
《視覺感知的生態學方法》(第 126 頁)

基於 REST 的資源不僅提供資料,還提供控制元件。形成靈活服務的最後一個要素是關於如何使用各種控制元件的詳細可供性。因為可供性與連結相關聯,Spring HATEOAS 提供了一個 API,可以將所需數量的相關方法附加到連結。就像您可以透過指向 Spring MVC 控制器方法來建立連結一樣(有關詳細資訊,請參閱在 Spring MVC 中構建連結),您……

以下程式碼顯示瞭如何獲取一個連結並關聯另外兩個可供性

示例 13. 將可供性連線到 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(…​)),您可以使用控制器的方法將 PUTPATCH 操作連線到 GET 操作。想象一下,上面可供的相關方法看起來像這樣

示例 14. 響應 PUT /employees/{id}updateEmpoyee 方法
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee( //
    @RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
示例 15. 響應 PATCH /employees/{id}partiallyUpdateEmployee 方法
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee( //
    @RequestBody EntityModel<Employee> employee, @PathVariable Integer id)

使用 afford(…) 方法指向這些方法將導致 Spring HATEOAS 分析請求體和響應型別,並捕獲元資料,以允許不同的媒體型別實現使用該資訊將其轉換為輸入和輸出的描述。

3.3.1. 手動構建可供性

雖然註冊連結可供性是主要方式,但可能需要手動構建其中一些。這可以透過使用 Affordances API 實現

示例 16. 使用 Affordances API 手動註冊可供性
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 例項,為描述可供性建立上下文。
2 每個可供性都以其應支援的 HTTP 方法開始。然後我們註冊一個型別作為負載描述,並明確命名可供性。後者可以省略,將從 HTTP 方法和輸入型別名稱派生一個預設名稱。這實際上建立了與指向 EmployeeController.newEmployee(…) 相同的可供性。
3 下一個可供性旨在反映指向 EmployeeController.search(…) 時發生的情況。這裡我們定義 Employee 為建立的響應的模型,並明確註冊 QueryParameter

可供性由媒體型別特定的可供性模型支援,這些模型將通用可供性元資料轉換為特定的表示。請務必檢視媒體型別部分中有關可供性的部分,以找到有關如何控制該元資料暴露的更多詳細資訊。

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,如此處所述。

示例 17. 註冊 ForwardedHeaderFilter
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
    return new ForwardedHeaderFilter();
}

這將建立一個 servlet 過濾器,用於處理所有 X-Forwarded-… 頭部。它將正確地與 servlet 處理程式註冊。

對於 Spring WebFlux 應用程式,反應式對應物是 ForwardedHeaderTransformer

示例 18. 註冊 ForwardedHeaderTransformer
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
    return new ForwardedHeaderTransformer();
}

這將建立一個函式,用於轉換反應式 Web 請求,處理 X-Forwarded-… 頭部。它將正確地與 WebFlux 註冊。

在上述配置到位的情況下,傳遞 X-Forwarded-… 頭部的請求將看到這些頭部反映在生成的連結中

示例 19. 使用 X-Forwarded-… 頭部的請求
curl -v localhost:8080/employees \
    -H 'X-Forwarded-Proto: https' \
    -H 'X-Forwarded-Host: example.com' \
    -H 'X-Forwarded-Port: 9001'
示例 20. 考慮這些頭部生成的相應響應和連結
{
  "_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"
    }
  }
}
EntityLinks 及其各種實現目前尚未為 Spring WebFlux 應用程式提供開箱即用的功能。EntityLinks SPI 中定義的契約最初旨在用於 Spring Web MVC,並未考慮 Reactor 型別。開發支援反應式程式設計的可比較契約仍在進行中。

到目前為止,我們透過指向 Web 框架實現(即 Spring MVC 控制器)並檢查對映來建立連結。在許多情況下,這些類實質上讀取和寫入由模型類支援的表示。

EntityLinks 介面現在公開了一個 API,用於根據模型型別查詢 LinkLinkBuilder。這些方法本質上返回指向集合資源(例如 /people)或專案資源(例如 /people/1)的連結。以下示例展示瞭如何使用 EntityLinks

EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);

透過在 Spring MVC 配置中啟用 @EnableHypermediaSupportEntityLinks 可透過依賴注入獲得。這將導致註冊各種 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 允許構建 LinkBuilderLink 例項,指向實體型別的集合和專案資源。以 linkFor… 開頭的方法將為您生成 LinkBuilder 例項,供您擴充套件和增強附加路徑段、引數等。以 linkTo 開頭的方法生成完全準備好的 Link 例項。

雖然對於集合資源,提供實體型別就足夠了,但指向專案資源的連結需要提供識別符號。這通常看起來像這樣

示例 21. 獲取指向專案資源的連結
entityLinks.linkToItemResource(order, order.getId());

如果您發現自己重複這些方法呼叫,可以將識別符號提取步驟提取到一個可重用的 Function 中,以便在不同的呼叫中重複使用

Function<Order, Object> idExtractor = Order::getId; (1)

entityLinks.linkToItemResource(order, idExtractor); (2)
1 識別符號提取被外部化,以便可以將其儲存在欄位或常量中。
2 使用提取器進行連結查詢。
TypedEntityLinks

由於控制器實現通常圍繞實體型別分組,因此您會經常發現在整個控制器類中都使用相同的提取器函式(有關詳細資訊,請參閱EntityLinks API 詳解)。我們可以透過獲取一個 TypedEntityLinks 例項,一次性提供提取器來進一步集中識別符號提取邏輯,這樣實際的查詢就根本不需要處理提取了。

示例 22. 使用 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 中所有其他可用的 EntityLinks 實現作為 bean。它被註冊為主要 bean,因此當您注入 EntityLinks 時,它始終是唯一的注入候選者。ControllerEntityLinks 是預設實現,將包含在設定中,但使用者可以自由實現和註冊自己的實現。要使這些實現可用於注入的 EntityLinks 例項,只需將您的實現註冊為 Spring bean 即可。

示例 23. 宣告自定義 EntityLinks 實現
@Configuration
class CustomEntityLinksConfiguration {

  @Bean
  MyEntityLinks myEntityLinks(…) {
    return new MyEntityLinks(…);
  }
}

此機制可擴充套件性的一個示例是 Spring Data REST 的 RepositoryEntityLinks,它使用儲存庫對映資訊建立指向由 Spring Data 儲存庫支援的資源的連結。同時,它甚至為其他型別的資源公開了額外的查詢方法。如果您想利用這些方法,只需顯式注入 RepositoryEntityLinks 即可。

3.6. 表示模型裝配器

由於從實體到表示模型的對映必須在多個地方使用,因此建立專門負責此操作的類是有意義的。轉換包含非常自定義的步驟,但也包含一些樣板步驟

  1. 模型類的例項化

  2. 新增一個 relself 的連結,指向正在渲染的資源。

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(…) 方法允許您建立資源例項,並向其新增一個 relselfLink。該連結的 href 由配置的控制器的請求對映加上實體 ID 確定(例如,/people/1)。

  • 資源型別透過反射例項化,並期望一個無參建構函式。如果您想使用專用建構函式或避免反射效能開銷,可以覆蓋 instantiateModel(…)

然後,您可以使用裝配器來裝配 RepresentationModelCollectionModel。以下示例建立 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" : "https:///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" : "https:///orders/999"
    },
    "payments" : { (1)
      "href" : "/payments/42" (2)
    }
  }
}
1 您看到 LinkRelation.of("payments") 被插入為該連結的關係。
2 URI 由處理器提供。

這個例子很簡單,但您可以輕鬆地

  • 使用 WebMvcLinkBuilderWebFluxLinkBuilder 構造指向 PaymentController 的動態連結。

  • 注入任何必要的服務,以根據狀態有條件地新增其他連結(例如 cancelamend)。

  • 利用像 Spring Security 這樣的橫切服務,根據當前使用者的上下文新增、刪除或修改連結。

此外,在此示例中,PaymentProcessor 更改了提供的 EntityModel<Order>。您還可以將其替換為另一個物件。請注意,API 要求返回型別等於輸入型別。

3.7.1. 處理空集合模型

為了找到要為 RepresentationModel 例項呼叫的正確 RepresentationModelProcessor 例項集,呼叫基礎設施會對其註冊的 RepresentationModelProcessor 的泛型宣告進行詳細分析。對於 CollectionModel 例項,這包括檢查底層集合的元素,因為在執行時,單個模型例項不會公開泛型資訊(由於 Java 的型別擦除)。這意味著,預設情況下,RepresentationModelProcessor 例項不會為空集合模型呼叫。為了仍然允許基礎設施正確推斷有效負載型別,您可以從一開始就使用顯式備用有效負載型別初始化空 CollectionModel 例項,或者透過呼叫 CollectionModel.withFallbackType(…) 註冊它。有關詳細資訊,請參閱集合資源表示模型

3.8. 使用 LinkRelationProvider API

在構建連結時,您通常需要確定要用於連結的關係型別。在大多數情況下,關係型別直接與(域)型別關聯。我們將查詢關係型別的詳細演算法封裝在 LinkRelationProvider API 後面,該 API 允許您確定單個和集合資源的關係型別。查詢關係型別的演算法如下

  1. 如果型別用 @Relation 註解,我們使用註解中配置的值。

  2. 如果沒有,我們預設使用未大寫的簡單類名加上集合 rel 的附加 List

  3. 如果 classpath 中存在 EVO inflector JAR,我們使用複數化演算法提供的單個資源 rel 的複數。

  4. @ExposesResourceFor 註解的 @Controller 類(有關詳細資訊,請參閱使用 EntityLinks 介面)透明地查詢註解中配置的型別的關係型別,以便您可以使用 LinkRelationProvider.getItemResourceRelFor(MyController.class) 並獲取公開的域型別的關係型別。

當您使用 @EnableHypermediaSupport 時,LinkRelationProvider 會自動作為 Spring bean 公開。您可以透過實現介面並將其作為 Spring bean 暴露來插入自定義提供程式。

4. 媒體型別

4.1. HAL – 超文字應用語言

JSON 超文字應用語言(HAL)是最簡單且最廣泛採用的超媒體媒體型別之一,當不討論特定 Web 棧時。

它是 Spring HATEOAS 採用的第一個基於規範的媒體型別。

4.1.1. 構建 HAL 表示模型

自 Spring HATEOAS 1.1 起,我們提供了一個專用的 HalModelBuilder,它允許透過 HAL 慣用 API 建立 RepresentationModel 例項。其基本假設如下

  1. HAL 表示可以由任意物件(實體)支援,該物件構建表示中包含的域欄位。

  2. 表示可以由各種嵌入式文件豐富,這些文件可以是任意物件,也可以是 HAL 表示本身(即包含巢狀嵌入和連結)。

  3. 某些 HAL 特定模式(例如預覽)可以直接在 API 中使用,以便設定表示的程式碼讀起來就像您在描述遵循這些習語的 HAL 表示。

這是一個 API 使用示例

// An order
var order = new Order(…); (1)

// The customer who placed the order
var customer = customer.findById(order.getCustomerId());

var customerLink = Link.of("/orders/{id}/customer") (2)
  .expand(order.getId())
  .withRel("customer");

var additional = …

var model = HalModelBuilder.halModelOf(order)
  .preview(new CustomerSummary(customer)) (3)
  .forLink(customerLink) (4)
  .embed(additional) (5)
  .link(Link.of(…, IanaLinkRelations.SELF));
  .build();
1 我們設定了一些領域型別。在這種情況下,是一個訂單,它與下單的客戶有關係。
2 我們準備一個指向資源(將公開客戶詳細資訊)的連結
3 我們透過提供應在 _embeddable 子句中渲染的有效負載來開始構建預覽。
4 我們透過提供目標連結來結束該預覽。它被透明地新增到 _links 物件中,並且其連結關係被用作上一步中提供的物件的鍵。
5 可以新增其他物件以顯示在 _embedded 下。它們在其中列出的鍵是從物件的關係設定中派生的。它們可以透過 @Relation 或專用的 LinkRelationProvider 進行自定義(有關詳細資訊,請參閱使用 LinkRelationProvider API)。
{
  "_links" : {
    "self" : { "href" : "…" }, (1)
    "customer" : { "href" : "/orders/4711/customer" } (2)
  },
  "_embedded" : {
    "customer" : { … }, (3)
    "additional" : { … } (4)
  }
}
1 明確提供了 self 連結。
2 透過 ….preview(…).forLink(…) 透明添加了 customer 連結。
3 提供了預覽物件。
4 透過顯式 ….embed(…) 添加了其他元素。

在 HAL 中,_embedded 也用於表示頂級集合。它們通常按從物件型別派生的連結關係分組。也就是說,訂單列表在 HAL 中看起來像這樣

{
  "_embedded" : {
    "order : [
      … (1)
    ]
  }
}
1 單個訂單文件在這裡。

建立這樣的表示非常簡單

Collection<Order> orders = …;

HalModelBuilder.emptyHalDocument()
  .embed(orders);

也就是說,如果訂單為空,則無法推匯出 _embedded 中應出現的連結關係,因此如果集合為空,文件將保持為空。

如果您希望明確地表示空集合,可以將型別傳遞給接受 Collection….embed(…) 方法的過載。如果傳遞給方法的集合為空,這將導致渲染一個欄位,其連結關係從給定型別派生。

HalModelBuilder.emptyHalModel()
  .embed(Collections.emptyList(), Order.class);
  // or
  .embed(Collections.emptyList(), LinkRelation.of("orders"));

將建立以下更明確的表示。

{
  "_embedded" : {
    "orders" : []
  }
}

4.1.2. 配置連結渲染

在 HAL 中,_links 條目是一個 JSON 物件。屬性名稱是連結關係,每個值都是連結物件或連結物件陣列

對於具有兩個或更多連結的給定連結關係,規範對其表示方式明確

示例 24. 具有兩個連結與一個關係關聯的 HAL 文件
{
  "_links": {
    "item": [
      { "href": "https://myhost/cart/42" },
      { "href": "https://myhost/inventory/12" }
    ]
  },
  "customer": "Dave Matthews"
}

但是,如果給定關係只有一個連結,則規範是模糊的。您可以將其渲染為單個物件或單個項陣列。

預設情況下,Spring HATEOAS 使用最簡潔的方法,並將單鏈接關係渲染為

示例 25. HAL 文件,其中單個連結渲染為物件
{
  "_links": {
    "item": { "href": "https://myhost/inventory/12" }
  },
  "customer": "Dave Matthews"
}

一些使用者在消費 HAL 時不喜歡在陣列和物件之間切換。他們更喜歡這種渲染方式

示例 26. HAL,其中單個連結渲染為陣列
{
  "_links": {
    "item": [{ "href": "https://myhost/inventory/12" }]
  },
  "customer": "Dave Matthews"
}

如果您希望自定義此策略,只需將 HalConfiguration bean 注入到您的應用程式配置中即可。有多種選擇。

示例 27. 全域性 HAL 單鏈接渲染策略
@Bean
public HalConfiguration globalPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 透過將所有單鏈接關係渲染為陣列來覆蓋 Spring HATEOAS 的預設設定。

如果您只想覆蓋某些特定的連結關係,可以像這樣建立 HalConfiguration bean

示例 28. 基於連結關係的 HAL 單鏈接渲染策略
@Bean
public HalConfiguration linkRelationBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1)
      .withRenderSingleLinksFor( //
          LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2)
}
1 始終將 item 連結關係渲染為陣列。
2 當只有一個連結時,將 prev 連結關係渲染為物件。

如果這些都不符合您的需求,您可以使用 Ant 風格的路徑模式

示例 29. 基於模式的 HAL 單鏈接渲染策略
@Bean
public HalConfiguration patternBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          "http*", RenderSingleLinks.AS_ARRAY); (1)
}
1 將所有以 http 開頭的連結關係渲染為陣列。
基於模式的方法使用 Spring 的 AntPathMatcher

所有這些 HalConfiguration 的 wither 方法都可以組合成一個全面的策略。請務必廣泛測試您的 API,以避免意外。

4.1.3. 連結標題國際化

HAL 為其連結物件定義了一個 title 屬性。這些標題可以透過使用 Spring 的資源包抽象和名為 rest-messages 的資源包來填充,以便客戶端可以直接在其 UI 中使用它們。此包將自動設定,並在 HAL 連結序列化期間使用。

要為連結定義標題,請使用鍵模板 _links.$relationName.title,如下所示

示例 30. 示例 rest-messages.properties
_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout

這將導致以下 HAL 表示

示例 31. 定義了連結標題的示例 HAL 文件
{
  "_links" : {
    "cancel" : {
      "href" : "…"
      "title" : "Cancel order"
    },
    "payment" : {
      "href" : "…"
      "title" : "Proceed to checkout"
    }
  }
}

4.1.4. 使用 CurieProvider API

Web 連結 RFC 描述了註冊和擴充套件連結關係型別。註冊的關係是已知的字串,已註冊到 IANA 連結關係型別登錄檔。應用程式不希望註冊關係型別時可以使用擴充套件 rel URI。每個都是唯一標識關係型別的 URI。rel URI 可以序列化為緊湊 URI 或 Curie。例如,如果 ex 定義為 example.com/rels/{rel},則 ex:persons 的 curie 代表連結關係型別 example.com/rels/persons。如果使用 curie,則基本 URI 必須存在於響應範圍內。

預設 RelProvider 建立的 rel 值是擴充套件關係型別,因此必須是 URI,這可能會導致大量開銷。CurieProvider API 解決了這個問題:它允許您將基本 URI 定義為 URI 模板,以及代表該基本 URI 的字首。如果存在 CurieProvider,則 RelProvider 會將 curie 字首新增到所有未在 IANA 註冊的 rel 值。此外,HAL 資源會自動新增一個 curies 連結。

以下配置定義了一個預設的 curie 提供程式

@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type= {HypermediaType.HAL})
public class Config {

  @Bean
  public CurieProvider curieProvider() {
    return new DefaultCurieProvider("ex", new UriTemplate("https://www.example.com/rels/{rel}"));
  }
}

請注意,現在 ex: 字首會自動出現在所有未在 IANA 註冊的 rel 值之前,例如 ex:orders。客戶端可以使用 curies 連結將 curie 解析為完整形式。以下示例展示瞭如何實現

{
  "_links": {
    "self": {
      "href": "https://myhost/person/1"
    },
    "curies": {
      "name": "ex",
      "href": "https://example.com/rels/{rel}",
      "templated": true
    },
    "ex:orders": {
      "href": "https://myhost/person/1/orders"
    }
  },
  "firstname": "Dave",
  "lastname": "Matthews"
}

由於 CurieProvider API 的目的是允許自動建立 curie,因此您每個應用程式範圍只能定義一個 CurieProvider bean。

4.2. HAL-FORMS

HAL-FORMS 旨在為 HAL 媒體型別新增執行時表單支援。

HAL-FORMS “看起來像 HAL”。然而,重要的是要記住 HAL-FORMS 與 HAL 不同——兩者絕不應被視為可以互換。

— Mike Amundsen
HAL-FORMS 規範

要啟用此媒體型別,請在您的程式碼中新增以下配置

示例 32. 啟用 HAL-FORMS 的應用程式
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {

}

每當客戶端提供帶有 application/prs.hal-forms+jsonAccept 頭部時,您都可以預期如下內容

示例 33. HAL-FORMS 示例文件
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "role" : "ring bearer",
  "_links" : {
    "self" : {
      "href" : "https://:8080/employees/1"
    }
  },
  "_templates" : {
    "default" : {
      "method" : "put",
      "properties" : [ {
        "name" : "firstName",
        "required" : true
      }, {
        "name" : "lastName",
        "required" : true
      }, {
        "name" : "role",
        "required" : true
      } ]
    },
    "partiallyUpdateEmployee" : {
      "method" : "patch",
      "properties" : [ {
        "name" : "firstName",
        "required" : false
      }, {
        "name" : "lastName",
        "required" : false
      }, {
        "name" : "role",
        "required" : false
      } ]
    }
  }
}

請查閱 HAL-FORMS 規範以瞭解 _templates 屬性的詳細資訊。閱讀可供性 API,為您的控制器新增此額外元資料。

對於單項(EntityModel)和聚合根集合(CollectionModel),Spring HATEOAS 的渲染方式與HAL 文件相同。

4.2.1. 定義 HAL-FORMS 元資料

HAL-FORMS 允許描述每個表單欄位的標準。Spring HATEOAS 允許透過為輸入和輸出型別塑造模型型別並在其上使用註解來自定義這些標準。

每個模板將定義以下屬性

表 1. 模板屬性
屬性 描述

contentType

伺服器預期接收的媒體型別。僅在指向的控制器方法公開 @RequestMapping(consumes = "…") 屬性,或在設定可供性時顯式定義了媒體型別時才包含。

method

提交模板時使用的 HTTP 方法。

target

提交表單的目標 URI。僅當可供性目標與宣告它的連結不同時才會渲染。

title

顯示模板時的人類可讀標題。

屬性

要與表單一起提交的所有屬性(參見下文)。

每個屬性將定義以下屬性

表 2. 屬性屬性
屬性 描述

只讀

如果屬性沒有 setter 方法,則設定為 true。如果存在,請在訪問器或欄位上明確使用 Jackson 的 @JsonProperty(Access.READ_ONLY)。預設情況下不渲染,因此預設為 false

regex

可以透過在欄位或型別上使用 JSR-303 的 @Pattern 註解進行自定義。在後一種情況下,模式將用於宣告為該特定型別的所有屬性。預設情況下不渲染。

required

可以透過使用 JSR-303 的 @NotNull 進行自定義。預設情況下不渲染,因此預設為 false。使用 PATCH 作為方法的模板將自動將所有屬性設定為非必需。

max

屬性允許的最大值。派生自 JSR-303 的 @Size、Hibernate Validator 的 @Range 或 JSR-303 的 @Max@DecimalMax 註解。

maxLength

屬性允許的最大長度值。派生自 Hibernate Validator 的 @Length 註解。

min

屬性允許的最小值。派生自 JSR-303 的 @Size、Hibernate Validator 的 @Range 或 JSR-303 的 @Min@DecimalMin 註解。

minLength

屬性允許的最小長度值。派生自 Hibernate Validator 的 @Length 註解。

options

提交表單時從中選擇值的選項。有關詳細資訊,請參閱為屬性定義 HAL-FORMS 選項

prompt

渲染表單輸入時使用的使用者可讀提示。有關詳細資訊,請參閱屬性提示

placeholder

使用者可讀的佔位符,用於給出預期格式的示例。定義方式遵循屬性提示,但使用字尾 _placeholder

型別

HTML 輸入型別派生自顯式 @InputType 註解、JSR-303 驗證註解或屬性型別。

對於無法手動註解的型別,您可以透過應用程式上下文中存在的 HalFormsConfiguration bean 註冊自定義模式。

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

    HalFormsConfiguration configuration = new HalFormsConfiguration();
    configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
  }
}

此設定將導致型別為 CreditCardNumber 的表示模型屬性的 HAL-FORMS 模板屬性宣告一個值為 [0-9]{16}regex 欄位。

為屬性定義 HAL-FORMS 選項

對於其值應與某個值超集匹配的屬性,HAL-FORMS 在屬性定義中定義了 options 子文件。可以使用 HalFormsConfigurationwithOptions(…) 方法來描述某個屬性可用的選項,該方法接受指向型別屬性的指標和用於將 PropertyMetadata 轉換為 HalFormsOptions 例項的建立器函式。

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

    HalFormsConfiguration configuration = new HalFormsConfiguration();
    configuration.withOptions(Order.class, "shippingMethod" metadata ->
      HalFormsOptions.inline("FedEx", "DHL"));
  }
}

請注意我們如何將選項值 FedExDHL 設定為 Order.shippingMethod 屬性的選擇選項。另外,HalFormsOptions.remote(…) 可以指向提供動態值的遠端資源。有關選項設定的更多限制,請參閱規範HalFormsOptions 的 Javadoc。

4.2.2. 表單屬性的國際化

HAL-FORMS 包含用於人類解釋的屬性,如模板的標題或屬性提示。這些可以使用 Spring 的資源包支援和 Spring HATEOAS 預設配置的 rest-messages 資源包進行定義和國際化。

模板標題

要定義模板標題,請使用以下模式:_templates.$affordanceName.title。請注意,在 HAL-FORMS 中,如果模板是唯一的,則其名稱為 default。這意味著您通常需要使用可供性描述的本地或完全限定輸入型別名稱來限定鍵。

示例 34. 定義 HAL-FORMS 模板標題
_templates.default.title=Some title (1)
_templates.putEmployee.title=Create employee (2)
Employee._templates.default.title=Create employee (3)
com.acme.Employee._templates.default.title=Create employee (4)
1 使用 default 作為鍵的標題的全域性定義。
2 使用實際可供性名稱作為鍵的標題的全域性定義。除非在建立可供性時明確定義,否則此名稱預設為建立可供性時指向的方法的名稱。
3 應用於所有名為 Employee 的型別的區域性定義標題。
4 使用完全限定型別名稱的標題定義。
使用實際可供性名稱的鍵優先於預設鍵。
屬性提示

屬性提示也可以透過 Spring HATEOAS 自動配置的 rest-messages 資源包進行解析。鍵可以全域性、區域性或完全限定定義,並且需要將 ._prompt 連線到實際屬性鍵

示例 35. 為 email 屬性定義提示
firstName._prompt=Firstname (1)
Employee.firstName._prompt=Firstname (2)
com.acme.Employee.firstName._prompt=Firstname (3)
1 所有名為 firstName 的屬性都將渲染為“Firstname”,無論它們在何種型別中宣告。
2 在名為 Employee 的型別中,firstName 屬性將提示為“Firstname”。
3 com.acme.EmployeefirstName 屬性將分配提示“Firstname”。

4.2.3. 一個完整示例

讓我們看一個結合了上述所有定義和自定義屬性的示例程式碼。客戶的 RepresentationModel 可能看起來像這樣

class CustomerRepresentation
  extends RepresentationModel<CustomerRepresentation> {

  String name;
  LocalDate birthdate; (1)
  @Pattern(regex = "[0-9]{16}") String ccn; (2)
  @Email String email; (3)
}
1 我們定義了一個型別為 LocalDatebirthdate 屬性。
2 我們期望 ccn 符合正則表示式。
3 我們使用 JSR-303 @Email 註解將 email 定義為電子郵件。

請注意,此型別不是域型別。它有意設計用於捕獲各種潛在的無效輸入,以便可以一次性拒絕欄位的潛在錯誤值。

讓我們繼續看看控制器如何使用該模型

@Controller
class CustomerController {

  @PostMapping("/customers")
  EntityModel<?> createCustomer(@RequestBody CustomerRepresentation payload) { (1)
    // …
  }

  @GetMapping("/customers")
  CollectionModel<?> getCustomers() {

    CollectionModel<?> model = …;

    CustomerController controller = methodOn(CustomerController.class);

    model.add(linkTo(controller.getCustomers()).withSelfRel() (2)
      .andAfford(controller.createCustomer(null)));

    return ResponseEntity.ok(model);
  }
}
1 聲明瞭一個控制器方法,如果向 /customers 發出 POST 請求,則使用上面定義的表示模型將請求體繫結到它。
2 /customersGET 請求準備一個模型,向其新增一個 self 連結,並在此連結上宣告一個指向對映到 POST 的控制器方法的可供性。這將導致構建一個可供性模型,該模型——根據最終要渲染的媒體型別——將被轉換為媒體型別特定的格式。

接下來,讓我們新增一些額外的元資料,使表單更易於人類訪問

rest-messages.properties 中宣告的附加屬性。
CustomerRepresentation._template.createCustomer.title=Create customer (1)
CustomerRepresentation.ccn._prompt=Credit card number (2)
CustomerRepresentation.ccn._placeholder=1234123412341234 (2)
1 我們為透過指向 createCustomer(…) 方法建立的模板定義了一個顯式標題。
2 我們明確為 CustomerRepresentation 模型的 ccn 屬性提供提示和佔位符。

如果客戶端現在使用 Accept 頭部 application/prs.hal-forms+json/customers 發出 GET 請求,則響應 HAL 文件將擴充套件為 HAL-FORMS 文件,以包含以下 _templates 定義

{
  …,
  "_templates" : {
    "default" : { (1)
      "title" : "Create customer", (2)
      "method" : "post", (3)
      "properties" : [ {
        "name" : "name",
        "required" : true,
        "type" : "text" (4)
      } , {
        "name" : "birthdate",
        "required" : true,
        "type" : "date" (4)
      } , {
        "name" : "ccn",
        "prompt" : "Credit card number", (5)
        "placeholder" : "1234123412341234" (5)
        "required" : true,
        "regex" : "[0-9]{16}", (6)
        "type" : "text"
      } , {
        "name" : "email",
        "prompt" : "Email",
        "required" : true,
        "type" : "email" (7)
      } ]
    }
  }
}
1 公開了一個名為 default 的模板。其名稱為 default,因為它是唯一定義的模板,並且規範要求使用該名稱。如果附加了多個模板(透過宣告額外的可供性),它們將各自以其指向的方法命名。
2 模板標題來自資源包中定義的值。請注意,根據請求傳送的 Accept-Language 頭部和可用性,可能會返回不同的值。
3 method 屬性的值來自派生可供性方法的對映。
4 type 屬性的值 text 源自屬性型別 Stringbirthdate 屬性也適用,但結果為 date
5 ccn 屬性的提示和佔位符也來自資源包。
6 ccn 屬性的 @Pattern 宣告作為模板屬性的 regex 屬性公開。
7 email 屬性上的 @Email 註解已轉換為相應的 type 值。

HAL-FORMS 模板被例如 HAL Explorer 考慮,它會自動從這些描述渲染 HTML 表單。

4.3. HTTP 問題詳情

HTTP API 問題詳情 是一種媒體型別,用於在 HTTP 響應中攜帶機器可讀的錯誤詳細資訊,以避免為 HTTP API 定義新的錯誤響應格式。

HTTP 問題詳情定義了一組 JSON 屬性,用於攜帶附加資訊,以向 HTTP 客戶端描述錯誤詳情。有關這些屬性的更多詳細資訊,請參見 RFC 文件的相關部分。

您可以透過在 Spring MVC 控制器中使用 Problem 媒體型別域型別來建立此類 JSON 響應

使用 Spring HATEOAS 的 Problem 型別報告問題詳細資訊
@RestController
class PaymentController {

  @PutMapping
  ResponseEntity<?> issuePayment(@RequestBody PaymentRequest request) {

    PaymentResult result = payments.issuePayment(request.orderId, request.amount);

    if (result.isSuccess()) {
      return ResponseEntity.ok(result);
    }

    String title = messages.getMessage("payment.out-of-credit");
    String detail = messages.getMessage("payment.out-of-credit.details", //
        new Object[] { result.getBalance(), result.getCost() });

    Problem problem = Problem.create() (1)
        .withType(OUT_OF_CREDIT_URI) //
        .withTitle(title) (2)
        .withDetail(detail) //
        .withInstance(PAYMENT_ERROR_INSTANCE.expand(result.getPaymentId())) //
        .withProperties(map -> { (3)
          map.put("balance", result.getBalance());
          map.put("accounts", Arrays.asList( //
              ACCOUNTS.expand(result.getSourceAccountId()), //
              ACCOUNTS.expand(result.getTargetAccountId()) //
          ));
        });

    return ResponseEntity.status(HttpStatus.FORBIDDEN) //
        .body(problem);
  }
}
1 您首先使用公開的工廠方法建立 Problem 例項。
2 您可以定義媒體型別定義的預設屬性的值,例如型別 URI、標題和詳細資訊,使用 Spring 的國際化功能(參見上文)。
3 可以透過 Map 或顯式物件新增自定義屬性(參見下文)。

要將專用物件用於自定義屬性,請宣告一個型別,建立並填充它的例項,並透過 ….withProperties(…) 或在例項建立時透過 Problem.create(…) 將其傳遞給 Problem 例項。

使用專用型別捕獲擴充套件問題屬性
class AccountDetails {
  int balance;
  List<URI> accounts;
}

problem.withProperties(result.getDetails());

// or

Problem.create(result.getDetails());

這將導致如下所示的響應

示例 HTTP 問題詳情響應
{
  "type": "https://example.com/probs/out-of-credit",
  "title": "You do not have enough credit.",
  "detail": "Your current balance is 30, but that costs 50.",
  "instance": "/account/12345/msgs/abc",
  "balance": 30,
  "accounts": ["/account/12345",
               "/account/67890"]
}

4.4. Collection+JSON

Collection+JSON 是一個 JSON 規範,已在 IANA 批准的媒體型別 application/vnd.collection+json 下注冊。

Collection+JSON 是一種基於 JSON 的讀寫超媒體型別,旨在支援簡單集合的管理和查詢。

— Mike Amundsen
Collection+JSON 規範

Collection+JSON 提供了一種統一的方式來表示單項資源和集合。要啟用此媒體型別,請將以下配置放入您的程式碼中

示例 36. 啟用 Collection+JSON 的應用程式
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {

}

此配置將使您的應用程式響應 Accept 頭部為 application/vnd.collection+json 的請求,如下所示。

以下規範示例顯示了單個專案

示例 37. Collection+JSON 單項示例
{
  "collection": {
    "version": "1.0",
    "href": "https://example.org/friends/", (1)
    "links": [   (2)
      {
        "rel": "feed",
        "href": "https://example.org/friends/rss"
      },
      {
        "rel": "queries",
        "href": "https://example.org/friends/?queries"
      },
      {
        "rel": "template",
        "href": "https://example.org/friends/?template"
      }
    ],
    "items": [  (3)
      {
        "href": "https://example.org/friends/jdoe",
        "data": [  (4)
          {
            "name": "fullname",
            "value": "J. Doe",
            "prompt": "Full Name"
          },
          {
            "name": "email",
            "value": "[email protected]",
            "prompt": "Email"
          }
        ],
        "links": [ (5)
          {
            "rel": "blog",
            "href": "https://examples.org/blogs/jdoe",
            "prompt": "Blog"
          },
          {
            "rel": "avatar",
            "href": "https://examples.org/images/jdoe",
            "prompt": "Avatar",
            "render": "image"
          }
        ]
      }
    ]
  }
}
1 self 連結儲存在文件的 href 屬性中。
2 文件的頂級 links 部分包含集合級連結(減去 self 連結)。
3 items 部分包含資料集合。由於這是一個單項文件,因此它只有一個條目。
4 data 部分包含實際內容。它由屬性組成。
5 專案的單個 links

前面的片段是從規範中提取的。當 Spring HATEOAS 渲染 EntityModel 時,它將

  • self 連結放入文件的 href 屬性和專案級 href 屬性。

  • 將模型的其餘連結放入頂級 links 和專案級 links

  • EntityModel 中提取屬性並將其轉換為……

當渲染資源集合時,文件幾乎相同,只是 items JSON 陣列中會有多個條目,每個條目一個。

Spring HATEOAS 更具體地會

  • 將整個集合的 self 連結放入頂級 href 屬性。

  • CollectionModel 連結(減去 self)將放入頂級 links 中。

  • 每個專案級的 href 將包含 CollectionModel.content 集合中每個條目對應的 self 連結。

  • 每個專案級的 links 將包含 CollectionModel.content 中每個條目的所有其他連結。

4.5. UBER - 統一表示交換基礎

UBER 是一個實驗性的 JSON 規範

UBER 文件格式是一種最小的讀寫超媒體型別,旨在支援簡單的狀態傳輸和基於超媒體的臨時轉換。

— Mike Amundsen
UBER 規範

UBER 提供了一種統一的方式來表示單個專案資源和集合。要啟用此媒體型別,請將以下配置放入您的程式碼中

示例 38. 啟用 UBER+JSON 的應用程式
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {

}

此配置將使您的應用程式響應使用 Accept 頭部 application/vnd.amundsen-uber+json 的請求,如下所示

示例 39. UBER 示例文件
{
  "uber" : {
    "version" : "1.0",
    "data" : [ {
      "rel" : [ "self" ],
      "url" : "/employees/1"
    }, {
      "name" : "employee",
      "data" : [ {
        "name" : "role",
        "value" : "ring bearer"
      }, {
        "name" : "name",
        "value" : "Frodo"
      } ]
    } ]
  }
}

此媒體型別仍在開發中,規範本身也是如此。如果您在使用過程中遇到問題,請隨意提交工單

UBER 媒體型別與乘車共享公司 Uber Technologies Inc. 沒有任何關聯。

4.6. ALPS - 應用級配置檔案語義

ALPS 是一種媒體型別,用於提供有關另一個資源的基於配置檔案的元資料。

ALPS 文件可以用作配置檔案,以解釋具有應用程式無關媒體型別(如 HTML、HAL、Collection+JSON、Siren 等)的文件的應用程式語義。這增加了配置檔案文件在不同媒體型別之間的可重用性。

— Mike Amundsen
ALPS 規範

ALPS 無需特殊啟用。相反,您“構建”一個 Alps 記錄並從 Spring MVC 或 Spring WebFlux Web 方法返回它,如下所示

示例 40. 構建 Alps 記錄
@GetMapping(value = "/profile", produces = ALPS_JSON_VALUE)
Alps profile() {

  return Alps.alps() //
      .doc(doc() //
          .href("https://example.org/samples/full/doc.html") //
          .value("value goes here") //
          .format(Format.TEXT) //
          .build()) //
      .descriptor(getExposedProperties(Employee.class).stream() //
          .map(property -> Descriptor.builder() //
              .id("class field [" + property.getName() + "]") //
              .name(property.getName()) //
              .type(Type.SEMANTIC) //
              .ext(Ext.builder() //
                  .id("ext [" + property.getName() + "]") //
                  .href("https://example.org/samples/ext/" + property.getName()) //
                  .value("value goes here") //
                  .build()) //
              .rt("rt for [" + property.getName() + "]") //
              .descriptor(Collections.singletonList(Descriptor.builder().id("embedded").build())) //
              .build()) //
          .collect(Collectors.toList()))
      .build();
}
  • 此示例利用 PropertyUtils.getExposedProperties() 提取有關域物件屬性的元資料。

此片段已插入測試資料。它生成如下 JSON

示例 41. ALPS JSON
{
  "version": "1.0",
  "doc": {
    "format": "TEXT",
    "href": "https://example.org/samples/full/doc.html",
    "value": "value goes here"
  },
  "descriptor": [
    {
      "id": "class field [name]",
      "name": "name",
      "type": "SEMANTIC",
      "descriptor": [
        {
          "id": "embedded"
        }
      ],
      "ext": {
        "id": "ext [name]",
        "href": "https://example.org/samples/ext/name",
        "value": "value goes here"
      },
      "rt": "rt for [name]"
    },
    {
      "id": "class field [role]",
      "name": "role",
      "type": "SEMANTIC",
      "descriptor": [
        {
          "id": "embedded"
        }
      ],
      "ext": {
        "id": "ext [role]",
        "href": "https://example.org/samples/ext/role",
        "value": "value goes here"
      },
      "rt": "rt for [role]"
    }
  ]
}

您可以手動編寫每個欄位,而不是將它們“自動”連結到域物件的欄位。也可以使用 Spring Framework 的訊息包和 MessageSource 介面。這使您能夠將這些值委託給特定於區域設定的訊息包,甚至國際化元資料。

4.7. 社群媒體型別

得益於建立您自己的媒體型別的能力,現在有許多社群主導的努力來構建額外的媒體型別。

4.7.1. JSON:API

Maven 座標
<dependency>
    <groupId>com.toedter</groupId>
    <artifactId>spring-hateoas-jsonapi</artifactId>
    <version>{see project page for current version}</version>
</dependency>
Gradle 座標
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'

如果您需要快照版本,請訪問專案頁面瞭解更多詳細資訊。

4.7.2. Siren

Maven 座標
<dependency>
    <groupId>de.ingogriebsch.hateoas</groupId>
    <artifactId>spring-hateoas-siren</artifactId>
    <version>{see project page for current version}</version>
    <scope>compile</scope>
</dependency>
Gradle 座標
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'

4.8. 註冊自定義媒體型別

Spring HATEOAS 允許您透過 SPI 整合自定義媒體型別。此類實現的構建塊是

  1. 某種形式的 Jackson ObjectMapper 自定義。在最簡單的情況下,它是一個 Jackson Module 實現。

  2. LinkDiscoverer 實現,以便客戶端支援能夠檢測表示中的連結。

  3. 少量的基礎設施配置,允許 Spring HATEOAS 找到自定義實現並將其拾取。

4.8.1. 自定義媒體型別配置

自定義媒體型別實現由 Spring HATEOAS 透過掃描應用程式上下文以查詢 HypermediaMappingInformation 介面的任何實現來拾取。每個媒體型別都必須實現此接口才能

定義您自己的媒體型別可以像這樣簡單

@Configuration
public class MyMediaTypeConfiguration implements HypermediaMappingInformation {

  @Override
  public List<MediaType> getMediaTypes() {
    return Collections.singletonList(MediaType.parseMediaType("application/vnd-acme-media-type")); (1)
  }

  @Override
  public Module getJacksonModule() {
    return new Jackson2MyMediaTypeModule(); (2)
  }

  @Bean
  MyLinkDiscoverer myLinkDiscoverer() {
    return new MyLinkDiscoverer(); (3)
  }
}
1 配置類返回其支援的媒體型別。這適用於伺服器端和客戶端場景。
2 它覆蓋 getJacksonModule() 以提供自定義序列化器,從而建立特定於媒體型別的表示。
3 它還聲明瞭一個自定義 LinkDiscoverer 實現以提供進一步的客戶端支援。

Jackson 模組通常為表示模型型別 RepresentationModelEntityModelCollectionModelPagedModel 宣告 SerializerDeserializer 實現。如果您需要進一步自定義 Jackson ObjectMapper(例如自定義 HandlerInstantiator),您可以選擇覆蓋 configureObjectMapper(…)

參考文件的早期版本曾提及實現 MediaTypeConfigurationProvider 介面並將其註冊到 spring.factories。這是不必要的。此 SPI 僅用於 Spring HATEOAS 提供的開箱即用的媒體型別。只需實現 HypermediaMappingInformation 介面並將其註冊為 Spring bean 即可。

4.8.2. 建議

實現媒體型別表示的首選方法是提供一個型別層次結構,該結構與預期格式匹配,並且可以由 Jackson 按原樣序列化。在為 RepresentationModel 註冊的 SerializerDeserializer 實現中,將例項轉換為特定於媒體型別的模型型別,然後查詢這些型別的 Jackson 序列化器。

預設支援的媒體型別使用與第三方實現相同的配置機制。因此,值得研究 mediatype 包中的實現。請注意,內建媒體型別實現將其配置類保持包私有,因為它們透過 @EnableHypermediaSupport 啟用。自定義實現應該將其公開,以確保使用者可以從其應用程式包中匯入這些配置類。

5. 配置

本節介紹如何配置 Spring HATEOAS。

5.1. 使用 @EnableHypermediaSupport

為了讓 RepresentationModel 子型別根據各種超媒體表示型別的規範進行渲染,您可以透過 @EnableHypermediaSupport 啟用對特定超媒體表示格式的支援。該註解接受一個 HypermediaType 列舉作為其引數。目前,我們支援 HAL 以及預設渲染。使用該註解會觸發以下操作:

  • 它註冊必要的 Jackson 模組,以超媒體特定格式渲染 EntityModelCollectionModel

  • 如果 JSONPath 在類路徑上,它會自動註冊一個 LinkDiscoverer 例項,以在純 JSON 表示中按其 rel 查詢連結(請參閱 使用 LinkDiscoverer 例項)。

  • 預設情況下,它啟用 實體連結,並自動拾取 EntityLinks 實現,並將它們打包到一個您可以自動裝配的 DelegatingEntityLinks 例項中。

  • 它會自動拾取 ApplicationContext 中的所有 RelProvider 實現,並將它們打包到一個您可以自動裝配的 DelegatingRelProvider 中。它註冊提供程式以考慮領域型別上的 @Relation 以及 Spring MVC 控制器。如果 EVO inflector 在類路徑上,集合 rel 值將使用庫中實現的複數化演算法派生(請參閱 [spis.rel-provider])。

5.1.1. 顯式啟用對專用 Web 棧的支援

預設情況下,@EnableHypermediaSupport 將透過反射檢測您正在使用的 Web 應用程式棧,並與為這些棧註冊的 Spring 元件掛鉤,以啟用對超媒體表示的支援。但是,在某些情況下,您可能只想顯式啟用對特定棧的支援。例如,如果您的基於 Spring WebMVC 的應用程式使用 WebFlux 的 WebClient 發出出站請求,並且該請求不應與超媒體元素一起工作,您可以透過在配置中顯式宣告 WebMVC 來限制要啟用的功能

示例 42. 顯式啟用對特定 Web 棧的超媒體支援
@EnableHypermediaSupport(…, stacks = WebStack.WEBMVC)
class MyHypermediaConfiguration { … }

6. 客戶端支援

本節介紹 Spring HATEOAS 對客戶端的支援。

6.1. Traverson

Spring HATEOAS 為客戶端服務遍歷提供了 API。它受到 Traverson JavaScript 庫的啟發。以下示例展示瞭如何使用它:

Map<String, Object> parameters = new HashMap<>();
parameters.put("user", 27);

Traverson traverson = new Traverson(URI.create("https://:8080/api/"), MediaTypes.HAL_JSON);
String name = traverson
    .follow("movies", "movie", "actor").withTemplateParameters(parameters)
    .toObject("$.name");

您可以透過將其指向 REST 伺服器並配置要設定為 Accept 頭的媒體型別來設定 Traverson 例項。然後,您可以定義要發現和遵循的關係名稱。關係名稱可以是簡單名稱或 JSONPath 表示式(以 $ 開頭)。

然後,示例將引數對映傳遞給 Traverson 例項。這些引數用於擴充套件在遍歷期間找到的 URI(這些 URI 是模板化的)。遍歷透過訪問最終遍歷的表示來結束。在前面的示例中,我們評估一個 JSONPath 表示式來訪問演員的姓名。

前面的示例是遍歷的最簡單版本,其中 rel 值是字串,並且在每個跳躍點都應用相同的模板引數。

還有更多選項可以在每個級別自定義模板引數。以下示例展示了這些選項。

ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};

EntityModel<Item> itemResource = traverson.//
    follow(rel("items").withParameter("projection", "noImages")).//
    follow("$._embedded.items[0]._links.self.href").//
    toObject(resourceParameterizedTypeReference);

靜態 rel(…​) 函式是定義單個 Hop 的便捷方式。使用 .withParameter(key, value) 可以輕鬆指定 URI 模板變數。

.withParameter() 返回一個可鏈式呼叫的新 Hop 物件。您可以根據需要連線任意多個 .withParameter。結果是一個單一的 Hop 定義。以下示例展示了一種實現方式:
ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};

Map<String, Object> params = Collections.singletonMap("projection", "noImages");

EntityModel<Item> itemResource = traverson.//
    follow(rel("items").withParameters(params)).//
    follow("$._embedded.items[0]._links.self.href").//
    toObject(resourceParameterizedTypeReference);

您還可以使用 .withParameters(Map) 載入整個引數 Map

follow() 是可鏈式呼叫的,這意味著您可以連線多個跳躍,如前面的示例所示。您可以放置多個基於字串的 rel 值(follow("items", "item"))或具有特定引數的單個跳躍。

6.1.1. EntityModel<T> vs. CollectionModel<T>

到目前為止所示的示例演示瞭如何規避 Java 的型別擦除,並將單個 JSON 格式的資源轉換為 EntityModel<Item> 物件。但是,如果您獲得一個像 \_embedded HAL 集合這樣的集合怎麼辦?您可以僅透過一個小小的調整來實現,如下面的示例所示:

CollectionModelType<Item> collectionModelType =
    new TypeReferences.CollectionModelType<Item>() {};

CollectionModel<Item> itemResource = traverson.//
    follow(rel("items")).//
    toObject(collectionModelType);

它不是獲取單個資源,而是將集合反序列化為 CollectionModel

在使用啟用超媒體的表示時,一個常見的任務是查詢其中具有特定關係型別的連結。Spring HATEOAS 為預設表示渲染或開箱即用的 HAL 提供了基於 JSONPathLinkDiscoverer 介面實現。當使用 @EnableHypermediaSupport 時,我們會自動將支援配置的超媒體型別的例項作為 Spring bean 公開。

或者,您可以按如下方式設定和使用例項:

String content = "{'_links' :  { 'foo' : { 'href' : '/foo/bar' }}}";
LinkDiscoverer discoverer = new HalLinkDiscoverer();
Link link = discoverer.findLinkWithRel("foo", content);

assertThat(link.getRel(), is("foo"));
assertThat(link.getHref(), is("/foo/bar"));

6.3. 配置 WebClient 例項

如果您需要配置 WebClient 以進行超媒體通訊,這很容易。獲取 HypermediaWebClientConfigurer,如下所示:

示例 43. 自己配置 WebClient
@Bean
WebClient.Builder hypermediaWebClient(HypermediaWebClientConfigurer configurer) { (1)
 return configurer.registerHypermediaTypes(WebClient.builder()); (2)
}
1 在您的 @Configuration 類中,獲取 Spring HATEOAS 註冊的 HypermediaWebClientConfigurer bean 的副本。
2 建立 WebClient.Builder 後,使用配置器註冊超媒體型別。
HypermediaWebClientConfigurer 所做的是向 WebClient.Builder 註冊所有正確的編碼器和解碼器。要使用它,您需要將構建器注入到應用程式中的某個位置,並執行 build() 方法以生成 WebClient

如果您使用的是 Spring Boot,還有另一種方法:WebClientCustomizer

示例 44. 讓 Spring Boot 配置事物
@Bean (4)
WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) { (1)
    return webClientBuilder -> { (2)
        configurer.registerHypermediaTypes(webClientBuilder); (3)
    };
}
1 建立 Spring bean 時,請求 Spring HATEOAS 的 HypermediaWebClientConfigurer bean 的副本。
2 使用 Java 8 lambda 表示式定義一個 WebClientCustomizer
3 在函式呼叫內部,應用 registerHypermediaTypes 方法。
4 將整個內容作為 Spring bean 返回,以便 Spring Boot 可以拾取它並將其應用於其自動配置的 WebClient.Builder bean。

在此階段,無論何時您需要一個具體的 WebClient,只需將 WebClient.Builder 注入到您的程式碼中,並使用 build()WebClient 例項將能夠使用超媒體進行互動。

6.4. 配置 WebTestClient 例項

在使用啟用超媒體的表示時,一個常見的任務是使用 WebTestClient 執行各種測試。

要在測試用例中配置 WebTestClient 例項,請檢視此示例:

示例 45. 使用 Spring HATEOAS 時配置 WebTestClient
@Test // #1225
void webTestClientShouldSupportHypermediaDeserialization() {

  // Configure an application context programmatically.
  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  context.register(HalConfig.class); (1)
  context.refresh();

  // Create an instance of a controller for testing
  WebFluxEmployeeController controller = context.getBean(WebFluxEmployeeController.class);
  controller.reset();

  // Extract the WebTestClientConfigurer from the app context.
  HypermediaWebTestClientConfigurer configurer = context.getBean(HypermediaWebTestClientConfigurer.class);

  // Create a WebTestClient by binding to the controller and applying the hypermedia configurer.
  WebTestClient client = WebTestClient.bindToApplicationContext(context).build().mutateWith(configurer); (2)

  // Exercise the controller.
  client.get().uri("https:///employees").accept(HAL_JSON) //
      .exchange() //
      .expectStatus().isOk() //
      .expectBody(new TypeReferences.CollectionModelType<EntityModel<Employee>>() {}) (3)
      .consumeWith(result -> {
        CollectionModel<EntityModel<Employee>> model = result.getResponseBody(); (4)

        // Assert against the hypermedia model.
        assertThat(model.getRequiredLink(IanaLinkRelations.SELF)).isEqualTo(Link.of("https:///employees"));
        assertThat(model.getContent()).hasSize(2);
      });
}
1 註冊使用 @EnableHypermediaSupport 啟用 HAL 支援的配置類。
2 使用 HypermediaWebTestClientConfigurer 應用超媒體支援。
3 使用 Spring HATEOAS 的 TypeReferences.CollectionModelType 助手請求 CollectionModel<EntityModel<Employee>> 響應。
4 以 Spring HATEOAS 格式獲取“body”後,對其進行斷言!
WebTestClient 是一個不可變值型別,因此您無法原地更改它。HypermediaWebClientConfigurer 返回一個已更改的變體,您必須捕獲它才能使用它。

如果您使用 Spring Boot,還有其他選項,如下所示:

示例 46. 使用 Spring Boot 時配置 WebTestClient
@SpringBootTest
@AutoConfigureWebTestClient (1)
class WebClientBasedTests {

    @Test
    void exampleTest(@Autowired WebTestClient.Builder builder, @Autowired HypermediaWebTestClientConfigurer configurer) { (2)
        client = builder.apply(configurer).build(); (3)

        client.get().uri("/") //
                .exchange() //
                .expectBody(new TypeReferences.EntityModelType<Employee>() {}) (4)
                .consumeWith(result -> {
                    // assert against this EntityModel<Employee>!
                });
    }
}
1 這是 Spring Boot 的測試註解,它將為這個測試類配置一個 WebTestClient.Builder
2 將 Spring Boot 的 WebTestClient.Builder 自動裝配到 builder 中,並將 Spring HATEOAS 的配置器作為方法引數。
3 使用 HypermediaWebTestClientConfigurer 註冊對超媒體的支援。
4 使用 TypeReferences 指示您想要返回 EntityModel<Employee>

同樣,您可以使用與前面示例類似的斷言。

還有許多其他方法來設計測試用例。WebTestClient 可以繫結到控制器、函式和 URL。本節的目的不是展示所有這些。相反,這為您提供了一些入門示例。重要的是,透過應用 HypermediaWebTestClientConfigurer,任何 WebTestClient 例項都可以被修改以處理超媒體。

6.5. 配置 RestTemplate 例項

如果您想建立自己的 RestTemplate 副本,並配置為進行超媒體通訊,您可以使用 HypermediaRestTemplateConfigurer

示例 47. 自己配置 RestTemplate
/**
 * Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}.
 */
@Bean
RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { (1)
	return configurer.registerHypermediaTypes(new RestTemplate()); (2)
}
1 在您的 @Configuration 類中,獲取 Spring HATEOAS 註冊的 HypermediaRestTemplateConfigurer bean 的副本。
2 建立 RestTemplate 後,使用配置器應用超媒體型別。

您可以將此模式應用於您需要的任何 RestTemplate 例項,無論是建立註冊的 bean,還是在您定義的服務內部。

如果您使用的是 Spring Boot,還有另一種方法。

一般來說,Spring Boot 已經放棄了在應用程式上下文中註冊 RestTemplate bean 的概念。

  • 與不同的服務通訊時,您通常需要不同的憑據。

  • RestTemplate 使用底層連線池時,您會遇到額外的問題。

  • 使用者通常需要不同的例項,而不是單個 bean。

為了彌補這一點,Spring Boot 提供了 RestTemplateBuilder。這個自動配置的 bean 允許您定義用於構建 RestTemplate 例項的各種 bean。您請求一個 RestTemplateBuilder bean,呼叫其 build() 方法,然後應用最終設定(例如憑據和其他詳細資訊)。

要註冊基於超媒體的訊息轉換器,請將以下內容新增到您的程式碼中:

示例 48. 讓 Spring Boot 配置事物
@Bean (4)
RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { (1)
    return restTemplate -> { (2)
        configurer.registerHypermediaTypes(restTemplate); (3)
    };
}
1 建立 Spring bean 時,請求 Spring HATEOAS 的 HypermediaRestTemplateConfigurer bean 的副本。
2 使用 Java 8 lambda 表示式定義一個 RestTemplateCustomizer
3 在函式呼叫內部,應用 registerHypermediaTypes 方法。
4 將整個內容作為 Spring bean 返回,以便 Spring Boot 可以拾取它並將其應用於其自動配置的 RestTemplateBuilder

在此階段,無論何時您需要一個具體的 RestTemplate,只需將 RestTemplateBuilder 注入到您的程式碼中,並使用 build()RestTemplate 例項將能夠使用超媒體進行互動。

© . This site is unofficial and not affiliated with VMware.