書接上文???
在 REST API 中構建鏈接
到目前為止,您已經使用基本鏈接構建了一個可進化的 API。為了發展您的 API 并更好地為您的客戶服務,您需要接受超媒體作為應用程序狀態引擎的概念。
這意味著什么?在本節中,您將詳細探討它。
業務邏輯不可避免地會建立涉及流程的規則。此類系統的風險在于我們經常將此類服務器端邏輯帶入客戶端并建立強耦合。REST 就是要打破這種連接并最小化這種耦合。
為了展示如何在不觸發客戶端中斷更改的情況下應對狀態變化,想象一下添加一個履行訂單的系統。
第一步,定義一條Order記錄:
鏈接
/src/main/java/payroll/Order.java
package payroll;import java.util.Objects;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.Table;@Entity@Table(name = "CUSTOMER_ORDER")class Order { private @Id @GeneratedValue Long id; private String description; private Status status; Order() {} Order(String description, Status status) { this.description = description; this.status = status; } public Long getId() { return this.id; } public String getDescription() { return this.description; } public Status getStatus() { return this.status; } public void setId(Long id) { this.id = id; } public void setDescription(String description) { this.description = description; } public void setStatus(Status status) { this.status = status; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Order)) return false; Order order = (Order) o; return Objects.equals(this.id, order.id) && Objects.equals(this.description, order.description) && this.status == order.status; } @Override public int hashCode() { return Objects.hash(this.id, this.description, this.status); } @Override public String toString() { return "Order{" + "id=" + this.id + ", description='" + this.description + '\'' + ", status=" + this.status + '}'; }}復制
- 該類需要 JPA@Table注釋將表的名稱更改為,CUSTOMER_ORDER因為ORDER它不是表的有效名稱。
- 它包括一個description字段以及一個status字段。
從客戶提交訂單到完成或取消訂單時,訂單必須經歷一系列狀態轉換。這可以捕獲為 Java enum:
鏈接
/src/main/java/payroll/Status.java
package payroll;enum Status { IN_PROGRESS, // COMPLETED, // CANCELLED}復制
這enum捕獲了一個Order可以占據的各種狀態。對于本教程,讓我們保持簡單。
要支持與數據庫中的訂單交互,必須定義相應的 Spring Data 存儲庫:
Spring Data JPA 的JpaRepository基本接口
interface OrderRepository extends JpaRepository {}復制,>
有了這個,您現在可以定義一個基本的OrderController:
鏈接
/src/main/java/payroll/OrderController.java
@RestControllerclass OrderController { private final OrderRepository orderRepository; private final OrderModelAssembler assembler; OrderController(OrderRepository orderRepository, OrderModelAssembler assembler) { this.orderRepository = orderRepository; this.assembler = assembler; } @GetMapping("/orders") CollectionModel> all() { List> orders = orderRepository.findAll().stream() // .map(assembler::toModel) // .collect(Collectors.toList()); return CollectionModel.of(orders, // linkTo(methodOn(OrderController.class).all()).withSelfRel()); } @GetMapping("/orders/{id}") EntityModel one(@PathVariable Long id) { Order order = orderRepository.findById(id) // .orElseThrow(() -> new OrderNotFoundException(id)); return assembler.toModel(order); } @PostMapping("/orders") ResponseEntity> newOrder(@RequestBody Order order) { order.setStatus(Status.IN_PROGRESS); Order newOrder = orderRepository.save(order); return ResponseEntity // .created(linkTo(methodOn(OrderController.class).one(newOrder.getId())).toUri()) // .body(assembler.toModel(newOrder)); }}復制
- 它包含與您迄今為止構建的控制器相同的 REST 控制器設置。
- 它同時注入OrderRepositorya 和 a (not yet built) OrderModelAssembler。
- 前兩個 Spring MVC 路由處理聚合根以及單個項目Order資源請求。
- 第三條 Spring MVC 路由通過在IN_PROGRESS狀態中啟動它們來處理創建新訂單。
- 所有控制器方法都返回 Spring HATEOAS 的RepresentationModel子類之一以正確呈現超媒體(或圍繞此類類型的包裝器)。
在構建 之前OrderModelAssembler,讓我們討論需要發生的事情。您正在對 、 和 之間的狀態流Status.IN_PROGRESS進行Status.COMPLETED建模Status.CANCELLED。向客戶端提供此類數據時,一件很自然的事情是讓客戶端根據此有效負載決定它可以做什么。
但那是錯誤的。
當您在此流程中引入新狀態時會發生什么?UI 上各種按鈕的放置可能是錯誤的。
如果您更改了每個州的名稱,可能是在編碼國際支持并顯示每個州的區域設置特定文本時會怎樣?這很可能會破壞所有客戶。
輸入HATEOAS或超媒體作為應用程序狀態引擎。與其讓客戶端解析有效負載,不如為它們提供鏈接以發出有效操作的信號。將基于狀態的操作與數據負載分離。換句話說,當CANCEL和COMPLETE是有效操作時,將它們動態添加到鏈接列表中。客戶端只需要在鏈接存在時向用戶顯示相應的按鈕。
這使客戶端不必知道此類操作何時有效,從而降低了服務器及其客戶端在狀態轉換邏輯上不同步的風險。
已經接受了 Spring HATEOAS
RepresentationModelAssembler組件的概念,將這樣的邏輯放入其中OrderModelAssembler將是捕獲此業務規則的完美位置:
鏈接
/src/main/java/payroll/OrderModelAssembler.java
package payroll;import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;import org.springframework.hateoas.EntityModel;import org.springframework.hateoas.server.RepresentationModelAssembler;import org.springframework.stereotype.Component;@Componentclass OrderModelAssembler implements RepresentationModelAssembler> { @Override public EntityModel toModel(Order order) { // Unconditional links to single-item resource and aggregate root EntityModel orderModel = EntityModel.of(order, linkTo(methodOn(OrderController.class).one(order.getId())).withSelfRel(), linkTo(methodOn(OrderController.class).all()).withRel("orders")); // Conditional links based on state of the order if (order.getStatus() == Status.IN_PROGRESS) { orderModel.add(linkTo(methodOn(OrderController.class).cancel(order.getId())).withRel("cancel")); orderModel.add(linkTo(methodOn(OrderController.class).complete(order.getId())).withRel("complete")); } return orderModel; }}復制,>
此資源組裝器始終包含指向單項資源的自身鏈接以及返回聚合根的鏈接。但它也包括兩個條件鏈接OrderController.cancel(id)以及OrderController.complete(id)(尚未定義)。這些鏈接僅在訂單狀態為 時顯示Status.IN_PROGRESS。
如果客戶可以采用 HAL 和讀取鏈接的能力,而不是簡單地讀取普通的舊 JSON 數據,他們可以交換對訂單系統領域知識的需求。這自然減少了客戶端和服務器之間的耦合。它打開了調整訂單履行流程的大門,而不會在流程中破壞客戶。
要完成訂單履行,請將以下內容添加到OrderController操作中cancel:
在 OrderController 中創建“取消”操作
@DeleteMapping("/orders/{id}/cancel")ResponseEntity cancel(@PathVariable Long id) { Order order = orderRepository.findById(id) // .orElseThrow(() -> new OrderNotFoundException(id)); if (order.getStatus() == Status.IN_PROGRESS) { order.setStatus(Status.CANCELLED); return ResponseEntity.ok(assembler.toModel(orderRepository.save(order))); } return ResponseEntity // .status(HttpStatus.METHOD_NOT_ALLOWED) // .header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) // .body(Problem.create() // .withTitle("Method not allowed") // .withDetail("You can't cancel an order that is in the " + order.getStatus() + " status"));}復制
Order它在允許取消之前檢查狀態。如果它不是一個有效的狀態,它會返回一個RFC-7807 Problem,一個支持超媒體的錯誤容器。如果轉換確實有效,則將 轉換Order為CANCELLED。
并將其添加到OrderController訂單完成中:
在 OrderController 中創建“完整”操作
@PutMapping("/orders/{id}/complete")ResponseEntity complete(@PathVariable Long id) { Order order = orderRepository.findById(id) // .orElseThrow(() -> new OrderNotFoundException(id)); if (order.getStatus() == Status.IN_PROGRESS) { order.setStatus(Status.COMPLETED); return ResponseEntity.ok(assembler.toModel(orderRepository.save(order))); } return ResponseEntity // .status(HttpStatus.METHOD_NOT_ALLOWED) // .header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) // .body(Problem.create() // .withTitle("Method not allowed") // .withDetail("You can't complete an order that is in the " + order.getStatus() + " status"));}復制
這實現了類似的邏輯以防止Order狀態完成,除非處于正確的狀態。
讓我們更新LoadDatabase以預加載一些Orders 以及Employee它之前加載的 s。
更新數據庫預加載器
package payroll;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.CommandLineRunner;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationclass LoadDatabase { private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class); @Bean CommandLineRunner initDatabase(EmployeeRepository employeeRepository, OrderRepository orderRepository) { return args -> { employeeRepository.save(new Employee("Bilbo", "Baggins", "burglar")); employeeRepository.save(new Employee("Frodo", "Baggins", "thief")); employeeRepository.findAll().forEach(employee -> log.info("Preloaded " + employee)); orderRepository.save(new Order("MacBook Pro", Status.COMPLETED)); orderRepository.save(new Order("iPhone", Status.IN_PROGRESS)); orderRepository.findAll().forEach(order -> { log.info("Preloaded " + order); }); }; }}復制
現在你可以測試了!
要使用新生成的訂單服務,只需執行一些操作:
$ curl -v http://localhost:8080/orders{ “_嵌入”:{ “訂單”: [ { “身份證”:3, “描述”:“MacBook Pro”, “狀態”:“已完成”, “_鏈接”:{ “自己”: { "href": "http://localhost:8080/orders/3" }, “訂單”: { "href": "http://localhost:8080/orders" } } }, { “身份證”:4, “描述”:“iPhone”, “狀態”:“IN_PROGRESS”, “_鏈接”:{ “自己”: { "href": "http://localhost:8080/orders/4" }, “訂單”: { "href": "http://localhost:8080/orders" }, “取消”: { "href": "http://localhost:8080/orders/4/cancel" }, “完全的”: { "href": "http://localhost:8080/orders/4/complete" } } } ] }, “_鏈接”:{ “自己”: { "href": "http://localhost:8080/orders" } }}
此 HAL 文檔會根據其當前狀態立即顯示每個訂單的不同鏈接。
- 第一個訂單,即COMPLETED只有導航鏈接。未顯示狀態轉換鏈接。
- 第二個訂單,即 IN_PROGRESS還具有取消鏈接和完整鏈接。
嘗試取消訂單:
$ curl -v -X 刪除 http://localhost:8080/orders/4/cancel> 刪除 /orders/4/cancel HTTP/1.1> 主機:本地主機:8080> 用戶代理:curl/7.54.0> 接受:*/*>< HTTP/1.1 200< 內容類型:application/hal+json;charset=UTF-8< 傳輸編碼:分塊< 日期:2018 年 8 月 27 日星期一 15:02:10 GMT<{ “身份證”:4, “描述”:“iPhone”, “狀態”:“取消”, “_鏈接”:{ “自己”: { "href": "http://localhost:8080/orders/4" }, “訂單”: { "href": "http://localhost:8080/orders" } }}
此響應顯示一個HTTP 200狀態代碼,表明它是成功的。響應 HAL 文檔顯示該訂單處于新狀態 ( CANCELLED)。改變狀態的鏈接消失了。
如果再次嘗試相同的操作……
$ curl -v -X 刪除 http://localhost:8080/orders/4/cancel* TCP_NODELAY 設置* 連接到 localhost (::1) 端口 8080 (#0)> 刪除 /orders/4/cancel HTTP/1.1> 主機:本地主機:8080> 用戶代理:curl/7.54.0> 接受:*/*>< HTTP/1.1 405< 內容類型:應用程序/問題+json< 傳輸編碼:分塊< 日期:2018 年 8 月 27 日星期一 15:03:24 GMT<{ "title": "方法不允許", "detail": "您不能取消處于 CANCELED 狀態的訂單"}
…?您會看到HTTP 405 Method Not Allowed響應。DELETE已成為無效操作。Problem響應對象清楚地表明您不能“取消”已經處于“CANCELLED”狀態的訂單。
此外,嘗試完成相同的訂單也會失敗:
$ curl -v -X PUT localhost:8080/orders/4/complete* TCP_NODELAY 設置* 連接到 localhost (::1) 端口 8080 (#0)> PUT /orders/4/完成 HTTP/1.1> 主機:本地主機:8080> 用戶代理:curl/7.54.0> 接受:*/*>< HTTP/1.1 405< 內容類型:應用程序/問題+json< 傳輸編碼:分塊< 日期:2018 年 8 月 27 日星期一 15:05:40 GMT<{ "title": "方法不允許", "detail": "您無法完成處于 CANCELED 狀態的訂單"}
有了這一切,您的訂單履行服務就能夠有條件地顯示可用的操作。它還可以防止無效操作。
通過利用超媒體和鏈接協議,客戶端可以構建得更堅固,并且不太可能僅僅因為數據的變化而崩潰。Spring HATEOAS 可以輕松構建您需要為客戶提供服務的超媒體。
概括
在本教程中,您使用了各種策略來構建 REST API。事實證明,REST 不僅僅是漂亮的 URI 和返回 JSON 而不是 XML。
相反,以下策略有助于降低您的服務破壞您可能控制或可能無法控制的現有客戶的可能性:
- 不要刪除舊字段。相反,支持他們。
- 使用基于 rel 的鏈接,這樣客戶端就不必擔心 URI 進行硬編碼。
- 盡可能長時間地保留舊鏈接。即使您必須更改 URI,也要保留 rels,以便舊客戶端可以使用新功能。
- 當各種狀態驅動操作可用時,使用鏈接而不是有效負載數據來指示客戶端。
RepresentationModelAssembler為每種資源類型構建實現并在所有控制器中使用這些組件似乎需要一些努力。但是這種額外的服務器端設置(感謝 Spring HATEOAS 使之變得容易)可以確保您控制的客戶端(更重要的是,您不控制的客戶端)可以隨著您的 API 隨著發展而輕松升級。
我們關于如何使用 Spring 構建 RESTful 服務員的教程到此結束。本教程的每個部分都在單個 github 存儲庫中作為單獨的子項目進行管理:
- nonrest — 沒有自媒體的簡單 Spring MVC 應用程序
- rest — Spring MVC + Spring HATEOAS 應用程序,每個資源的 HAL 表示
- 進化- REST 應用程序,其中一個字段已進化但保留舊數據以實現向后兼容性
- 鏈接- REST 應用程序,其中條件鏈接用于向客戶端發出有效狀態更改信號
要查看使用 Spring HATEOAS 的更多示例,請參閱Spring中國教育管理中心
審核編輯:湯梓紅
-
spring
+關注
關注
0文章
338瀏覽量
14310 -
REST
+關注
關注
0文章
32瀏覽量
9398
發布評論請先 登錄
相關推薦
評論