vlambda博客
学习文章列表

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

PART I

Getting Started with Microservice Development Using Spring Boot

在这一部分中,您将学习如何使用 Spring Boot 的一些最重要的特性来开发微服务。

本部分包括以下章节:

  • 第 1 章微服务简介
  • 第二章Spring Boot简介
  • 第 3 章创建一组协作微服务
  • 第 4 章使用 Docker 部署我们的微服务
  • 第 5 章使用 OpenAPI 添加 API 描述
  • 第 6 章添加持久性
  • 第 7 章开发响应式微服务

Creating a Set of Cooperating Microservices

在本章中,我们将构建我们的第一对微服务。我们将学习如何创建具有简约功能的协作微服务。在接下来的章节中,我们将为这些微服务添加越来越多的功能。在本章结束时,我们将拥有一个由复合微服务公开的 RESTful API。复合微服务将使用它们的 RESTful API 调用其他三个微服务来创建聚合响应。

本章将涵盖以下主题:

  • 介绍微服务环境
  • 生成骨架微服务
  • 添加 RESTful API
  • 添加复合微服务
  • 添加错误处理
  • 手动测试 API
  • 单独添加微服务的自动化测试
  • 将半自动化测试添加到微服务环境

技术要求

有关如何安装本书中使用的工具以及如何访问本书源代码的说明,请参阅:

  • 第 21 章适用于 macOS
  • 第 22 章 适用于 Windows

本章代码示例均来自$BOOK_HOME/Chapter03中的源码。

有了工具和源代码,我们就可以开始学习本章将要创建的微服务的系统环境了。

介绍微服务环境

第1章微服务简介中,我们简要介绍了基于微服务的< /a>我们将在本书中使用的系统环境:

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.1:微服务格局

它由三个核心微服务组成,Product、Review和Recommendation服务,所有这些服务都处理一种类型的资源,以及一个称为 Product Composite 服务的复合微服务,它聚合来自三个核心服务的信息。

微服务处理的信息

为了使本书中的源代码 代码示例易于理解,它们具有最少的业务逻辑。出于同样的原因,他们处理的业务对象的信息模型保持最小。在本节中,我们将介绍每个微服务处理的信息,包括与基础设施相关的信息。

产品服务

product 服务管理产品信息,并使用以下属性描述每个产品:

  • 产品编号
  • 姓名
  • 重量

审核服务

review 服务管理 产品评论并存储有关每条评论的以下信息:

  • 产品编号
  • 审核 ID
  • 作者
  • 主题
  • 内容

推荐服务

recommendation 服务 管理产品推荐并存储有关每个推荐的以下信息:

  • 产品编号
  • 推荐 ID
  • 作者
  • 速度
  • 内容

产品复合服务

产品组合服务 聚合来自三个核心服务的信息,并呈现有关产品的信息,如下所示:

  • 产品信息,如 product 服务中所述
  • 指定产品的产品评论列表,如 review 服务中所述
  • 指定产品的产品推荐列表,如recommendation服务中所述

基础设施相关信息

一旦我们开始将我们的 微服务作为由基础设施管理的容器(首先是 Docker,然后是 Kubernetes)运行,跟踪哪个容器实际响应了我们的请求将会很有趣.作为一个简单的解决方案,serviceAddress 属性已添加到所有响应中,格式为 主机名/IP 地址:端口

第 18 章使用服务网格提高可观察性和管理,和第 19 章< /em>,使用 EFK 堆栈进行集中式日志记录,我们将了解更强大的解决方案来跟踪由微服务处理的请求。

临时替换服务发现

由于现阶段我们没有任何服务发现机制,我们将在localhost 并为每个微服务使用硬编码的端口号。我们将使用以下端口:

  • 产品组合服务:7000
  • 产品服务:7001
  • 审核服务:7002
  • 推荐服务:7003

稍后当我们开始使用 Docker 和 Kubernetes 时,我们将摆脱硬编码的端口!

在本节中,我们已经介绍了我们将要创建的微服务以及它们将处理的信息。在下一节中,我们将使用 Spring Initializr 为微服务创建骨架代码。

生成骨架微服务

现在是时候看看我们如何为我们的微服务创建项目了。该主题的最终结果可以在 $BOOK_HOME/Chapter03/1-spring-init 文件夹中找到。为了简化项目的设置,我们将使用 Spring Initializr 为每个微服务生成一个框架项目。骨架项目包含构建项目所需的文件,以及一个空的 main 类和微服务的测试类。之后,我们将看到如何在我们将使用的构建工具 Gradle 中使用一个 命令构建所有微服务。

使用 Spring Initializr 生成骨架代码

要开始 开发我们的微服务,我们将使用一个名为 Spring Initializr 的工具为我们生成框架代码。 Spring Initializr 由 Spring 团队提供,可用于配置和生成新的 Spring Boot 应用程序。该工具可帮助开发人员选择应用程序要使用的其他 Spring 模块,并确保将依赖项配置为使用所选模块的兼容版本。该工具支持使用 Maven 或 Gradle 作为构建系统,并且可以为 Java、Kotlin 或 Groovy 生成源代码。

可以使用 URL https://start.spring 从 Web 浏览器调用它。 io/ 或通过命令行工具 spring init。为了更容易重现微服务的创建,我们将使用命令行工具。

对于每个微服务,我们将创建一个 Spring Boot 项目,该项目执行以下操作:

  • 使用 Gradle 作为构建工具
  • 为 Java 8 生成代码
  • 将项目打包为胖 JAR 文件
  • 引入 ActuatorWebFlux Spring 模块的依赖项
  • 基于 Spring Boot v2.5.2(依赖于 Spring Framework v5.3.8)

Spring Boot Actuator 启用了许多 有价值的端点来进行管理和监控。稍后我们将看到它们的实际应用。 Spring WebFlux 将在此处用于创建我们的 RESTful API。

要为我们的微服务创建骨架代码,我们需要为 product-service 运行以下命令:

spring init \
--boot-version=2.5.2 \
--build=gradle \
--java-version=1.8 \
--packaging=jar \
--name=product-service \
--package-name=se.magnus.microservices.core.product \
--groupId=se.magnus.microservices.core.product \
--dependencies=actuator,webflux \
--version=1.0.0-SNAPSHOT \
product-service

