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

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

书接上文⬆⬆⬆

在 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<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> all() { List<EntityModel> 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<EntityModel> 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<Order, EntityModel> { @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的一些讨论,对你有帮助吗?如果你有兴趣深入了解,欢迎到Spring中国教育管理中心留言交流!

相关文章
|
21天前
|
安全 Java 数据库
安全无忧!在 Spring Boot 3.3 中轻松实现 TOTP 双因素认证
【10月更文挑战第8天】在现代应用程序开发中,安全性是一个不可忽视的重要环节。随着技术的发展,双因素认证(2FA)已经成为增强应用安全性的重要手段之一。本文将详细介绍如何在 Spring Boot 3.3 中实现基于时间的一次性密码(TOTP)双因素认证,让你的应用安全无忧。
56 5
|
18天前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
33 4
|
20天前
|
Java API 数据库
如何使用Spring Boot构建RESTful API,以在线图书管理系统为例
【10月更文挑战第9天】本文介绍了如何使用Spring Boot构建RESTful API,以在线图书管理系统为例,从项目搭建、实体类定义、数据访问层创建、业务逻辑处理到RESTful API的实现,详细展示了每个步骤。通过Spring Boot的简洁配置和强大功能,开发者可以高效地开发出功能完备、易于维护的Web应用。
47 3
|
4天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
80 62
|
2天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
2天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
10 2
|
11天前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
49 6
|
21天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第8天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建并配置 Spring Boot 项目,实现后端 API 和安全配置。接着,使用 Ant Design Pro Vue 脚手架创建前端项目,配置动态路由和菜单,并创建相应的页面组件。最后,通过具体实践心得,分享了版本兼容性、安全性、性能调优等注意事项,帮助读者快速搭建高效且易维护的应用框架。
28 3
|
22天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用
【10月更文挑战第7天】本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,通过 Spring Initializr 创建 Spring Boot 项目并配置 Spring Security。接着,实现后端 API 以提供菜单数据。在前端部分,使用 Ant Design Pro Vue 脚手架创建项目,并配置动态路由和菜单。最后,启动前后端服务,实现高效、美观且功能强大的应用框架。
27 2
|
2月前
|
Java API 对象存储
微服务魔法启动!Spring Cloud与Netflix OSS联手,零基础也能创造服务奇迹!
这段内容介绍了如何使用Spring Cloud和Netflix OSS构建微服务架构。首先,基于Spring Boot创建项目并添加Spring Cloud依赖项。接着配置Eureka服务器实现服务发现,然后创建REST控制器作为API入口。为提高服务稳定性,利用Hystrix实现断路器模式。最后,在启动类中启用Eureka客户端功能。此外,还可集成其他Netflix OSS组件以增强系统功能。通过这些步骤,开发者可以更高效地构建稳定且可扩展的微服务系统。
48 1