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

© 2012-2021 原始作者。

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

1. 前言

1.1. 遷移到 Spring HATEOAS 1.0

在 1.0 版本中,我們藉此機會重新評估了 0.x 版本中做出的一些設計和包結構選擇。我們收到了大量關於這方面的反饋,主要版本升級似乎是重構這些內容最自然的時機。

1.1.1. 變更

包結構的最大變化是由引入超媒體型別註冊 API 驅動的,該 API 用於支援 Spring HATEOAS 中的其他媒體型別。這使得客戶端和伺服器 API(分別命名為相應包)以及 mediatype 包中的媒體型別實現清晰分離。

將您的程式碼庫升級到新 API 的最簡單方法是使用 遷移指令碼。在開始之前,這裡快速瀏覽一下變更。

表示模型

ResourceSupport/Resource/Resources/PagedResources 這組類的命名從未真正讓人覺得恰當。畢竟,這些型別實際上並未體現資源本身,而是表示模型,可以利用超媒體資訊和 affordances 進行豐富。下面是新舊名稱的對映關係:

  • 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(…) 接受 Affordance 詳細資訊的方法已移至 Affordances。現在手動構建 Affordance 例項,請使用 Affordances.of(link).afford(…)。另請注意從 Affordances 中公開的新型別 AffordanceBuilder,以用於流暢的用法。詳情請參見 Affordances

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

  • 如果屬性值符合規範中定義的預設值,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 為根的類。它本質上是 Links 集合的容器,並具有方便的方法將這些 Links 新增到模型中。這些模型稍後可以渲染成各種媒體型別格式,這些格式將定義超媒體元素在表示中的樣子。有關這方面的更多資訊,請參閱 媒體型別

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

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

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

  String firstname, lastname;
}

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

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

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

示例 10. 為 person 表示模型生成的 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 被限制始終包含載荷,因此允許對單個例項上的型別 arrangement 進行推理,但 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 中引入了非複合樣式。如果今天從頭開始,我們可能會預設採用該樣式,並寧願讓使用者顯式選擇複合樣式,而不是反過來。

TODO

3.3. Affordances

環境的 affordances 是它所提供的……它提供或賦予的,無論是好的還是壞的。“afford” 這個動詞在詞典中可以找到,但名詞 “affordance” 則沒有。是我創造的。

— James J. Gibson
知覺的生態學進路 (The Ecological Approach to Visual Perception)(第 126 頁)

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

以下程式碼展示瞭如何獲取一個 self 連結並關聯另外兩個 affordances:

示例 13. 將 affordances 連線到 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 操作。想象一下,上面 affords 的相關方法看起來像這樣:

示例 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

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

示例 16. 使用 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 方法開頭。然後我們將一個型別註冊為載荷描述,並顯式命名該 affordance。後者可以省略,預設名稱將從 HTTP 方法和輸入型別名稱派生。這實際上建立了與指向 EmployeeController.newEmployee(…) 所建立的 affordance 相同的功能。
3 下一個 affordance 的構建旨在反映指向 EmployeeController.search(…) 所發生的情況。在這裡,我們將 Employee 定義為建立的響應的模型,並顯式註冊 QueryParameters。

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

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 應用程式,對應的 reactive 元件是 ForwardedHeaderTransformer

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

這將建立一個函式,用於轉換 reactive 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);

EntityLinks 透過在您的 Spring MVC 配置中啟用 @EnableHypermediaSupport 來透過依賴注入可用。這將導致註冊各種 EntityLinks 的預設實現。最基礎的一個是 ControllerEntityLinks,它檢查 Spring MVC 控制器類。如果您想註冊自己的 EntityLinks 實現,請檢視 此部分

3.5.1. 基於 Spring MVC 控制器的 EntityLinks

啟用 entity links 功能會導致檢查當前 ApplicationContext 中所有可用的 Spring MVC 控制器是否存在 @ExposesResourceFor(…) 註解。該註解暴露了控制器管理的模型型別。除此之外,我們假定您遵守以下 URI 對映設定和約定:

  • 一個型別級別的 @ExposesResourceFor(…) 註解,宣告控制器暴露集合和專案資源的實體型別。

  • 一個類級別的基本對映,代表集合資源。

  • 一個額外的方法級別對映,擴充套件該對映以將識別符號追加為額外的路徑段。

以下示例展示了 EntityLinks-capable 控制器的實現:

@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 允許構建 LinkBuilders 和 Link 例項,以指向實體型別的集合和專案資源。以 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 (primary 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(…)

然後,您可以使用匯編器來彙編一個 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" : "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. 處理空集合模型

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

3.8. 使用 LinkRelationProvider API

構建連結時,通常需要確定用於連結的關係型別 (rel)。在大多數情況下,關係型別直接與(領域)型別關聯。我們將查詢關係型別的詳細演算法封裝在 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 物件。屬性名稱是連結關係 (link relations),每個值是一個連結物件或一個連結物件陣列

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

示例 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 Linking 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 會在所有未註冊到 IANA 的 rel 值前加上 curie 字首。此外,一個 curies 連結會自動新增到 HAL 資源中。

以下配置定義了一個預設的 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 屬性的詳細資訊。閱讀Affordances API以使用這些額外元資料增強您的控制器。

至於單項 (EntityModel) 和聚合根集合 (CollectionModel),Spring HATEOAS 將它們渲染得與 HAL 文件相同。

4.2.1. 定義 HAL-FORMS 元資料

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

每個模板將定義以下屬性:

表 1. 模板屬性
屬性 描述

contentType

伺服器期望接收的媒體型別。僅當指向的控制器方法暴露了 @RequestMapping(consumes = "…") 屬性,或在設定 affordance 時明確定義了媒體型別時,才包含此屬性。

method

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

target

提交表單的目標 URI。僅當 affordance 目標與其宣告的連結不同時,才渲染此屬性。

title

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

properties

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

每個屬性將定義以下屬性:

表 2. 屬性屬性
屬性 描述

readOnly

如果屬性沒有 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

type

從顯式 @InputType 註解、JSR-303 驗證註解或屬性型別派生的 HTML input 型別。

對於您無法手動註解的型別,可以透過 application context 中存在的 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。這意味著您通常需要使用 affordance 描述的本地或完全限定的輸入型別名稱來限定鍵。

示例 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 使用實際 affordance 名稱作為鍵的全域性標題定義。除非在建立 affordance 時明確定義,否則此名稱預設為建立 affordance 時指向的方法的名稱。
3 本地定義的標題,將應用於所有名為 Employee 的型別。
4 使用完全限定型別名稱的標題定義。
使用實際 affordance 名稱的鍵優先於預設鍵。
屬性提示

屬性提示也可以透過 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 我們定義了一個 LocalDate 型別的 birthdate 屬性。
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 聲明瞭一個控制器方法,用於在使用 POST 請求到 /customers 時,將請求體繫結到上面定義的表示模型。
2 /customers 發出 GET 請求會準備一個模型,為其新增一個 self 連結,並在此連結上額外宣告一個指向對映到 POST 的控制器方法的 affordance。這將導致構建一個 affordance 模型,該模型(取決於最終要渲染的媒體型別)將被轉換為媒體型別特定的格式。

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

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 屬性指定了一個提示和佔位符。

如果客戶端現在使用 Acceptapplication/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,因為它定義的唯一模板,規範要求使用該名稱。如果附加了多個模板(透過宣告額外的 affordances),它們將分別以它們指向的方法命名。
2 模板標題源自資源包中定義的值。請注意,根據隨請求傳送的 Accept-Language 頭和可用性,可能會返回不同的值。
3 method 屬性的值源自 derive affordance 的方法的對映。
4 type 屬性的值 text 源自屬性的型別 Stringbirthdate 屬性也同樣適用,但結果是 date
5 ccn 屬性的提示和佔位符也源自資源包。
6 ccn 屬性的 @Pattern 宣告暴露為模板屬性的 regex 屬性。
7 email 屬性上的 @Email 註解已轉換為相應的 type 值。

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

4.3. HTTP 問題詳情

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

HTTP Problem Details 定義了一組 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 您可以為媒體型別定義的預設屬性定義值,例如使用 Spring 的國際化功能(見上文)定義型別 URI、標題和詳細資訊。
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 {

}

此配置將使您的應用程式響應帶有 Acceptapplication/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 - Uniform Basis for Exchanging Representations

UBER 是一種實驗性的 JSON 規範。

UBER 文件格式是一種極簡的讀/寫超媒體型別,旨在支援簡單的狀態轉移和特設的基於超媒體的轉換。

—— Mike Amundsen
UBER 規範

UBER 提供了一種統一的方式來表示單項資源和集合。要啟用此媒體型別,請在您的程式碼中新增以下配置:

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

}

此配置將使您的應用程式響應使用 Acceptapplication/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 - Application-Level Profile Semantics

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 透過掃描應用上下文(application context)來查詢 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 模組通常宣告用於表示模型型別 RepresentationModelEntityModelCollectionModelPagedModelSerializerDeserializer 實現。如果需要進一步定製 Jackson ObjectMapper(例如自定義的 HandlerInstantiator),您可以選擇覆蓋 configureObjectMapper(…​) 方法。

