此專案提供一些 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
套件。 -
LinkBuilder
和EntityLinks
API 已移至server
套件。 -
ControllerLinkBuilder
已移至server.mvc
並已棄用,將由WebMvcLinkBuilder
取代。 -
RelProvider
已重新命名為LinkRelationProvider
,並傳回LinkRelation
實例而不是String
。 -
VndError
已移至mediatype.vnderror
套件。
1.1.2. 遷移腳本
您可以找到腳本,從您的應用程式根目錄執行,它將更新所有匯入語句和對 Spring HATEOAS 類型的靜態方法引用,這些類型在我們的原始碼儲存庫中已移動。只需下載它,從您的專案根目錄執行它即可。預設情況下,它將檢查所有 Java 原始碼檔案,並將舊版 Spring HATEOAS 類型引用替換為新的類型引用。
$ ./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
-
取得 Affordance 詳細資訊的
Link.andAffordance(…)
已移至Affordances
。若要手動建立Affordance
實例,現在請使用Affordances.of(link).afford(…)
。另請注意從Affordances
公開的新AffordanceBuilder
類型,以便流暢使用。請參閱意符以取得詳細資訊。 -
AffordanceModelFactory.getAffordanceModel(…)
現在接收InputPayloadMetadata
和PayloadMetadata
實例,而不是ResolvableType
,以允許非基於類型的實作。自訂媒體類型實作必須相應地進行調整。 -
HAL 表單現在不會呈現屬性屬性,如果其值符合規格中定義為預設值的值。也就是說,如果先前將
required
明確設定為false
,我們現在只會省略required
的條目。我們現在也只強制它們對於使用PATCH
作為 HTTP 方法的範本來說是非必要的。
2. 基礎知識
本節涵蓋 Spring HATEOAS 的基礎知識及其基本網域抽象。
2.1. 連結
超媒體的基本概念是使用超媒體元素豐富資源的表示形式。最簡單的形式是連結。它們向客户端指示它可以導航到特定資源。相關資源的語義在所謂的連結關係中定義。您可能已經在 HTML 檔案的標頭中看到過這個
<link href="theme.css" rel="stylesheet" type="text/css" />
如您所見,連結指向資源 theme.css
,並指示它是一個樣式表。連結通常攜帶額外資訊,例如所指向的資源將傳回的媒體類型。但是,連結的基本建構區塊是其參考和關係。
Spring HATEOAS 允許您透過其不可變的 Link
值類型來處理連結。其建構函式同時採用超文本參考和連結關係,後者預設為 IANA 連結關係 self
。在連結關係中閱讀更多關於後者的資訊。
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 的結構,它只需要知道變數的名稱。
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 模板可以手動建構,模板變數可以在稍後新增。
UriTemplate template = UriTemplate.of("/{segment}/something")
.with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM);
assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");
2.3. 連結關係
為了指示目標資源與目前資源的關係,使用了所謂的連結關係。Spring HATEOAS 提供了 LinkRelation
類型,以便輕鬆建立基於 String
的實例。
2.3.1. IANA 連結關係
網際網路指派號碼管理機構包含一組預先定義的連結關係。它們可以透過 IanaLinkRelations
引用。
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
集合的容器,並且具有將它們新增至模型的便捷方法。這些模型稍後可以呈現為各種媒體類型格式,這些格式將定義超媒體元素在表示形式中的外觀。有關此的更多資訊,請參閱媒體類型。
RepresentationModel
類別階層使用 RepresentationModel
的預設方式是建立其子類別,以包含表示形式應包含的所有屬性,建立該類別的實例,填入屬性並使用連結豐富它。
class PersonModel extends RepresentationModel<PersonModel> {
String firstname, lastname;
}
泛型自類型是必要的,以讓 RepresentationModel.add(…)
傳回自身的實例。模型類型現在可以像這樣使用
PersonModel model = new PersonModel();
model.firstname = "Dave";
model.lastname = "Matthews";
model.add(Link.of("https://myhost/people/42"));
如果您從 Spring MVC 或 WebFlux 控制器傳回這樣的實例,並且客户端傳送了設定為 application/hal+json
的 Accept
標頭,則回應將如下所示
{
"_links" : {
"self" : {
"href" : "https://myhost/people/42"
}
},
"firstname" : "Dave",
"lastname" : "Matthews"
}
2.4.1. 項目資源表示模型
對於由單一物件或概念支援的資源,存在便捷的 EntityModel
類型。您可以重複使用現有類型,並將其實例包裝到 EntityModel
中,而不是為每個概念建立自訂模型類型。
EntityModel
包裝現有物件Person person = new Person("Dave", "Matthews");
EntityModel<Person> model = EntityModel.of(person);
2.4.2. 集合資源表示模型
對於概念上是集合的資源,可以使用 CollectionModel
。其元素可以是簡單物件,也可以是反過來的 RepresentationModel
實例。
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. 伺服器端支援
3.1. 在 Spring MVC 中建立連結
現在我們已經有了網域詞彙,但主要挑戰仍然存在:如何以較不易損壞的方式建立要包裝到 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 基礎和其他值。這很麻煩,並且需要醜陋的手動字串串連程式碼。
-
您可能不想將
/people
串連到您的基礎 URI 之上,因為這樣您就必須在多個位置維護資訊。如果您變更對應,則必須變更指向它的所有客户端。
Spring HATEOAS 現在提供了一個 WebMvcLinkBuilder
,可讓您透過指向控制器類別來建立連結。以下範例示範如何執行此操作
Link link = linkTo(PersonController.class).withRel("people");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("people"));
assertThat(link.getHref()).endsWith("/people");
WebMvcLinkBuilder
在幕後使用 Spring 的 ServletUriComponentsBuilder
從目前的請求中取得基本 URI 資訊。假設您的應用程式在 localhost:8080/your-app
上執行,這正是您在之上建構額外部分的 URI。建構器現在檢查給定的控制器類別的根對應,因此最終得到 localhost:8080/your-app/people
。您也可以建立更巢狀的連結。以下範例示範如何執行此操作
Person person = new Person(1L, "Dave", "Matthews");
// /person / 1
Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel();
assertThat(link.getRel(), is(IanaLinkRelation.SELF.value()));
assertThat(link.getHref(), endsWith("/people/1"));
建構器也允許建立 URI 實例以進行建構(例如,回應標頭值)
HttpHeaders headers = new HttpHeaders();
headers.setLocation(linkTo(PersonController.class).slash(person).toUri());
return new ResponseEntity<PersonModel>(headers, HttpStatus.CREATED);
3.1.1. 建立指向方法的連結
您甚至可以建立指向方法的連結,或建立虛擬控制器方法調用。第一種方法是將 Method
實例傳遞給 WebMvcLinkBuilder
。以下範例示範如何執行此操作
Method method = PersonController.class.getMethod("show", Long.class);
Link link = linkTo(method, 2L).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2"));
這仍然有點令人不滿意,因為我們必須首先取得 Method
實例,這會擲回例外狀況,而且通常非常麻煩。至少我們不會重複對應。更好的方法是在控制器 Proxy 上虛擬方法調用目標方法,我們可以透過使用 methodOn(…)
輔助程式來建立該 Proxy。以下範例示範如何執行此操作
Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2");
methodOn(…)
建立控制器類別的 Proxy,該 Proxy 記錄方法調用並在為方法傳回類型建立的 Proxy 中公開它。這允許流暢地表示我們要取得對應的方法。但是,可以使用此技術取得的方法有一些限制
-
傳回類型必須能夠 Proxy,因為我們需要在其上公開方法調用。
-
傳遞到方法中的參數通常被忽略(透過
@PathVariable
參考的參數除外,因為它們構成 URI)。
控制請求參數的呈現方式
集合值請求參數實際上可以透過兩種不同的方式具體化。URI 模板規格列出了呈現它們的複合方式,該方式為每個值重複參數名稱 (param=value1¶m=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. 意符
環境的意符是它提供的東西… 它為好或壞提供的或提供的東西。動詞「to afford」在字典中可以找到,但名詞「affordance」則沒有。是我創造了它。
視覺感知的生態方法(第 126 頁)
基於 REST 的資源不僅提供資料,還提供控制項。形成彈性服務的最後一個要素是關於如何使用各種控制項的詳細意符。由於意符與連結相關聯,Spring HATEOAS 提供了一個 API,可將盡可能多的相關方法附加到連結。正如您可以透過指向 Spring MVC 控制器方法來建立連結一樣(有關詳細資訊,請參閱在 Spring MVC 中建立連結),您…
以下程式碼示範如何取得 self 連結並關聯另外兩個意符
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
運算。想像一下,上面 afforded 的相關方法看起來像這樣
PUT /employees/{id}
的 updateEmpoyee
方法@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee( //
@RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
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 來實現
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 。 |
意符由媒體類型特定的意符模型支援,這些模型將一般意符元數據轉換為特定的表示形式。請務必查看 媒體類型 區段中關於意符的章節,以找到關於如何控制該元數據公開的更多詳細資訊。
3.4. 轉發標頭處理
RFC-7239 轉發標頭在您的應用程式位於 Proxy、負載平衡器或雲端之後時最常用。實際接收 Web 請求的節點是基礎架構的一部分,並將請求轉發到您的應用程式。
您的應用程式可能在 localhost:8080
上執行,但在外部世界,您應該位於 reallycoolsite.com
(以及 Web 的標準埠 80)。透過讓 Proxy 包含額外標頭(許多 Proxy 已經這樣做),Spring HATEOAS 可以正確產生連結,因為它使用 Spring Framework 功能來取得原始請求的基礎 URI。
任何可以根據外部輸入變更根 URI 的內容都必須受到適當的保護。這就是為什麼預設情況下,轉發標頭處理是停用的。您必須啟用它才能運作。如果您要部署到雲端或部署到您控制 Proxy 和負載平衡器的設定中,那麼您肯定會想要使用此功能。 |
若要啟用轉發標頭處理,您需要在您的應用程式中為 Spring MVC 註冊 Spring 的 ForwardedHeaderFilter
(詳細資訊在此處)或為 Spring WebFlux 註冊 ForwardedHeaderTransformer
(詳細資訊在此處)。在 Spring Boot 應用程式中,這些元件可以簡單地宣告為 Spring bean,如此處所述。
ForwardedHeaderFilter
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
這將建立一個 Servlet 篩選器,用於處理所有 X-Forwarded-…
標頭。它將使用 Servlet 處理程式正確註冊它。
對於 Spring WebFlux 應用程式,反應式對應物是 ForwardedHeaderTransformer
ForwardedHeaderTransformer
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
這將建立一個轉換反應式 Web 請求的函數,處理 X-Forwarded-…
標頭。它將使用 WebFlux 正確註冊它。
透過如上所示的設定,傳遞 X-Forwarded-…
標頭的請求將看到這些標頭反映在產生的連結中
X-Forwarded-…
標頭的請求curl -v localhost:8080/employees \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Host: example.com' \
-H 'X-Forwarded-Port: 9001'
{
"_embedded": {
"employees": [
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "https://example.com:9001/employees/1"
},
"employees": {
"href": "https://example.com:9001/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "https://example.com:9001/employees"
},
"root": {
"href": "https://example.com:9001"
}
}
}
3.5. 使用 EntityLinks 介面
EntityLinks 及其各種實作目前未針對 Spring WebFlux 應用程式提供現成可用的功能。EntityLinks SPI 中定義的合約最初是針對 Spring Web MVC,並且不考量 Reactor 類型。支援反應式程式設計的可比較合約仍在開發中。 |
到目前為止,我們已經透過指向 Web Framework 實作(即 Spring MVC 控制器)並檢查對應來建立連結。在許多情況下,這些類別基本上讀取和寫入由模型類別支援的表示形式。
EntityLinks
介面現在公開了一個 API,用於根據模型類型查閱 Link
或 LinkBuilder
。這些方法基本上傳回指向集合資源(例如 /people
)或項目資源(例如 /people/1
)的連結。以下範例示範如何使用 EntityLinks
EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);
EntityLinks
可透過在您的 Spring MVC 設定中啟用 @EnableHypermediaSupport
透過依賴注入來使用。這將導致註冊各種 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
允許為實體類型的集合和項目資源建立 LinkBuilder
和 Link
實例。以 linkFor…
開頭的方法將為您產生 LinkBuilder
實例,以供您擴展和使用額外路徑區段、參數等來擴充。以 linkTo
開頭的方法產生完全準備好的 Link
實例。
雖然對於集合資源來說,提供實體類型就足夠了,但項目資源的連結將需要提供識別碼。這通常看起來像這樣
entityLinks.linkToItemResource(order, order.getId());
如果您發現自己不斷重複呼叫這些方法,則可以將識別符提取步驟提取到可重複使用的 Function
中,以便在不同的調用中重複使用
Function<Order, Object> idExtractor = Order::getId; (1)
entityLinks.linkToItemResource(order, idExtractor); (2)
1 | 識別符提取已外部化,因此可以保存在欄位或常數中。 |
2 | 使用提取器的連結查找。 |
TypedEntityLinks
由於控制器實作通常圍繞實體類型分組,因此您會經常發現自己在整個控制器類別中使用相同的提取器函數(詳細資訊請參閱EntityLinks API 詳情)。我們可以透過取得提供提取器一次的 TypedEntityLinks
實例,更進一步集中識別符提取邏輯,以便實際查找不再需要處理提取。
class OrderController {
private final TypedEntityLinks<Order> links;
OrderController(EntityLinks entityLinks) { (1)
this.links = entityLinks.forType(Order::getId); (2)
}
@GetMapping
ResponseEntity<Order> someMethod(…) {
Order order = … // lookup order
Link link = links.linkToItemResource(order); (3)
}
}
1 | 注入 EntityLinks 實例。 |
2 | 表明您將使用特定的識別符提取器函數查找 Order 實例。 |
3 | 根據單一 Order 實例查找項目資源連結。 |
3.5.3. EntityLinks 作為 SPI
由 @EnableHypermediaSupport
建立的 EntityLinks
實例類型為 DelegatingEntityLinks
,它反過來會挑選 ApplicationContext
中作為 bean 提供的所有其他 EntityLinks
實作。它被註冊為主要 bean,因此當您通常注入 EntityLinks
時,它始終是唯一的注入候選者。ControllerEntityLinks
是預設實作,將包含在設定中,但使用者可以自由實作和註冊自己的實作。讓這些實作可供注入的 EntityLinks
實例使用,只需將您的實作註冊為 Spring bean 即可。
@Configuration
class CustomEntityLinksConfiguration {
@Bean
MyEntityLinks myEntityLinks(…) {
return new MyEntityLinks(…);
}
}
此機制可擴展性的一個範例是 Spring Data REST 的 RepositoryEntityLinks
,它使用儲存庫映射資訊來建立指向由 Spring Data 儲存庫支援的資源的連結。同時,它甚至為其他類型的資源公開了額外的查找方法。如果您想使用這些方法,只需顯式注入 RepositoryEntityLinks
即可。
3.6. 表現模型組裝器
由於從實體到表現模型的映射必須在多個地方使用,因此建立一個專門的類別來負責執行此操作是有意義的。轉換包含非常客製化的步驟,但也包含一些樣板步驟
-
模型類別的實例化
-
新增
rel
為self
的連結,指向要呈現的資源。
Spring HATEOAS 現在提供了一個 RepresentationModelAssemblerSupport
基底類別,可協助減少您需要編寫的程式碼量。以下範例示範如何使用它
class PersonModelAssembler extends RepresentationModelAssemblerSupport<Person, PersonModel> {
public PersonModelAssembler() {
super(PersonController.class, PersonModel.class);
}
@Override
public PersonModel toModel(Person person) {
PersonModel resource = createResource(person);
// … do further mapping
return resource;
}
}
createResource(…) 是您編寫的程式碼,用於在給定 Person 物件的情況下實例化 PersonModel 物件。它應該只專注於設定屬性,而不是填充 Links 。 |
按照前一個範例中的方式設定類別,您可以獲得以下好處
-
有一些
createModelWithId(…)
方法可讓您建立資源的實例,並將rel
為self
的Link
新增至其中。該連結的 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" : "http://localhost/orders/999"
}
}
}
您希望新增一個連結,以便客戶可以付款,但不希望將有關 PaymentController
的詳細資訊混入 OrderController
中。您可以編寫一個像這樣的 RepresentationModelProcessor
,而不是污染您的訂購系統的詳細資訊
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> { (1)
@Override
public EntityModel<Order> process(EntityModel<Order> model) {
model.add( (2)
Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) //
.expand(model.getContent().getOrderId()));
return model; (3)
}
}
1 | 此處理器將僅應用於 EntityModel<Order> 物件。 |
2 | 透過新增無條件連結來操作現有的 EntityModel 物件。 |
3 | 傳回 EntityModel ,以便可以將其序列化為請求的媒體類型。 |
向您的應用程式註冊處理器
@Configuration
public class PaymentProcessingApp {
@Bean
PaymentProcessor paymentProcessor() {
return new PaymentProcessor();
}
}
現在,當您發出 Order
的超媒體表現時,客戶會收到以下內容
{
"orderId" : "42",
"state" : "AWAITING_PAYMENT",
"_links" : {
"self" : {
"href" : "http://localhost/orders/999"
},
"payments" : { (1)
"href" : "/payments/42" (2)
}
}
}
1 | 您會看到 LinkRelation.of("payments") 作為此連結的關係插入。 |
2 | URI 由處理器提供。 |
此範例非常簡單,但您可以輕鬆地
-
使用
WebMvcLinkBuilder
或WebFluxLinkBuilder
來建構指向您的PaymentController
的動態連結。 -
注入所需的任何服務以有條件地新增由狀態驅動的其他連結(例如
cancel
、amend
)。 -
利用跨領域服務(如 Spring Security)根據目前使用者的上下文新增、移除或修改連結。
此外,在此範例中,PaymentProcessor
會更改提供的 EntityModel<Order>
。您也可以替換為另一個物件。請注意,API 要求傳回類型必須等於輸入類型。
3.7.1. 處理空集合模型
為了找到要為 RepresentationModel
實例調用的正確 RepresentationModelProcessor
實例集,調用基礎架構會對已註冊的 RepresentationModelProcessor
的泛型宣告執行詳細分析。對於 CollectionModel
實例,這包括檢查底層集合的元素,因為在運行時,唯一的模型實例不會公開泛型資訊(由於 Java 的類型擦除)。這表示,預設情況下,RepresentationModelProcessor
實例不會針對空集合模型調用。為了仍然允許基礎架構正確推斷有效負載類型,您可以從一開始就使用顯式回退有效負載類型初始化空 CollectionModel
實例,或透過呼叫 CollectionModel.withFallbackType(…)
註冊它。有關詳細資訊,請參閱 集合資源表現模型。
3.8. 使用 LinkRelationProvider
API
在建立連結時,您通常需要決定要用於連結的關係類型。在大多數情況下,關係類型直接與(網域)類型關聯。我們將查找關係類型的詳細演算法封裝在 LinkRelationProvider
API 後面,讓您可以決定單一和集合資源的關係類型。查找關係類型的演算法如下
-
如果類型使用
@Relation
註解,我們將使用註解中配置的值。 -
否則,我們預設為未大寫的簡單類別名稱加上附加的
List
作為集合rel
。 -
如果 EVO inflector JAR 在類別路徑中,我們將使用複數化演算法提供的單一資源
rel
的複數形式。 -
使用
@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
實例。這些是其基本假設
-
HAL 表現可以由任意物件(實體)支援,該物件建構表現中包含的網域欄位。
-
表現可以透過各種嵌入式文件來豐富,這些文件可以是任意物件或 HAL 表現本身(即包含巢狀嵌入式和連結)。
-
某些 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 | customer 連結透過 ….preview(…).forLink(…) 透明地新增。 |
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 物件。屬性名稱是 連結關係,每個值都是 連結物件或連結物件陣列。
對於給定連結關係具有兩個或多個連結的情況,規格清楚地說明了表現
{
"_links": {
"item": [
{ "href": "https://myhost/cart/42" },
{ "href": "https://myhost/inventory/12" }
]
},
"customer": "Dave Matthews"
}
但是,如果給定關係只有一個連結,則規格不明確。您可以將其呈現為單一物件或單一項目陣列。
預設情況下,Spring HATEOAS 使用最簡潔的方法,並像這樣呈現單一連結關係
{
"_links": {
"item": { "href": "https://myhost/inventory/12" }
},
"customer": "Dave Matthews"
}
有些使用者不希望在使用 HAL 時在陣列和物件之間切換。他們更喜歡這種呈現類型
{
"_links": {
"item": [{ "href": "https://myhost/inventory/12" }]
},
"customer": "Dave Matthews"
}
如果您希望自訂此策略,您只需將 HalConfiguration
bean 注入到您的應用程式配置中即可。有多種選擇。
@Bean
public HalConfiguration globalPolicy() {
return new HalConfiguration() //
.withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 | 覆寫 Spring HATEOAS 的預設值,方法是將所有單一連結關係呈現為陣列。 |
如果您只想覆寫某些特定的連結關係,您可以建立像這樣的 HalConfiguration
bean
@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 樣式路徑模式
@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
,如下所示
rest-messages.properties
的範例_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout
這將產生以下 HAL 表現
{
"_links" : {
"cancel" : {
"href" : "…"
"title" : "Cancel order"
},
"payment" : {
"href" : "…"
"title" : "Proceed to checkout"
}
}
}
4.1.4. 使用 CurieProvider
API
Web Linking RFC 描述了已註冊和擴展連結關係類型。已註冊的 rel 是在 IANA 連結關係類型註冊表中註冊的眾所周知的字串。擴展 rel
URI 可以由不希望註冊關係類型的應用程式使用。每個 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
會將所有 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 規格
若要啟用此媒體類型,請將以下配置放入您的程式碼中
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {
}
每當客戶提供 Accept
標頭,其中包含 application/prs.hal-forms+json
時,您可以預期類似這樣的內容
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost: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 允許透過塑造輸入和輸出類型的模型類型並在其上使用註解來自訂這些準則。
每個範本都將定義以下屬性
屬性 | 描述 |
---|---|
|
伺服器預期接收的媒體類型。僅當指向的控制器方法公開 |
|
提交範本時要使用的 HTTP 方法。 |
|
提交表單的目標 URI。僅當 affordance 目標與宣告它的連結不同時才會呈現。 |
|
顯示範本時的人類可讀標題。 |
|
要與表單一起提交的所有屬性(請參閱下文)。 |
每個屬性都將定義以下屬性
屬性 | 描述 |
---|---|
|
如果屬性沒有 setter 方法,則設定為 |
|
可以使用 JSR-303 的 |
|
可以使用 JSR-303 的 |
|
屬性允許的最大值。從 JSR-303 的 |
|
屬性允許的最大長度值。從 Hibernate Validator 的 |
|
屬性允許的最小值。從 JSR-303 的 |
|
屬性允許的最小長度值。從 Hibernate Validator 的 |
|
提交表單時要從中選取值的選項。有關詳細資訊,請參閱 定義屬性的 HAL-FORMS 選項。 |
|
呈現表單輸入時要使用的人類可讀提示。有關詳細資訊,請參閱 屬性提示。 |
|
人類可讀的預留位置,提供預期格式的範例。定義這些預留位置的方式遵循 屬性提示,但使用字尾 |
|
從顯式 |
對於您無法手動註解的類型,您可以透過應用程式上下文中的 HalFormsConfiguration
bean 註冊自訂模式。
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
}
}
此設定將導致 CreditCardNumber
類型的表現模型屬性的 HAL-FORMS 範本宣告一個 regex
欄位,其值為 [0-9]{16}
。
定義屬性的 HAL-FORMS 選項
對於其值應符合特定超集值的屬性,HAL-FORMS 在屬性定義中定義了 options
子文件。可以使用 HalFormsConfiguration
的 withOptions(…)
(採用指向類型屬性的指標和建立器函數以將 PropertyMetadata
轉換為 HalFormsOptions
實例)來描述適用於特定屬性的選項。
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.withOptions(Order.class, "shippingMethod" metadata ->
HalFormsOptions.inline("FedEx", "DHL"));
}
}
請參閱我們如何設定選項值 FedEx
和 DHL
作為 Order.shippingMethod
屬性要從中選取的選項。或者,HalFormsOptions.remote(…)
可以指向遠端資源,以動態提供值。有關選項設定的更多限制,請參閱 規格 或 HalFormsOptions
的 Javadoc。
4.2.2. 表單屬性的國際化
HAL-FORMS 包含旨在供人類解釋的屬性,例如範本的標題或屬性提示。可以使用 Spring 的資源包支援和 Spring HATEOAS 預設配置的 rest-messages
資源包來定義這些屬性並將其國際化。
範本標題
若要定義範本標題,請使用以下模式:_templates.$affordanceName.title
。請注意,在 HAL-FORMS 中,如果範本是唯一的,則範本的名稱為 default
。這表示您通常需要使用 affordance 描述的局部或完整限定的輸入類型名稱來限定索引鍵。
_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
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.Employee 的 firstName 屬性將獲指派提示「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 | 我們定義 email 為使用 JSR-303 @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 | 對 /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 屬性提供提示和預留位置。 |
如果客戶現在使用 application/prs.hal-forms+json
的 Accept
標頭向 /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 ,因為它是唯一定義的範本,並且規格要求使用該名稱。如果附加了多個範本(透過宣告額外的 affordance),它們將分別以它們指向的方法命名。 |
2 | 範本標題是從資源包中定義的值衍生的。請注意,根據與請求一起傳送的 Accept-Language 標頭和可用性,可能會傳回不同的值。 |
3 | method 屬性的值是從 affordance 衍生的方法的映射衍生的。 |
4 | type 屬性的值 text 是從屬性的類型 String 衍生的。這同樣適用於 birthdate 屬性,但結果為 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 回應
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());
這將產生如下所示的回應
{
"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 是一個向 IANA 註冊的 JSON 規格,其核准的媒體類型為 application/vnd.collection+json
。
Collection+JSON 是一種基於 JSON 的讀/寫超媒體類型,旨在支援簡單集合的管理和查詢。
Collection+JSON 規格
Collection+JSON 提供了一種統一的方式來表示單一項目資源和集合。若要啟用此媒體類型,請在您的程式碼中加入以下組態
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {
}
此組態將使您的應用程式回應 Accept
標頭為 application/vnd.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 呈現
|
當呈現資源集合時,文件幾乎相同,只是 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 文件格式是一種最小化的讀/寫超媒體類型,旨在支援簡單的狀態傳輸和特別的基於超媒體的轉換。
UBER 規格
UBER 提供了一種統一的方式來表示單一項目資源和集合。若要啟用此媒體類型,請在您的程式碼中加入以下組態
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {
}
此組態將使您的應用程式使用 Accept
標頭 application/vnd.amundsen-uber+json
回應請求,如下所示
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "self" ],
"url" : "/employees/1"
}, {
"name" : "employee",
"data" : [ {
"name" : "role",
"value" : "ring bearer"
}, {
"name" : "name",
"value" : "Frodo"
} ]
} ]
}
}
此媒體類型仍在開發中,規格本身也是如此。如果您在使用時遇到問題,請隨時開啟一個 issue。
UBER 媒體類型與乘車共享公司 Uber Technologies Inc. 没有任何關聯。 |
4.6. ALPS - Application-Level Profile Semantics
ALPS 是一種媒體類型,用於提供關於另一個資源的基於配置文件的元數據。
ALPS 文件可以用作配置文件,以解釋使用應用程式不可知的媒體類型(例如 HTML、HAL、Collection+JSON、Siren 等)的文件之應用程式語意。這提高了跨媒體類型之配置文件文件的可重用性。
ALPS 規格
ALPS 不需要特殊的啟用。相反地,您可以「建構」一個 Alps
記錄,並從 Spring MVC 或 Spring WebFlux 的 Web 方法中傳回它,如下所示
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
{ "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
<dependency>
<groupId>com.toedter</groupId>
<artifactId>spring-hateoas-jsonapi</artifactId>
<version>{see project page for current version}</version>
</dependency>
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'
如果您想要快照版本,請造訪專案頁面以取得更多詳細資訊。
4.7.2. Siren
-
媒體類型指定:
application/vnd.siren+json
-
專案負責人:Ingo Griebsch
<dependency>
<groupId>de.ingogriebsch.hateoas</groupId>
<artifactId>spring-hateoas-siren</artifactId>
<version>{see project page for current version}</version>
<scope>compile</scope>
</dependency>
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'
4.8. 註冊自訂媒體類型
Spring HATEOAS 允許您透過 SPI 整合自訂媒體類型。此類實作的建構區塊為
-
Jackson
ObjectMapper
的某種形式的自訂。在最簡單的情況下,這是一個 JacksonModule
實作。 -
一個
LinkDiscoverer
實作,以便客戶端支援能夠偵測表示形式中的連結。 -
一小部分的基礎架構組態,可讓 Spring HATEOAS 找到自訂實作並將其選取。
4.8.1. 自訂媒體類型組態
Spring HATEOAS 透過掃描應用程式內容中 HypermediaMappingInformation
介面的任何實作來選取自訂媒體類型實作。每個媒體類型都必須實作此介面,才能
-
應用於
WebClient
、WebTestClient
或RestTemplate
實例。 -
支援從 Spring Web MVC 和 Spring WebFlux 控制器提供該媒體類型。
定義您自己的媒體類型可以像這樣簡單
@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 模組通常為表示模型類型 RepresentationModel
、EntityModel
、CollectionModel
和 PagedModel
宣告 Serializer
和 Deserializer
實作。如果您需要進一步自訂 Jackson ObjectMapper
(例如自訂 HandlerInstantiator
),您可以選擇覆寫 configureObjectMapper(…)
。
先前版本的參考文件曾提及實作 |
4.8.2. 建議
實作媒體類型表示形式的首選方法是提供一個符合預期格式的類型層次結構,並且可以由 Jackson 原樣序列化。在為 RepresentationModel
註冊的 Serializer
和 Deserializer
實作中,將實例轉換為媒體類型特定的模型類型,然後查找這些類型的 Jackson 序列化器。
預設支援的媒體類型使用與第三方實作相同的組態機制。因此,值得研究 mediatype
套件中的實作。請注意,內建的媒體類型實作將其組態類別保持為套件私有,因為它們是透過 @EnableHypermediaSupport
啟用的。自訂實作可能應該將這些類別設為公開,以確保使用者可以從其應用程式套件匯入這些組態類別。
5. 組態
本節說明如何組態 Spring HATEOAS。
5.1. 使用 @EnableHypermediaSupport
若要讓 RepresentationModel
子類型根據各種超媒體表示類型規格呈現,您可以透過 @EnableHypermediaSupport
啟用對特定超媒體表示格式的支援。註解採用 HypermediaType
列舉作為其引數。目前,我們支援 HAL 以及預設呈現。使用註解會觸發以下操作
-
它註冊必要的 Jackson 模組,以超媒體特定格式呈現
EntityModel
和CollectionModel
。 -
如果 JSONPath 在類別路徑中,它會自動註冊一個
LinkDiscoverer
實例,以在純 JSON 表示形式中按其rel
查找連結(請參閱使用LinkDiscoverer
實例)。 -
預設情況下,它啟用實體連結,並自動選取
EntityLinks
實作,並將它們捆綁到您可以自動注入的DelegatingEntityLinks
實例中。 -
它會自動選取
ApplicationContext
中的所有RelProvider
實作,並將它們捆綁到您可以自動注入的DelegatingRelProvider
中。它註冊提供者以考慮網域類型和 Spring MVC 控制器上的@Relation
。如果 EVO inflector 在類別路徑中,則集合rel
值是透過使用程式庫中實作的複數化演算法衍生的(請參閱[spis.rel-provider])。
5.1.1. 明確啟用對專用 Web 堆疊的支援
預設情況下,@EnableHypermediaSupport
將反射性地偵測您正在使用的 Web 應用程式堆疊,並掛鉤到為這些堆疊註冊的 Spring 組件,以啟用對超媒體表示形式的支援。但是,在某些情況下,您可能只想明確地啟用對特定堆疊的支援。例如,如果您的基於 Spring WebMVC 的應用程式使用 WebFlux 的 WebClient
來發出傳出請求,並且該請求不應與超媒體元素一起使用,則可以透過在組態中明確宣告 WebMVC 來限制要啟用的功能
@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("http://localhost:8080/api/"), MediaTypes.HAL_JSON);
String name = traverson
.follow("movies", "movie", "actor").withTemplateParameters(parameters)
.toObject("$.name");
您可以透過將 Traverson
實例指向 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 =
TypeReferences.CollectionModelType<Item>() {};
CollectionModel<Item> itemResource = traverson.//
follow(rel("items")).//
toObject(collectionModelType);
此範例不是提取單個資源,而是將集合反序列化為 CollectionModel
。
6.2. 使用 LinkDiscoverer
實例
當使用啟用超媒體的表示形式時,常見的任務是在其中找到具有特定關係類型的連結。Spring HATEOAS 提供了基於 JSONPath 的 LinkDiscoverer
介面實作,用於預設表示形式呈現或開箱即用的 HAL。當使用 @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
,如下所示
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 中註冊所有正確的編碼器和解碼器。若要使用它,您需要將 builder 注入到應用程式中的某個位置,並執行 build() 方法以產生 WebClient 。 |
如果您正在使用 Spring Boot,還有另一種方法:WebClientCustomizer
。
@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
的實例,請查看此範例
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("http://localhost/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("http://localhost/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,則還有其他選項,例如這樣
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
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()
方法,然後套用最終設定(例如憑證和其他詳細資訊)。
若要註冊基於超媒體的訊息轉換器,請將以下內容新增至您的程式碼
@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
實例將能夠使用超媒體進行互動。