2022就业季|Spring认证教你,如何使用 Spring 构建 REST 服务(五)

2022-07-28 16:34:17 浏览数 (1)

书接上文⬆⬆⬆

在 REST API 中构建链接

到目前为止,您已经使用基本链接构建了一个可进化的 API。为了发展您的 API 并更好地为您的客户服务,您需要接受超媒体作为应用程序状态引擎的概念。

这意味着什么?在本节中,您将详细探讨它。

业务逻辑不可避免地会建立涉及流程的规则。此类系统的风险在于我们经常将此类服务器端逻辑带入客户端并建立强耦合。REST 就是要打破这种连接并最小化这种耦合。

为了展示如何在不触发客户端中断更改的情况下应对状态变化,想象一下添加一个履行订单的系统。

第一步,定义一条Order记录:

链接 /src/main/java/payroll/Order.java

代码语言:javascript复制
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

代码语言:javascript复制
package payroll;enum Status {  IN_PROGRESS, //  COMPLETED, //  CANCELLED}复制

这enum捕获了一个Order可以占据的各种状态。对于本教程,让我们保持简单。

要支持与数据库中的订单交互,必须定义相应的 Spring Data 存储库:

Spring Data JPA 的JpaRepository基本接口

代码语言:javascript复制
interface OrderRepository extends JpaRepository<Order, Long> {}复制

有了这个,您现在可以定义一个基本的OrderController:

链接 /src/main/java/payroll/OrderController.java

代码语言:javascript复制
@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超媒体作为应用程序状态引擎。与其让客户端解析有效负载,不如为它们提供链接以发出有效操作的信号。将基于状态的操作与数据负载分离。换句话说,当CANCELCOMPLETE是有效操作时,将它们动态添加到链接列表中。客户端只需要在链接存在时向用户显示相应的按钮。

这使客户端不必知道此类操作何时有效,从而降低了服务器及其客户端在状态转换逻辑上不同步的风险。

已经接受了 Spring HATEOAS RepresentationModelAssembler组件的概念,将这样的逻辑放入其中OrderModelAssembler将是捕获此业务规则的完美位置:

链接 /src/main/java/payroll/OrderModelAssembler.java

代码语言:javascript复制
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 中创建“取消”操作

代码语言:javascript复制
@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 中创建“完整”操作

代码语言:javascript复制
@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。

更新数据库预加载器

代码语言:javascript复制
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);      });          };  }}复制

现在你可以测试了!

要使用新生成的订单服务,只需执行一些操作:

代码语言:javascript复制
$ 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还具有取消链接和完整链接。

尝试取消订单:

代码语言:javascript复制
$ 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)。改变状态的链接消失了。

如果再次尝试相同的操作……

代码语言:javascript复制
$ 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”状态的订单。

此外,尝试完成相同的订单也会失败:

代码语言:javascript复制
$ 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的一些讨论,对你有帮助吗?如果你有兴趣深入了解,欢迎留言交流!

0 人点赞