以前版本的參考文件提到過實現 MediaTypeConfigurationProvider 介面並將其註冊到 spring.factories 檔案中。這不是必需的。此 SPI 僅用於 Spring HATEOAS 提供的開箱即用的媒體型別。只需實現 HypermediaMappingInformation 介面並將其註冊為一個 Spring bean 即可。

4.8.2. 建議

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

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

5. 配置

本節介紹如何配置 Spring HATEOAS。

5.1. 使用 @EnableHypermediaSupport

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

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

  • 如果 classpath 中存在 JSONPath,它會自動註冊一個 LinkDiscoverer 例項,以便在純 JSON 表示中按其 rel 查詢連結(參見 使用 LinkDiscoverer 例項)。

  • 預設情況下,它會啟用 entity links,並自動查詢 EntityLinks 實現並將它們捆綁到一個 DelegatingEntityLinks 例項中,您可以對其進行 autowire。

  • 它自動查詢 ApplicationContext 中的所有 RelProvider 實現,並將它們捆綁到一個 DelegatingRelProvider 例項中,您可以對其進行 autowire。它註冊提供者來考慮域型別上的 @Relation 以及 Spring MVC 控制器。如果 classpath 中存在 EVO inflector,集合 rel 值將使用該庫中實現的複數化演算法推導得到(參見 [spis.rel-provider])。

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

預設情況下,@EnableHypermediaSupport 會透過反射檢測您正在使用的 Web 應用程式棧,並掛接到為這些棧註冊的 Spring 元件中,以啟用對超媒體表示的支援。但是,在某些情況下,您可能只想顯式啟用對特定棧的支援。例如,如果您的基於 Spring WebMVC 的應用程式使用 WebFlux 的 WebClient 發起對外請求,並且該 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");

您可以設定一個 Traverson 例項,透過將其指向一個 REST 伺服器並配置您想作為 Accept 頭髮送的媒體型別。然後,您可以定義您想發現和跟蹤的關係名(relation names)。關係名可以是簡單的名稱,也可以是 JSONPath 表示式(以 $ 開頭)。

然後,該示例將一個引數 map 傳遞給 Traverson 例項。這些引數用於展開在遍歷過程中找到的 URI(這些 URI 是模板化的)。遍歷透過訪問最終遍歷結果的表示來結束。在前面的示例中,我們評估一個 JSONPath 表示式來訪問 actor 的名字。

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

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

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);

您還可以載入整個引數 Map,透過使用 .withParameters(Map) 方法。

follow() 方法支援鏈式呼叫,這意味著您可以串聯多個 hop,如前面的示例所示。您可以放入多個基於字串的 rel 值(follow("items", "item")),或者一個帶有特定引數的單個 hop。

6.1.1. EntityModel<T>CollectionModel<T>

到目前為止展示的示例演示瞭如何規避 Java 的型別擦除,並將單個 JSON 格式的資源轉換為一個 EntityModel<Item> 物件。但是,如果您得到一個像 \_embedded HAL 集合那樣的集合怎麼辦?您只需稍作調整即可實現,如下面的示例所示

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

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

與獲取單個資源不同,這個示例將一個集合反序列化到 CollectionModel 中。

在使用支援超媒體的表示時,一個常見的任務是查詢包含特定關係型別(relation type)的連結。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 後,使用該 configurer 來註冊超媒體型別。
HypermediaWebClientConfigurer 的作用是向 WebClient.Builder 註冊所有正確的編碼器和解碼器。要使用它,您需要將該 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 請求一個型別為 CollectionModel<EntityModel<Employee>> 的響應,使用 Spring HATEOAS 的 TypeReferences.CollectionModelType 幫助類。
4 在獲取到 Spring HATEOAS 格式的“body”後,對其進行斷言!
WebTestClient 是一個不可變的值型別(immutable value type),因此您不能原地修改它。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 autowire 到 builder 中,並將 Spring HATEOAS 的 configurer 作為方法引數。
3 使用 HypermediaWebTestClientConfigurer 來註冊對超媒體的支援。
4 表明您希望返回一個 EntityModel<Employee>,使用 TypeReferences

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

還有許多其他方法來構建測試用例。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 後,使用該 configurer 來應用超媒體型別。

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

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

通常,Spring Boot 已經不再推薦在應用上下文(application context)中註冊 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 例項將能夠使用超媒體進行互動。