书接上文⬆⬆⬆
在 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 + '}'; }}复制
复制代码
从客户提交订单到完成或取消订单时,订单必须经历一系列状态转换。这可以捕获为 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<Order, Long> {}复制
复制代码
有了这个,您现在可以定义一个基本的 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<EntityModel<Order>> all() { List<EntityModel<Order>> orders = orderRepository.findAll().stream() // .map(assembler::toModel) // .collect(Collectors.toList()); return CollectionModel.of(orders, // linkTo(methodOn(OrderController.class).all()).withSelfRel()); } @GetMapping("/orders/{id}") EntityModel<Order> one(@PathVariable Long id) { Order order = orderRepository.findById(id) // .orElseThrow(() -> new OrderNotFoundException(id)); return assembler.toModel(order); } @PostMapping("/orders") ResponseEntity<EntityModel<Order>> 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 HATEOASRepresentationModelAssembler 组件的概念,将这样的逻辑放入其中 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<Order, EntityModel<Order>> { @Override public EntityModel<Order> toModel(Order order) { // Unconditional links to single-item resource and aggregate root EntityModel<Order> 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。
相反,以下策略有助于降低您的服务破坏您可能控制或可能无法控制的现有客户的可能性:
不要删除旧字段。相反,支持他们。
使用基于 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 中国教育管理中心留言交流!
评论