如果你想了解更多关于 spring init CLI,你可以运行 spring help init 命令。要查看可以添加哪些依赖项,请运行 spring init --list 命令。

如果你想自己创建这四个项目,而不是使用本书 GitHub 存储库中的源代码,试试 $BOOK_HOME/Chapter03/1-spring-init/create-projects.bash,如下:

mkdir some-temp-folder
cd some-temp-folder
$BOOK_HOME/Chapter03/1-spring-init/create-projects.bash

使用 create-projects.bash 创建我们的四个项目后,我们将具有以下文件结构:

microservices/
├── product-composite-service
├── product-service
├── recommendation-service
└── review-service

对于每个项目,我们可以列出创建的文件。让我们为 product-service 项目执行此操作:

find microservices/product-service -type f

我们将收到以下输出:

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.2:列出我们为 product-service 创建的文件

Spring Initializr 为 Gradle 创建了多个文件,一个 .gitignore 文件,以及三个 Spring引导文件:

  • ProductServiceApplication.java,我们的主要应用类
  • application.properties,一个空的属性文件
  • ProductServiceApplicationTests.java,一个测试类,已配置为使用 JUnit 在我们的 Spring Boot 应用程序上运行测试

main 应用程序类 ProductServiceApplication.java 看起来正如我们根据前一章中的神奇的@SpringBootApplication注解部分所期望的:

package se.magnus.microservices.core.product;

@SpringBootApplication
public class ProductServiceApplication {

   public static void main(String[] args) {
      SpringApplication.run(ProductServiceApplication.class, args);
   }
}

测试类如下所示:

package se.magnus.microservices.core.product;

@SpringBootTest
class ProductServiceApplicationTests {

   @Test
   void contextLoads() {
   }
}

@SpringBootTest 注解会以与 @ 相同的方式初始化我们的应用程序SpringBootApplication 在运行应用程序时执行;也就是说,Spring 应用程序上下文将在使用组件扫描和自动配置执行测试之前设置,如前一章所述。

让我们也看看最重要的 Gradle 文件,build.gradle。该文件的内容描述了如何构建项目,例如如何解决依赖关系以及如何编译、测试和打包源代码。 Gradle 文件首先列出要应用的插件:

