书接上文⬆⬆⬆
到目前为止,您已经使用基本链接构建了一个可进化的 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 + '}'; }}复制
从客户提交订单到完成或取消订单时,订单必须经历一系列状态转换。这可以捕获为 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)); }}复制
在构建 之前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 文档会根据其当前状态立即显示每个订单的不同链接。
尝试取消订单:
$ 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。
相反,以下策略有助于降低您的服务破坏您可能控制或可能无法控制的现有客户的可能性:
RepresentationModelAssembler为每种资源类型构建实现并在所有控制器中使用这些组件似乎需要一些努力。但是这种额外的服务器端设置(感谢 Spring HATEOAS 使之变得容易)可以确保您控制的客户端(更重要的是,您不控制的客户端)可以随着您的 API 随着发展而轻松升级。
我们关于如何使用 Spring 构建 RESTful 服务员的教程到此结束。本教程的每个部分都在单个 github 存储库中作为单独的子项目进行管理:
要查看使用 Spring HATEOAS 的更多示例,请参阅Spring中国教育管理中心
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !