1. 概述
本文介紹使用 Spring HATEOAS 項目創建基於超媒體的 REST Web 服務的過程。
2. Spring-HATEOAS
The Spring HATEOAS project is a library of APIs that we can use to easily create REST representations that follow the principle of HATEOAS (Hypertext as the Engine of Application State).
Generally speaking, the principle implies that the API should guide the client through the application by returning relevant information about the next potential steps, along with each response.
In this article, we’re going to build an example using Spring HATEOAS with the goal of decoupling the client and server, and theoretically allowing the API to change its URI scheme without breaking clients.
3. 準備
首先,讓我們添加 Spring HATEOAS 依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<version>2.6.4</version>
</dependency>如果未使用 Spring Boot,我們可以將以下庫添加到我們的項目中:
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>如往常一樣,我們可以搜索 Maven Central 中最新版本的 starter HATEOAS、spring-hateoas 和 spring-plugin-core 依賴項。
接下來,我們有未包含 Spring HATEOAS 支持的 Customer 資源:
public class Customer {
private String customerId;
private String customerName;
private String companyName;
// standard getters and setters
}
我們有一個沒有 Spring HATEOAS 支持的控制器類:
@RestController
@RequestMapping(value = "/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
@GetMapping("/{customerId}")
public Customer getCustomerById(@PathVariable String customerId) {
return customerService.getCustomerDetail(customerId);
}
}
最後,客户資源表示形式:
{
"customerId": "10A",
"customerName": "Jane",
"customerCompany": "ABC Company"
}
4. 添加 HATEOAS 支持
在 Spring HATEOAS 項目中,我們無需查找 Servlet 上下文,也不需要將路徑變量與基本 URI 連接起來。
相反,Spring HATEOAS 提供三種抽象用於創建 URI – RepresentationModel, Link, 和 WebMvcLinkBuilder。我們可以使用這些來創建元數據並將其關聯到資源表示形式。
4.1. 為資源添加超媒體支持
該項目提供了一個名為 RepresentationModel 的基礎類,用於創建資源的表示形式。
public class Customer extends RepresentationModel<Customer> {
private String customerId;
private String customerName;
private String companyName;
// standard getters and setters
}
客户資源繼承自表示模型類,繼承了add()方法。因此,一旦我們創建了鏈接,就可以輕鬆地將該值設置為資源表示,而無需向其中添加任何新的字段。
4.2. 創建鏈接
Spring HATEOAS 提供一個 Link 對象來存儲元數據(資源的位置或 URI)。
首先,我們將手動創建一個簡單的鏈接:
Link link = new Link("http://localhost:8080/spring-security-rest/api/customers/10A");
Link 對象遵循 Atom 鏈接語法,包含一個rel屬性,用於標識與資源的關聯,以及href屬性,即實際的鏈接本身。
以下是Customer資源現在包含新鏈接後的樣子:
{
"customerId": "10A",
"customerName": "Jane",
"customerCompany": "ABC Company",
"_links":{
"self":{
"href":"http://localhost:8080/spring-security-rest/api/customers/10A"
}
}
}
響應相關的 URI 被標記為 self 鏈接。 self 關係的語義清晰——它只是資源可以訪問的規範位置。
4.3. 創建更好的鏈接
庫提供的另一個非常重要的抽象是 WebMvcLinkBuilder – 它通過避免硬編碼鏈接,簡化了構建 URI 的過程。
以下代碼片段展示了使用 WebMvcLinkBuilder 類構建客户自服務鏈接:
linkTo(CustomerController.class).slash(customer.getCustomerId()).withSelfRel();
讓我們來查看一下:
- linkTo() 方法檢查控制器類並獲取其根映射
- slash() 方法將 customerId 值添加到鏈接的路徑變量中
- 最後,withSelfMethod() 方法將關係限定為自鏈接
5. 關係
在上一節中,我們展示了自引用關係。然而,更復雜的系統也可能涉及其他關係。
例如,一個 客户 可以與訂單存在關係。 讓我們將 訂單 類建模為資源:
public class Order extends RepresentationModel<Order> {
private String orderId;
private double price;
private int quantity;
// standard getters and setters
}
此時,我們可以通過擴展 CustomerController,添加一個方法來返回特定客户的所有訂單:
@GetMapping(value = "/{customerId}/orders", produces = { "application/hal+json" })
public CollectionModel<Order> getOrdersForCustomer(@PathVariable final String customerId) {
List<Order> orders = orderService.getAllOrdersForCustomer(customerId);
for (final Order order : orders) {
Link selfLink = linkTo(methodOn(CustomerController.class)
.getOrderById(customerId, order.getOrderId())).withSelfRel();
order.add(selfLink);
}
Link link = linkTo(methodOn(CustomerController.class)
.getOrdersForCustomer(customerId)).withSelfRel();
CollectionModel<Order> result = CollectionModel.of(orders, link);
return result;
}
我們的方法返回一個 CollectionModel 對象以符合 HAL 返回類型,以及每個訂單的 “_self” 鏈接和完整的訂單列表。
需要注意的是,客户訂單的超鏈接取決於 getOrdersForCustomer() 方法的映射。我們將這些類型的鏈接稱為方法鏈接,並展示如何使用 WebMvcLinkBuilder 來創建它們。
6. 鏈接到控制器方法
<em>WebMvcLinkBuilder</em> 提供了豐富的支持,用於 Spring MVC 控制器。以下示例展示瞭如何基於 <em>CustomerController</em> 類中的 <em>getOrdersForCustomer()</em> 方法,構建 HATEOAS 鏈接:
Link ordersLink = linkTo(methodOn(CustomerController.class)
.getOrdersForCustomer(customerId)).withRel("allOrders");
方法 methodOn() 通過在目標方法上對代理控制器進行空調用,獲取方法映射,並將其 customerId 設置為 URI 的路徑變量。
7. Spring HATEOAS in Action
讓我們將自鏈接和方法鏈接創建全部整合到 getAllCustomers() 方法中:
@GetMapping(produces = { "application/hal+json" })
public CollectionModel<Customer> getAllCustomers() {
List<Customer> allCustomers = customerService.allCustomers();
for (Customer customer : allCustomers) {
String customerId = customer.getCustomerId();
Link selfLink = linkTo(CustomerController.class).slash(customerId).withSelfRel();
customer.add(selfLink);
if (orderService.getAllOrdersForCustomer(customerId).size() > 0) {
Link ordersLink = linkTo(methodOn(CustomerController.class)
.getOrdersForCustomer(customerId)).withRel("allOrders");
customer.add(ordersLink);
}
}
Link link = linkTo(CustomerController.class).withSelfRel();
CollectionModel<Customer> result = CollectionModel.of(allCustomers, link);
return result;
}接下來,讓我們調用 getAllCustomers() 方法:
curl http://localhost:8080/spring-security-rest/api/customers
並檢查結果:
{
"_embedded": {
"customerList": [{
"customerId": "10A",
"customerName": "Jane",
"companyName": "ABC Company",
"_links": {
"self": {
"href": "http://localhost:8080/spring-security-rest/api/customers/10A"
},
"allOrders": {
"href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders"
}
}
},{
"customerId": "20B",
"customerName": "Bob",
"companyName": "XYZ Company",
"_links": {
"self": {
"href": "http://localhost:8080/spring-security-rest/api/customers/20B"
},
"allOrders": {
"href": "http://localhost:8080/spring-security-rest/api/customers/20B/orders"
}
}
},{
"customerId": "30C",
"customerName": "Tim",
"companyName": "CKV Company",
"_links": {
"self": {
"href": "http://localhost:8080/spring-security-rest/api/customers/30C"
}
}
}]
},
"_links": {
"self": {
"href": "http://localhost:8080/spring-security-rest/api/customers"
}
}
}在每個資源表示中,都包含一個 self 鏈接和一個 allOrders 鏈接,用於提取客户的所有訂單。如果客户沒有訂單,則訂單鏈接不會出現。
這個示例展示了 Spring HATEOAS 如何在 REST Web 服務中促進 API 可發現性。 如果鏈接存在,客户端可以遵循它並獲取客户的所有訂單:
curl http://localhost:8080/spring-security-rest/api/customers/10A/orders
{
"_embedded": {
"orderList": [{
"orderId": "001A",
"price": 150,
"quantity": 25,
"_links": {
"self": {
"href": "http://localhost:8080/spring-security-rest/api/customers/10A/001A"
}
}
},{
"orderId": "002A",
"price": 250,
"quantity": 15,
"_links": {
"self": {
"href": "http://localhost:8080/spring-security-rest/api/customers/10A/002A"
}
}
}]
},
"_links": {
"self": {
"href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders"
}
}
}8. 結論
在本教程中,我們討論瞭如何使用 Spring HATEOAS 項目構建基於超媒體的 Spring REST Web 服務。
在示例中,我們可以看到客户端可以有一個單一的入口點,並且可以根據響應表示中的元數據採取進一步的操作。
這使得服務器可以更改其 URI 方案而不會破壞客户端。 此外,應用程序可以通過在表示中添加新的鏈接或 URI 來宣傳新的功能。