plugins {
    id 'org.springframework.boot' version '2.5.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

声明的插件使用如下:

  • java 插件将 Java 编译器添加到项目中。
  • 插件 org.springframework.bootio.spring.dependency- management 被声明,它们共同确保 Gradle 将构建一个胖 JAR 文件,并且我们不需要在 Spring Boot 启动依赖项上指定任何显式版本号。相反,它们被 org.springframework.boot 插件的版本隐含,即 2.5.2

在构建文件的其余部分,我们基本上为我们的项目、Java 版本及其依赖项声明了组名和版本:

group = 'se.magnus.microservices.composite.product'
version = '1.0.0-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

test {
    useJUnitPlatform()
}

关于所使用的依赖项和最终 test 声明的一些 注释:

  • 与前面的插件一样,依赖项是从中央 Maven 存储库中获取的。
  • 依赖项设置为 ActuatorWebFlux 模块,以及一些有用的测试依赖项。
  • 最后,JUnit 被配置为用于在 Gradle 构建中运行我们的测试。

我们可以使用以下命令分别构建每个微服务:

cd microservices/product-composite-service; ./gradlew build; cd -; \
cd microservices/product-service;           ./gradlew build; cd -; \
cd microservices/recommendation-service;    ./gradlew build; cd -; \
cd microservices/review-service;            ./gradlew build; cd -;

注意我们如何使用 Spring Initializr 创建的 gradlew 可执行文件;也就是说,我们不需要安装 Gradle!

我们第一次使用 gradlew 运行命令时,它会自动下载 Gradle。使用的 Gradle 版本由 gradle 中的 distributionUrl 属性确定/wrapper/gradle-wrapper.properties 文件。

在 Gradle 中设置多项目构建

为了让用一个命令构建所有微服务更简单一些,我们可以在Gradle中建立一个多项目构建.步骤如下:

  1. 首先,我们创建 settings.gradle 文件,该文件描述了 Gradle 应该构建哪些项目:
    cat <<EOF > settings.gradle 包括“:微服务:产品服务” 包括“:微服务:审查服务” 包括“:微服务:推荐服务” 包括“:微服务:产品复合服务” EOF 
  2. 接下来,我们复制从其中一个项目生成的 Gradle 可执行文件,以便我们可以将它们重用于多项目构建:
    cp -r microservices/product-service/gradle 。 cp 微服务/产品服务/gradlew 。 cp 微服务/产品服务/gradlew.bat。 cp 微服务/产品服务/.gitignore 。 
  3. 我们不再需要每个项目中生成的 Gradle 可执行文件,因此我们可以使用以下命令将其删除:
    查找微服务 -depth -name "gradle" -exec rm -rfv "{}" \; 查找微服务 -depth -name "gradlew*" -exec rm -fv "{}" \; 

    结果 应该与您在$BOOK_HOME/ Chapter03/1-spring-init

  4. Now, we can build all the microservices with one command:
    ./gradlew build 

    如果您还没有运行上述命令,您可以直接转到本书的源代码并从那里构建它:

    cd $BOOK_HOME/Chapter03/1-spring-init ./gradlew build 

    这应该会产生以下输出:

    读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.3:成功构建后的输出

使用 Spring Initializr 创建的微服务骨架 项目 并使用 Gradle 成功构建,我们准备向微服务添加一些代码在下一节中。

从 DevOps 的角度来看,多项目设置可能不是首选。相反,为了让每个微服务都有自己的构建和发布周期,为每个微服务项目设置单独的构建管道可能是首选。但是,出于本书的目的,我们将使用多项目设置,以便更轻松地使用单个命令构建和部署整个系统环境。

添加 RESTful API

现在我们已经为我们的微服务设置了项目,让我们为我们的三个核心微服务添加一些 RESTful API!

这个和本章剩余主题的最终结果可以在 $BOOK_HOME/Chapter03/2-basic-rest-services 文件夹中找到.

首先,我们将添加两个项目(apiutil) 将包含由微服务项目共享的代码,然后我们将实现 RESTful API。

添加 API 和 util 项目

要添加 api 项目,我们需要 执行以下操作:

  1. First, we will set up a separate Gradle project where we can place our API definitions. We will use Java interfaces in order to describe our RESTful APIs and model classes to describe the data that the API uses in its requests and responses. To describe what types of errors can be returned by the API, a number of exception classes are also defined. Describing a RESTful API in a Java interface instead of directly in the Java class is, to me, a good way of separating the API definition from its implementation. We will further extend this pattern later in this book when we add more API information in the Java interfaces to be exposed in an OpenAPI specification. See Chapter 5, Adding an API Description Using OpenAPI, for more information.

    将一组微服务的 API 定义存储在一个通用 API 模块中是否是一种好的做法是值得商榷的。它可能会导致微服务之间出现不希望的依赖关系,从而导致整体特征,例如,导致开发过程更加复杂和缓慢。对我来说,对于属于同一交付组织的微服务来说,这是一个不错的选择,也就是说,其发布由同一组织管理(将其与 bounded context 中的领域驱动设计,我们的微服务被放置在一个单一的有界上下文中)。正如在第 1 章中已经讨论过的,微服务简介,同一限界上下文中的微服务< /a> 需要有基于公共信息模型的 API 定义,因此将这些 API 定义存储在同一个 API 模块中不会添加任何不希望的依赖项。

  2. Next, we will create a util project that can hold some helper classes that are shared by our microservices, for example, for handling errors in a uniform way.

    同样,从 DevOps 的角度来看,最好在自己的构建管道中构建所有项目,并为 api 提供版本控制的依赖项以及微服务项目中的util项目,也就是让每个微服务可以选择apiutil 项目使用。但是为了在本书的上下文中保持构建和部署步骤的简单性,我们将制作 apiutil 项目是多项目构建的一部分。

API 项目

api 项目将被 打包成一个库;也就是说,它不会有自己的 main 应用程序类。不幸的是,Spring Initializr 不支持创建库项目。相反,必须从头开始手动创建库项目。 API 项目的源代码位于 $BOOK_HOME/Chapter03/2-basic-rest-services/api

库项目的结构与应用程序项目的结构相同,只是我们不再有 main 应用程序类,以及一些build.gradle 文件中的细微差别。 Gradle 插件 org.springframework.boot 替换为 实现平台 部分:

ext {
    springBootVersion = '2.5.2'
}
dependencies {
    implementation platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")

这允许我们保留 Spring Boot 依赖管理,同时我们在构建步骤中将胖 JAR 的构建替换为仅包含项目自己的类和属性文件的普通 JAR 文件的创建。

我们三个核心微服务的api项目中的Java文件如下:

$BOOK_HOME/Chapter03/2-basic-rest-services/api/src/main/java/se/magnus/api/core
├── product
│   ├── Product.java
│   └── ProductService.java
├── recommendation
│   ├── Recommendation.java
│   └── RecommendationService.java
└── review
    ├── Review.java
    └── ReviewService.java

三个核心微服务的 Java 类的结构看起来非常相似,因此我们将只介绍 product 服务的源代码。

首先我们来看ProductService.java Java接口,如下代码所示:

package se.magnus.api.core.product;

public interface ProductService {

    @GetMapping(
        value    = "/product/{productId}",
        produces = "application/json")
     Product getProduct(@PathVariable int productId);
}

Java 接口 声明的工作方式如下:

  • product 服务只公开了一种API方法,getProduct() (我们将在本书后面的Chapter 6添加持久性中扩展 API)。
  • 要将方法映射到 HTTP GET 请求,我们使用 @ GetMapping Spring注解,我们在其中指定方法将映射到哪个URL路径(/product/{productId})以及什么在这种情况下,响应将采用 JSON 格式。
  • 路径的 {productId} 部分映射到名为 的路径变量产品ID
  • productId 方法参数用 @PathVariable 注释,这会将 HTTP 请求中传递的值映射到参数。例如,一个 HTTP GET 请求到 /product/123 将导致使用 调用 getProduct() 方法productId 参数设置为 123

该方法返回一个 Product 对象,一个基于 POJO 的普通模型类,其成员变量对应于 产品,如本章开头所述。 Product.java 如下所示(不包括构造函数和 getter 方法):

public class Product {
  private final int productId;
  private final String name;
  private final int weight;
  private final String serviceAddress;
}

这种类型的 POJO 类也称为 作为 数据传输对象 (DTO) 因为它用于在 API 实现和 API 调用者之间传输数据。当我们进入第 6 章添加持久性,我们将看看另一种类型的 POJO,它可以用来描述数据如何存储在数据库中,也称为实体对象。

API 项目还包含异常类 InvalidInputExceptionNotFoundException

实用项目

util 项目 将与 api 项目。 util 项目的源代码位于 $BOOK_HOME/Chapter03 /2-basic-rest-services/util。该项目包含以下实用程序类:GlobalControllerExceptionHandlerHttpErrorInfoServiceUtil

除了 ServiceUtil.java 中的代码之外,这些类都是可重用的实用程序类,我们可以使用它们将 Java 异常映射到正确的 HTTP 状态代码,如在后面的章节添加错误处理中描述。 ServiceUtil.java的主要目的是找出微服务使用的主机名、IP地址和端口。该类公开了一个方法,getServiceAddress(),微服务可以使用该方法来查找它们的主机名、IP 地址和端口,如所述在上一节中,基础设施相关信息

实现我们的 API

现在我们可以开始 在核心微服务中实现我们的 API!

三个核心微服务的实现看起来非常相似,因此我们将只介绍 product 服务的源代码。您可以在 $BOOK_HOME/Chapter03/2-basic-rest-services/microservices 中找到其他文件。让我们看看我们是怎么做的:

  1. 我们需要添加 apiutil 项目为build.gradle 文件中的依赖项,在 product-service< /代码>项目:
    依赖{ 实施项目(':api') 实施项目(':util') 
  2. 启用 Spring Boot 的自动配置功能以检测 api 中的 Spring Bean util 项目,我们还需要在 main应用类,包含apiutil 项目:
    @SpringBootApplication @ComponentScan("se.magnus") 公共类 ProductServiceApplication { 
  3. 接下来,我们创建服务实现文件ProductServiceImpl.java,以实现Java接口ProductService,来自 api 项目并使用 @RestController 这样Spring就会根据Interface 类:
    package se.magnus.microservices.core.product.services; @RestController 公共类 ProductServiceImpl 实现 ProductService { } 
  4. 能够使用 ServiceUtil 类-Text--PACKT-">util项目,我们将其注入到构造函数中,如下:
    private final ServiceUtil serviceUtil; @自动连线 公共 ProductServiceImpl(ServiceUtil serviceUtil) { this.serviceUtil = serviceUtil; } 
  5. Now, we can implement the API by overriding the getProduct() method from the interface in the api project:
    @Override public Product getProduct(int productId) { return new Product(productId, "name-" + productId, 123, serviceUtil.getServiceAddress()); } 

    由于我们目前没有使用数据库,我们只是根据 productId 的输入以及由提供的服务地址返回一个硬编码响应ServiceUtil 类。

    有关最终结果,包括日志记录和错误处理,请参阅 ProductServiceImpl.java

  6. Finally, we also need to set up some runtime properties – what port to use and the desired level of logging. This is added to the property file application.yml:
    server.port: 7001 logging: level: root: INFO se.magnus.microservices: DEBUG 

    请注意,Spring Initializr 生成的空 application.properties 文件已替换为 YAML 文件,application.yml。与 相比,YAML 文件对相关属性的分组提供了更好的支持。properties< /代码>文件。请参阅上面的日志级别设置作为示例。

  7. 我们可以自行试用product服务。使用以下命令构建并启动微服务:
    cd $BOOK_HOME/Chapter03/2-basic-rest-services ./gradlew 构建 java -jar microservices/product-service/build/libs/*.jar & 

    等到终端打印以下内容:

    <图类=“媒体对象”> 读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

    图3.4:启动ProductServiceApplication

  8. Make a test call to the product service:
    curl http://localhost:7001/product/123 

    它应该以类似于以下内容的方式响应:

    读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

    图 3.5:对测试调用的预期响应

  9. 最后,停止 product 服务:
    kill $(jobs -p) 

我们现在已经构建、运行和测试了我们的第一个单一微服务。在下一节中,我们将实现复合微服务,该微服务将使用我们迄今为止创建的三个核心微服务。

从 Spring Boot v2.5.0 开始,运行 ./gradlew build 命令时会创建两个 jar 文件:普通的 jar 文件,加上一个普通的jar 文件仅包含在 Spring Boot 应用程序中编译 Java 文件产生的类文件。由于我们不需要新的普通 jar 文件,因此已禁用它的创建,以便在运行 Spring Boot 应用程序时使用通配符引用普通 jar 文件,例如:

java -jar microservices/product-service/build/libs/*.jar

通过在每个微服务的 build.gradle 文件中添加以下行,已禁用新的普通 jar 文件的创建:

jar {
    enabled = false
}

有关详细信息,请参阅 < span class="url">https://docs.spring.io/spring-boot/docs/2.5.2/gradle-plugin/reference/htmlsingle/#packaging-executable.and-plain-archives< /a>。

添加复合微服务

现在,是时候通过添加将调用三个核心服务的复合服务来将 事物联系在一起了!

组合服务的实现分为两部分:一个集成组件,用于处理向核心服务发出的 HTTP 请求,另一个是组合服务实现本身。这种职责分工的主要原因是它简化了自动化单元和集成测试;我们可以通过用模拟替换集成组件来单独测试服务实现。

正如我们将在本书后面看到的那样,这种责任分工也将更容易介绍断路器!

在我们研究这两个组件的源代码之前,我们需要看一下复合微服务将使用的 API 类,并了解如何使用运行时属性来保存核心微服务的地址信息。

集成组件的完整实现和复合服务的实现都可以在 Java 包 se.magnus.microservices.composite.product.services< /代码>。

API 类

在本节中,我们将查看描述复合组件 API 的类。它们可以在 $BOOK_HOME/Chapter03/2-basic-rest-services/api 中找到。以下是 API 类:

$BOOK_HOME/Chapter03/2-basic-rest-services/api
└── src/main/java/se/magnus/api/composite
    └── product
        ├── ProductAggregate.java
        ├── ProductCompositeService.java
        ├── RecommendationSummary.java
        ├── ReviewSummary.java
        └── ServiceAddresses.java

Java 接口类 ProductCompositeService.java 遵循核心服务使用的相同模式,如下所示:

package se.magnus.api.composite.product;

public interface ProductCompositeService {

    @GetMapping(
        value    = "/product-composite/{productId}",
        produces = "application/json")
    ProductAggregate getProduct(@PathVariable int productId);
}

模型类 ProductAggregate.java 比核心模型复杂一点,因为它包含推荐和评论列表的字段:

package se.magnus.api.composite.product;

public class ProductAggregate {
    private final int productId;
    private final String name;
    private final int weight;
    private final List<RecommendationSummary> recommendations;
    private final List<ReviewSummary> reviews;
    private final ServiceAddresses serviceAddresses;

其余的 API 类是普通的基于 POJO 的模型对象,并且具有与核心 API 的模型对象相同的结构。

特性

为了避免将核心服务的地址信息硬编码到复合微服务的源代码中,后者使用一个属性文件来存储有关如何查找核心服务的信息。属性文件 application.yml 如下所示:

server.port: 7000

app:
  product-service:
    host: localhost
    port: 7001
  recommendation-service:
    host: localhost
    port: 7002
  review-service:
    host: localhost
    port: 7003

如前所述,此配置将在本书后面被服务发现机制取代。

集成组件

我们来看第一部分复合微服务的实现,集成组件,ProductCompositeIntegration.java< /代码>。它使用 @Component 注释声明为 Spring Bean,并实现三个核心服务的 API 接口:

package se.magnus.microservices.composite.product.services;

@Component
public class ProductCompositeIntegration implements ProductService, RecommendationService, ReviewService {

集成组件使用 Spring Framework 中的帮助程序类 RestTemplate 来执行对核心微服务的实际 HTTP 请求。在将其注入集成组件之前,我们需要对其进行配置。我们在 main 应用程序类 ProductCompositeServiceApplication.java ,如下:

@Bean
RestTemplate restTemplate() {
   return new RestTemplate();
}

RestTemplate 对象是高度可配置的,但我们暂时保留它的默认值。

Chapter 2Spring WebFlux部分,Spring Boot简介,我们介绍了响应式 HTTP 客户端 WebClient。在本章中使用 WebClient 而不是 RestTemplate 需要所有使用 WebClient 的源代码也是响应式的,包括 API 项目中 RESTful API 的声明和组合中的源代码微服务。在第 7 章开发响应式微服务,我们将学习如何更改微服务的实现以遵循响应式编程模型。作为该更新中的步骤之一,我们将 RestTemplate 辅助类替换为 WebClient 类。但在我们了解 Spring 中的响应式开发之前,我们将使用 RestTemplate 类。

我们现在可以 注入 RestTemplate 以及 JSON mapper 用于在出现错误时访问错误消息,以及我们在属性文件中设置的配置值。让我们看看这是如何完成的:

  1. 对象和配置值被注入到构造函数中,如下所示:
    
    private final RestTemplate restTemplate;
    private final ObjectMapper mapper;
    
    private final String productServiceUrl;
    private final String recommendationServiceUrl;
    private final String reviewServiceUrl;
    
    @Autowired
    public ProductCompositeIntegration(
      RestTemplate restTemplate,
      ObjectMapper mapper,
    
      @Value("${app.product-service.host}") 
      String productServiceHost,
      
      @Value("${app.product-service.port}")
      int productServicePort,
    
      @Value("${app.recommendation-service.host}")
      String recommendationServiceHost,
    
      @Value("${app.recommendation-service.port}")
      int recommendationServicePort,
    
      @Value("${app.review-service.host}")
      String reviewServiceHost,
    
      @Value("${app.review-service.port}")
      int reviewServicePort
    )
    								
  2. 构造函数的主体存储注入的对象并根据注入的值构建URL,如下所示:
    {
      this.restTemplate = restTemplate;
      this.mapper = mapper;
    
      productServiceUrl = "http://" + productServiceHost + ":" + 
      productServicePort + "/product/";
      recommendationServiceUrl = "http://" + recommendationServiceHost
      + ":" + recommendationServicePort + "/recommendation?
      productId="; reviewServiceUrl = "http://" + reviewServiceHost + 
      ":" + reviewServicePort + "/review?productId=";
    }
    								
  3. 最后,集成组件通过 RestTemplate 实现三个核心服务的 API 方法进行实际的传出调用:
    
    public Product getProduct(int productId) {
     String url = productServiceUrl + productId;
     Product product = restTemplate.getForObject(url, Product.class);
     return product;
    }
    
    public List<Recommendation> getRecommendations(int productId) {
        String url = recommendationServiceUrl + productId;
        List<Recommendation> recommendations = 
        restTemplate.exchange(url, GET, null, new 
        ParameterizedTypeReference<List<Recommendation>>() 
        {}).getBody();
        return recommendations;
    }
    
    public List<Review> getReviews(int productId) {
        String url = reviewServiceUrl + productId;
        List<Review> reviews = restTemplate.exchange(url, GET, null,
        new ParameterizedTypeReference<List<Review>>() {}).getBody();
        return reviews;
    }
    				

    关于方法实现的一些有趣的注释

    1. 对于 getProduct() 实现,getForObject() 方法可以用在 RestTemplate 中。预期的响应是 Product 对象。它可以通过指定 来表示在对 getForObject() 的调用中RestTemplate 将 JSON 响应映射到的 Product.class 类。
    2. 对于 getRecommendations()getReviews(),一种更高级的方法,exchange()。其原因是从 JSON 响应到 RestTemplate 执行的模型类的自动映射。 getRecommendations()getReviews() 方法期望响应中的通用列表,即 List<Recommendation>列表<评论>。由于泛型在运行时不保存任何类型的信息,因此我们不能指定方法在其响应中期望泛型列表。相反,我们可以使用 Spring Framework 中的帮助程序类 ParameterizedTypeReference,旨在通过在运行时保存类型信息来解决此问题。这意味着 RestTemplate 可以确定将 JSON 响应映射到哪个类。要使用这个帮助类,我们必须使用更复杂的 exchange() 方法 RestTemplate 上更简单的 getForObject() 方法。

复合 API 实现

最后我们来看复合微服务实现的最后一个片段:API实现类ProductCompositeServiceImpl .java。让我们一步一步来:

  1. 与我们为核心服务所做的方式相同,复合服务实现其 API 接口 ProductCompositeService,并使用 @RestController 将其标记为 REST 服务:
    
    package se.magnus.microservices.composite.product.services;
    
    @RestController
    public class ProductCompositeServiceImpl implements ProductCompositeService {
    								
  2. 实现类需要 ServiceUtil bean 和它自己的集成组件,因此它们被注入到它的构造函数中:
    
    private final ServiceUtil serviceUtil;
    private ProductCompositeIntegration integration;
    
    @Autowired
    public ProductCompositeServiceImpl(ServiceUtil serviceUtil, ProductCompositeIntegration integration) {
        this.serviceUtil = serviceUtil;
        this.integration = integration;
    }
    								
  3. 最后,API方法实现如下:
    
    @Override
    public ProductAggregate getProduct(int productId) {
        
      Product product = integration.getProduct(productId);
      List<Recommendation> recommendations = 
      integration.getRecommendations(productId);
      List<Review> reviews = integration.getReviews(productId);
      
      return createProductAggregate(product, recommendations,
      reviews, serviceUtil.getServiceAddress());
    }
    								

集成组件用于调用三个核心服务,以及一个辅助方法createProductAggregate() ,用于根据对集成组件的调用的响应创建 ProductAggregate 类型的响应对象。

辅助方法 createProductAggregate() 的实现相当冗长且不是很重要,因此本章省略;但是,它可以在本书的源代码中找到。

集成组件和复合服务的完整实现可以在 Java 包 se.magnus.microservices.composite.product.services 中找到.

从功能的角度来看,这完成了复合微服务的实现。在下一节中,我们将看到我们如何处理错误。

添加错误处理

在微服务环境中,大量微服务使用同步 API(例如,使用 HTTP 和JSON。将特定于协议的错误处理(例如 HTTP 状态代码)与业务逻辑分开也很重要。

可以说,在实现微服务时应该为业务逻辑添加一个单独的层。这应该确保业务逻辑与特定于协议的代码分离,从而更容易测试和重用。为避免本书提供的示例不必要的复杂性,我们省略了单独的业务逻辑层,因此微服务直接在 @ 中实现其业务逻辑RestController 组件。

我在 API 实现和 API 客户端都使用的 util 项目中创建了一组 Java 异常,最初是 InvalidInputExceptionNotFoundException。查看 Java 包 se.magnus.util.exceptions 了解详细信息。

全局 REST 控制器异常处理程序

为了将特定协议的错误处理与 REST 控制器中的业务逻辑分开,即 API 实现,我创建了一个实用程序类 GlobalControllerExceptionHandler.java,在 util 项目中被注释为 @RestControllerAdvice

对于 API 实现抛出的每个 Java 异常,实用程序类都有一个异常处理程序方法,该方法将 Java 异常映射到正确的 HTTP 响应,即具有正确的 HTTP 状态和 HTTP 响应正文。

例如,如果 API 实现类抛出 InvalidInputException,实用程序类会将其映射到 HTTP 响应,并将状态代码设置为 422 (UNPROCESSABLE_ENTITY)。以下代码显示了这一点:

@ResponseStatus(UNPROCESSABLE_ENTITY)
@ExceptionHandler(InvalidInputException.class)
public @ResponseBody HttpErrorInfo handleInvalidInputException(
    ServerHttpRequest request, InvalidInputException ex) {

    return createHttpErrorInfo(UNPROCESSABLE_ENTITY, request, ex);
}

同理,NotFoundException 映射到 404 (NOT_FOUND) HTTP 状态码。

每当 REST 控制器抛出任何这些异常时,Spring 将使用实用程序类来创建 HTTP 响应。

注意 Spring 本身返回 HTTP 状态码 400 (BAD_REQUEST ) 检测到无效请求时,例如,如果请求包含非数字产品 ID(productId 指定为整数在 API 声明中)。

有关实用程序类的完整源代码,请参阅 GlobalControllerExceptionHandler.java

API 实现中的错误处理

API 实现使用 util 项目中的异常来 信号错误。它们将作为 HTTPS 状态代码报告给 REST 客户端,指示出了什么问题。比如Product微服务实现类,ProductServiceImpl.java ,使用 InvalidInputException 异常返回指示无效输入的错误,以及 NotFoundException 异常告诉我们所要求的产品不存在。代码 如下所示:

if (productId < 1) throw new InvalidInputException("Invalid productId:
    " + productId);
if (productId == 13) throw new NotFoundException("No product found for
    productId: " + productId);

由于我们目前没有使用数据库,我们必须模拟何时抛出 NotFoundException

API 客户端中的错误处理

API客户端,即Composite微服务的集成组件,一个 id="_idIndexMarker230"> 反向;它映射 422 (UNPROCESSABLE_ENTITY) HTTP 状态码到 InvalidInputException404 (NOT_FOUND) HTTP 状态码到 NotFoundException。请参阅 ProductCompositeIntegration.java 中的 getProduct() 方法用于执行此错误处理逻辑。源代码如下所示:

catch (HttpClientErrorException ex) {

    switch (ex.getStatusCode()) {

    case NOT_FOUND:
        throw new NotFoundException(getErrorMessage(ex));

    case UNPROCESSABLE_ENTITY:
        throw new InvalidInputException(getErrorMessage(ex));

    default:
        LOG.warn("Got an unexpected HTTP error: {}, will rethrow it",
        ex.getStatusCode());
        LOG.warn("Error body: {}", ex.getResponseBodyAsString());
        throw ex;
    }
}

getRecommendations()getReviews() 在集成组件中稍微宽松一点——归类为尽力而为,这意味着如果它成功获取产品信息但未能获得推荐或评论,它仍然被认为是 < /a>好的。但是,会在日志中写入警告。

对于 详细信息,请参阅 ProductCompositeIntegration.java

这样就完成了代码和复合微服务的实现。在下一节中,我们将测试微服务及其公开的 API。

手动测试 API

结束了我们微服务的实现。让我们通过执行以下步骤来尝试一下:

  1. 构建并启动微服务作为后台进程。
  2. 使用 curl 调用复合 API。
  3. 阻止他们。

首先,将每个微服务构建并启动为后台进程,如下:

cd $BOOK_HOME/Chapter03/2-basic-rest-services/
./gradlew build

构建完成后,我们可以使用以下代码将微服务作为后台进程启动到终端进程:

java -jar microservices/product-composite-service/build/libs/*.jar &
java -jar microservices/product-service/build/libs/*.jar &
java -jar microservices/recommendation-service/build/libs/*.jar &
java -jar microservices/review-service/build/libs/*.jar &

很多日志消息会写入终端,但几秒钟后,事情会平静下来,我们会发现以下消息写入日志:

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.6:应用程序启动后的日志消息

这意味着他们都准备好接收请求了。试试下面的代码:

curl http://localhost:7000/product-composite/1

在一些日志输出之后,我们将得到一个类似于以下内容的 JSON 响应:

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.7:请求后的 JSON 响应

要获得漂亮打印的 JSON 响应,您可以使用 jq 工具:

curl http://localhost:7000/product-composite/1 -s | jq .

这将产生以下输出(为了提高可读性,一些细节已被替换为 ...):

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.8:漂亮打印的 JSON 响应

如果您愿意,您 还可以尝试以下命令来验证错误处理是否按预期工作:

# Verify that a 404 (Not Found) error is returned for a non-existing productId (13)
curl http://localhost:7000/product-composite/13 -i

# Verify that no recommendations are returned for productId 113
curl http://localhost:7000/product-composite/113 -s | jq .

# Verify that no reviews are returned for productId 213
curl http://localhost:7000/product-composite/213 -s | jq .

# Verify that a 422 (Unprocessable Entity) error is returned for a productId that is out of range (-1)
curl http://localhost:7000/product-composite/-1 -i

# Verify that a 400 (Bad Request) error is returned for a productId that is not a number, i.e. invalid format
curl http://localhost:7000/product-composite/invalidProductId -i

最后,您可以使用以下命令关闭微服务:

kill $(jobs -p)

如果您使用的是 Visual Studio Code、Spring Tool Suite 或 IntelliJ IDEA Ultimate Edition 等 IDE,则可以使用它们对 Spring Boot Dashboard 的支持,一键启动和停止微服务。

以下 屏幕截图显示了在 Visual Studio Code 中使用 Spring Boot Dashboard:

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.9:Visual Studio Code 中的 Spring Boot 仪表板

以下屏幕截图显示了 Spring Tool Suite 中 Spring Boot Dashboard 的使用:

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.10:Spring Tool Suite 中的 Spring Boot 仪表板

以下 截图显示了 IntelliJ IDEA Ultimate Edition 中 Spring Boot Dashboard 的使用:

读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

图 3.11:IntelliJ IDEA Ultimate Edition 中的 Spring Boot Dashboard

在本节中,我们学习了如何手动启动、测试和停止协作微服务的系统环境。这些类型的测试非常耗时,因此它们显然需要自动化。在接下来的两节中,我们将迈出第一步,学习如何自动化测试,测试单独的单个微服务和协作微服务的整个系统环境。在本书中,我们将改进我们测试微服务的方式。

单独添加自动化微服务测试

我们结束实现之前,我们还需要编写一些自动化测试。

这个时候我们没有太多的业务逻辑要测试,所以不需要写任何单元测试。相反,我们将专注于测试我们的微服务公开的 API;也就是说,我们将在与他们的嵌入式 Web 服务器的集成测试中启动它们,然后使用测试客户端执行 HTTP 请求并验证响应。 Spring WebFlux 附带了一个测试客户端,WebTestClient,它提供了一个流畅的 API,用于发出请求,然后对其结果应用断言。

以下是我们通过执行以下测试来测试复合产品 API 的示例:

  • 为现有产品发送 productId 并断言我们返回 200 作为 HTTP 响应代码和 JSON 响应,其中包含请求的 productId 以及一条推荐和一条评论
  • 发送一个丢失的 productId 并断言我们找回了 404 作为 HTTP 响应代码和包含相关错误信息的 JSON 响应

这两个测试的实现如以下代码所示。第一个测试如下所示:

@Autowired
private WebTestClient client;

@Test
void getProductById() {
  client.get()
    .uri("/product-composite/" + PRODUCT_ID_OK)
    .accept(APPLICATION_JSON_UTF8)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(APPLICATION_JSON_UTF8)
    .expectBody()
    .jsonPath("$.productId").isEqualTo(PRODUCT_ID_OK)
    .jsonPath("$.recommendations.length()").isEqualTo(1)
    .jsonPath("$.reviews.length()").isEqualTo(1);
}

测试代码的工作方式如下:

  • 测试使用 fluent WebTestClient API 设置 URL 调用 "/product-composite/" + PRODUCT_ID_OK 并指定接受的响应格式 JSON。
  • 使用 exchange() 方法执行请求后,测试验证响应状态为 OK (200) 并且响应格式实际上是 JSON(根据要求)。
  • 最后,测试检查响应正文并验证它是否包含 productId 方面的预期信息以及推荐和评论的数量。

第二个 测试如下所示:

@Test
public void getProductNotFound() {
  client.get()
    .uri("/product-composite/" + PRODUCT_ID_NOT_FOUND)
    .accept(APPLICATION_JSON_UTF8)
    .exchange()
    .expectStatus().isNotFound()
    .expectHeader().contentType(APPLICATION_JSON_UTF8)
    .expectBody()
    .jsonPath("$.path").isEqualTo("/product-composite/" +
     PRODUCT_ID_NOT_FOUND)
    .jsonPath("$.message").isEqualTo("NOT FOUND: " +
     PRODUCT_ID_NOT_FOUND);
}

关于此测试代码的一个重要说明是:

  • 这个否定测试在结构上与前面的测试非常相似;主要区别在于它验证是否返回了错误状态代码,Not Found (404),并且响应正文包含预期的错误消息。

为了单独测试复合产品API,我们需要模拟它的依赖关系,即集成组件执行的对其他三个微服务的请求, ProductCompositeIntegration。我们使用 Mockito 来这样做,如下:

private static final int PRODUCT_ID_OK = 1;
private static final int PRODUCT_ID_NOT_FOUND = 2;
private static final int PRODUCT_ID_INVALID = 3;
@MockBean
private ProductCompositeIntegration compositeIntegration;

@BeforeEach
void setUp() {

  when(compositeIntegration.getProduct(PRODUCT_ID_OK)).
    thenReturn(new Product(PRODUCT_ID_OK, "name", 1, "mock-address"));
  when(compositeIntegration.getRecommendations(PRODUCT_ID_OK)).
    thenReturn(singletonList(new Recommendation(PRODUCT_ID_OK, 1,
    "author", 1, "content", "mock address")));
     when(compositeIntegration.getReviews(PRODUCT_ID_OK)).
    thenReturn(singletonList(new Review(PRODUCT_ID_OK, 1, "author",
    "subject", "content", "mock address")));

  when(compositeIntegration.getProduct(PRODUCT_ID_NOT_FOUND)).
    thenThrow(new NotFoundException("NOT FOUND: " +
    PRODUCT_ID_NOT_FOUND));

  when(compositeIntegration.getProduct(PRODUCT_ID_INVALID)).
    thenThrow(new InvalidInputException("INVALID: " +
    PRODUCT_ID_INVALID));
}

模拟 实现的工作方式如下:

  • 首先,我们声明测试类中使用的三个常量:PRODUCT_ID_OKPRODUCT_ID_NOT_FOUNDPRODUCT_ID_INVALID
  • 接下来,使用注解 @MockBean 来配置 Mockito 为 ProductCompositeIntegration 接口。
  • 如果 getProduct(), getRecommendations(),和 getReviews() 方法在集成组件上调用,productId 设置为 PRODUCT_ID_OK,mock 会返回正常响应。
  • 如果使用 productId 调用 getProduct() 方法设置为 PRODUCT_ID_NOT_FOUND,模拟将抛出 NotFoundException
  • 如果使用 productId 调用 getProduct() 方法设置为 PRODUCT_ID_INVALID,模拟将抛出 InvalidInputException

复合产品 API 的自动化集成测试的完整源代码可以在测试类 ProductCompositeServiceApplicationTests.java 中找到。

三个核心微服务公开的 API 的自动化集成测试类似,但更简单,因为它们不需要模拟任何东西!测试的源代码可以在每个微服务的 test 文件夹中找到。

在执行构建时,Gradle 会自动运行测试

./gradlew build

但是,您可以指定您只想运行测试(而不是构建的其余部分):

./gradlew test

这是关于如何单独为微服务编写自动化测试的介绍。在下一节中,我们将学习如何编写自动测试微服务环境的测试。在本章中,这些测试将仅是半自动化的。在接下来的章节中,测试将完全自动化,这是一项重大改进。

添加微服务环境的半自动化测试

能够使用纯Java、JUnit和Gradle自动为每个微服务单独运行单元和集成测试在开发过程中非常有用,但是当我们转移到操作端时就不够了.在操作中,我们还需要一种方法来自动验证协作微服务的系统环境是否满足我们的期望。能够在任何时候运行一个脚本来验证许多协作的微服务在操作中是否都按预期工作是非常有价值的——微服务越多,这种验证脚本的价值就越高。

出于这个原因,我编写了一个简单的 bash 脚本,它可以通过调用 RESTful API 来验证已部署系统环境的功能微服务。它基于我们在上面学习和使用的 curl 命令。该脚本使用 jq 验证返回代码和部分 JSON 响应。该脚本包含两个辅助函数,assertCurl()assertEqual() ,使测试代码简洁易读。

例如,发出一个正常的请求并期望 200 作为状态码,以及断言我们得到一个返回请求的 JSON 响应productId 以及三个建议和三个评论,如下所示:

# Verify that a normal request works, expect three recommendations and three reviews
assertCurl 200 "curl http://$HOST:${PORT}/product-composite/1 -s"
assertEqual 1 $(echo $RESPONSE | jq .productId)
assertEqual 3 $(echo $RESPONSE | jq ".recommendations | length")
assertEqual 3 $(echo $RESPONSE | jq ".reviews | length")

验证 我们得到 404 (Not Found) 作为 HTTP 响应代码(当我们尝试查找不存在的产品)如下所示:

# Verify that a 404 (Not Found) error is returned for a non-existing productId (13)
assertCurl 404 "curl http://$HOST:${PORT}/product-composite/13 -s"

测试脚本 test-em-all.bash 实现了在 部分中描述的手动测试手动测试 API,可以在顶级文件夹 $BOOK_HOME/Chapter03/2-basic-rest-services 中找到。随着我们在后面的章节中为系统环境添加更多功能,我们将扩展测试脚本的功能。

第 20 章监控微服务中,我们将学习用于自动监控运行中的系统环境的补充技术。在这里,我们将了解一个监控工具,它可以持续监控已部署微服务的状态,以及如果收集的指标超过配置的阈值(例如 CPU 或内存的过度使用)如何引发警报。

试用测试脚本

要试用测试脚本,请执行 以下步骤:

  1. 首先,启动微服务,就像我们之前所做的那样:
    cd $BOOK_HOME/Chapter03/2-basic-rest-services java -jar microservices/product-composite-service/build/libs/*.jar & java -jar microservices/product-service/build/libs/*.jar & java -jar microservices/recommendation-service/build/libs/*.jar & java -jar microservices/review-service/build/libs/*.jar & 
  2. Once they've all started up, run the test script:
    ./test-em-all.bash 

    预计 输出类似于以下内容:

    读书笔记《第一部分 使用 Spring Boot 开始微服务开发》第3章创建一组协作微服务

    图 3.12:运行测试脚本后的输出

  3. 通过使用以下命令关闭微服务来完成此操作:
    kill $(jobs -p) 

在本节中,我们已经迈出了对协作微服务的系统环境进行自动化测试的第一步,所有这些都将在接下来的章节中得到改进。

概括

我们现在已经使用 Spring Boot 构建了最初的几个微服务。在介绍了我们将在本书中使用的微服务环境之后,我们学习了如何使用 Spring Initializr 为每个微服务创建框架项目。

接下来,我们学习了如何使用 Spring WebFlux 为三个核心服务添加 API,并实现了一个组合服务,该服务使用三个核心服务的 API 来创建它们中信息的聚合视图。复合服务使用 Spring Framework 中的 RestTemplate 类对核心服务公开的 API 执行 HTTP 请求。在服务中添加错误处理逻辑后,我们在微服务环境中运行了一些手动测试。

我们通过学习如何单独为微服务添加测试以及它们何时作为系统环境一起工作来结束本章。为了为复合服务提供受控隔离,我们使用 Mockito 模拟了它对核心服务的依赖关系。整个系统环境的测试由 Bash 脚本执行,该脚本使用 curl 来执行对复合服务 API 的调用。

有了这些技能,我们就准备好迈出下一步,在下一章进入 Docker 和容器的世界!除其他外,我们将学习如何使用 Docker 来完全自动化测试协作微服务的系统环境。

问题

  1. 使用 spring init Spring Initializr CLI 工具创建新 Spring Boot 项目时列出可用依赖项的命令是什么?
  2. 如何设置 Gradle 以使用一个命令构建多个相关项目?
  3. 使用的 @PathVariable@RequestParam 注释是什么为了?
  4. 如何在 API 实现类中将特定于协议的错误处理与业务逻辑分开?
  5. Mockito 是做什么用的?