vlambda博客
学习文章列表

读书笔记《hands-on-cloud-native-microservices-with-jakarta-ee》使用Vert.X构建微服务

Building Microservices Using Vert.X

第2章中, 微服务和反应式架构,我们谈到了反应式系统和反应式架构,以及这种方法与传统的 Java EE/Jakarta EE 开发模型有何不同。

主要区别之一是响应式开发模型默认是事件驱动和异步的。所以,没有办法实现I/O阻塞代码,内核线程的使用率很低。

为了比较两个架构平台做出的不同开发选择,我们将尝试分析如何通过Eclipse Vert.x实现前几章开发的相同微服务。

您将在此 GitHub 存储库中找到本章中描述的代码 https://github.com/PacktPublishing/Hands-On-Cloud-Native-Microservices-with-Jakarta-EE/tree/master/appendix-B

Vert.x

Vert.x 是一个开源 Eclipse 工具包,用于构建分布式和反应式系统,通过其反应式流原则的实现,提供了一种灵活的方式来编写轻量级和响应式的应用程序。

它被设计为云原生:它允许许多进程以很少的资源(线程、CPU 等)运行。通过这种方式,Vert.x 应用程序可以在云环境中更有效地使用其 CPU 配额。不会因为创建大量新线程而导致不必要的开销。

它定义了一个基于事件循环的异步和非阻塞开发模型,该模型在客户端处理请求并避免长时间等待,而服务器端则因大量调用而受到压力。

由于它是一个工具包而不是一个框架,因此 Vert.x 可以作为一个典型的第三方库,您可以自由选择您的目标所需的组件。

您可以将 Vert.x 与多种语言一起使用,包括 Java、JavaScript、Groovy、Ruby、Ceylon、Scala 和 Kotlin。在我们的示例中,我们将使用 Java 语言。

Maven settings

Apache Maven 可能是最常用的构建管理系统——我们可以将其视为 Java 应用程序构建和打包操作的事实标准。在本节中,我们将使用它来构建和打包我们的应用程序。

Vert.x 与 Apache Maven 3.2 或更高版本兼容,为了轻松管理所有 Vert.x 依赖项,特别是正确的版本,您可以将 Maven POM 文件设置为从 vertx-stack-depchain< /kbd> 物料清单。

以下是您可以在 Vert.x 应用程序中使用的 pom.xml 示例:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

...

<properties>
   <java.version>1.8</java.version>
    <maven-compiler-plugin.version>3.5.1</maven-compiler-plugin.version>
    <maven-shade-plugin.version>2.4.3</maven-shade-plugin.version>
    <maven-surefire-plugin.version>2.21.0</maven-surefire-plugin.version>
    <exec-maven-plugin.version>1.5.0</exec-maven-plugin.version>
    <vertx.version>3.5.4</vertx.version>
    ...
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-stack-depchain</artifactId>
            <version>${vertx.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    ...
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>${maven-shade-plugin.version}</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <transformers>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <manifestEntries>
                                <Main-Class>io.vertx.core.Launcher</Main-Class>
                                <Main-Verticle>${main.verticle}</Main-Verticle>
                            </manifestEntries>
                            </transformer>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                <resource>
                                    META-INF/services/io.vertx.core.spi.VerticleFactory
                                </resource>
                            </transformer>
                        </transformers>
                        <outputFile>
                    ${project.build.directory}/${project.artifactId}-${project.version}-fat.jar
                        </outputFile>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
</build>
</project>

之后,像往常一样,您将能够使用以下命令构建和打包您的应用程序:

$ mvn clean package

Gradle settings

Gradle 是一个开源构建自动化工具,它使用用 Groovy 或 Kotlin DSL 编写的脚本。它受到主要 IDE 的支持,您可以使用命令行界面或通过持续集成服务器运行它。

安装后,您可以通过从项目的根目录启动以下命令来创建新项目或自动将现有 Maven 项目转换为 Gradle 项目:

$ gradle init

为了在 Gradle 项目中使用 Vert.x,您可以创建一个 Gradle 文件,如下所示:

plugins {
    id 'java'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '2.0.4'
}
ext {
    vertxVersion = '3.5.4'
    junitJupiterEngineVersion = '5.2.0'
}

repositories {
    mavenLocal()
    jcenter()
}

group = 'com.packtpub.vertx'
version = '1.0.0-SNAPSHOT'

sourceCompatibility = '1.8'
mainClassName = 'io.vertx.core.Launcher'

def mainVerticleName = 'com.packtpub.vertx.football-player-microservice.MainVerticle'
def watchForChange = 'src/**/*'
def doOnChange = './gradlew classes'

dependencies {
    implementation "io.vertx:vertx-core:$vertxVersion"
    implementation "io.vertx:vertx-config:$vertxVersion"
...
}

shadowJar {
    classifier = 'fat'
    manifest {
        attributes 'Main-Verticle': mainVerticleName
    }
    mergeServiceFiles {
        include 'META-INF/services/io.vertx.core.spi.VerticleFactory'
    }
}
...
run {
    args = ['run', mainVerticleName, "--redeploy=$watchForChange", "--launcher- 
        class=$mainClassName", "--on-redeploy=$doOnChange"]
}

...

此代码段将添加 Vert.x 版本的声明作为外部依赖项 - 这样,您不需要具有特定版本,因为它们在 BOM 文件中隐式定义。

要构建可执行 JAR,您可以执行以下命令:

$ ./gradlew shadowJar

之后,您可以通过执行以下命令来运行它:

$ java -jar build/libs/gradle-my-vertx-project.jar

或者,您可以改为执行以下 Gradle 命令:

$ ./gradlew run

Building a football player microservice

在本节中,我将实现我们在 第 4 章中看到的相同的足球运动员微服务构建, 使用 Thorntail 构建微服务。我将分析应用程序的服务器端层的细节,(因为客户端层保持不变,使用 Angular 6 实现)。

结果将是一个处理足球运动员域的简单足球运动员微服务:它将公开 CRUD API,并将使用 PostgreSQL 数据库存储和检索信息。

您可以完成我们在 第 4 章中看到的整个应用程序构建, 使用 Thorntail 构建微服务, 使用我们将在此处实现的相同方法。

为了构建应用程序,我将使用以下工具,并为每个工具指定安装它所需的信息:

Project details

在本节中,我将为我们的微服务构建源代码。为了做到这一点,除了前面描述的先决条件之外,我需要在系统上安装 PostgreSQL。正如我之前所说,我使用 Docker 来安装和处理 PostgreSQL。我使用 macOS High Sierra 作为我的工作环境;如果您使用 Thorntail 实现了相同的项目,如 第 4 章, 使用 Thorntail 构建微服务,请随意跳过本节。否则,请按照说明如何在 Docker 容器中安装和运行 PostgreSQL 的说明进行操作。

Database installation and configuration

在你的机器上安装 Docker 之后,是时候运行 PostgreSQL 的容器化版本了。为此,请打开一个新终端 window 并启动以下命令:

$ docker run --name postgres_vertx -e POSTGRES_PASSWORD=postgresPwd -e POSTGRES_DB=football_players_registry -d -p 5532:5432 postgres

此命令触发从 Docker 的公共注册表中提取标记为最新的 PostgreSQL 版本,下载运行容器所需的所有层,如下所示:

Unable to find image 'postgres:latest' locally
latest: Pulling from library/postgres
683abbb4ea60: Pull complete
c5856e38168a: Pull complete
c3e6f1ceebb0: Pull complete
3303bcd00128: Pull complete
ea95ff44bf6e: Pull complete
ea3f31f1e620: Pull complete
234873881fb2: Pull complete
f020aa822d21: Pull complete
27bad92d09a5: Pull complete
6849f0681f5a: Pull complete
a112faac8662: Pull complete
bc92d0ab9365: Pull complete
9e87959714b8: Pull complete
ac7c29b2bea7: Pull complete
Digest: sha256:d99f15cb8d0f47f0a66274afe30102b5bb7a95464d1e25acb66ccf7bd7bd8479
Status: Downloaded newer image for postgres:latest
83812c6e76656f6abab5bf1f00f07dca7105d5227df3b3b66382659fa55b5077

之后,PostgreSQL 镜像作为容器启动。要验证它,您可以启动 $ docker ps -a 命令,它会为您提供已创建容器的列表和相关状态:

CONTAINER ID IMAGE COMMAND CREATED

1073daeefc52 postgres "docker-entrypoint.s..." Less than a second ago

STATUS PORTS NAMES

Up 4 seconds 0.0.0.0:5532->5432/tcp postgres_vertx

我不得不将命令结果分成两行以使其可读。

您还可以检查容器日志以检索有关 PostgreSQL 状态的信息。启动以下命令:

$ docker logs -f 1073daeefc52

这里,1073daeefc52 是容器 ID。您应该找到以下信息:

PostgreSQL init process complete; ready for start up.

2018-07-13 22:53:36.465 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
2018-07-13 22:53:36.466 UTC [1] LOG: listening on IPv6 address "::", port 5432
2018-07-13 22:53:36.469 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"

现在是时候连接到容器来管理它了。启动以下命令:

$ docker exec -it 1073daeefc52 bash

这里,1073daeefc52 是容器 ID。现在使用以下命令登录 PostgreSQL:

$ psql -U postgres

现在您可以与数据库服务器交互:

psql (10.4 (Debian 10.4-2.pgdg90+1))
Type "help" for help.
postgres=#

您应该能够看到我们在创建容器时创建的 football_players_registry 数据库。运行 \l 命令并验证数据库列表:

读书笔记《hands-on-cloud-native-microservices-with-jakarta-ee》使用Vert.X构建微服务

好的,是时候创建我们的简单表来托管足球运动员的数据了。使用以下命令连接到 football_players 数据库:

$ \connect football_players_registry

并使用以下命令创建表:

CREATE TABLE FOOTBALL_PLAYER(
ID SERIAL PRIMARY KEY NOT NULL,
NAME VARCHAR(50) NOT NULL,
SURNAME VARCHAR(50) NOT NULL,
AGE INT NOT NULL,
TEAM VARCHAR(50) NOT NULL,

POSITION VARCHAR(50) NOT NULL,
PRICE NUMERIC
);

使用以下命令检查表结构:

$ \d+ football_player

您应该看到以下结果:

读书笔记《hands-on-cloud-native-microservices-with-jakarta-ee》使用Vert.X构建微服务

Creating the source code

我已经安装并配置了创建微服务以管理玩家注册表所需的一切。现在是时候编写公开我们的微服务 API 所需的代码了。

我使用 Vert.x 项目生成器实用程序 http://start.vertx.io/ ,以便让项目骨架开始工作。如前所述,微服务必须显示允许我们执行 CRUD 操作的 API。

为了实现微服务,我将使用以下组件:

  • Core: As the name suggested it contains the core functionalities as the support for HTTP
  • Config: Provides an extensible way to configure Vert.x applications
  • Web: Toolkit that contains utilities required to build web applications and HTTP microservices
  • JDBC client: This component enables developers to communicate with any JDBC-compliant database but with a different approach, the asynchronous API

我们将使用 com.packtpub.vertx 作为项目的 Maven Group 名称,并使用 footballplayermicroservice 作为 工件。在项目表单生成器中设置这些值,如以下屏幕截图所示:

读书笔记《hands-on-cloud-native-microservices-with-jakarta-ee》使用Vert.X构建微服务

单击 Generate Project 以创建和下载带有项目骨架的 ZIP 文件。

将文件解压缩到您选择的目录中,然后使用您最喜欢的 IDE(Eclipse、NetBeans、IntelliJ 等)打开 Maven 项目。

该项目的核心元素是 Maven pom.xml 文件,其中包含实现我们的微服务所需的所有依赖项。

项目使用vertx-stack-depchain来正确管理dependencyManagement

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-stack-depchain</artifactId>
            <version>${vertx.version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
    </dependencies>
</dependencyManagement>

我们还需要在 Maven pom.xml 文件中添加 PostgreSQL JDBC 驱动程序依赖项,以便连接到用于存储足球运动员信息的数据库:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>9.4.1212</version>
</dependency>

现在是时候使用这个命令启动第一个构建了:

$ mvn clean package

之后,您可以使用以下命令测试您的应用程序是否已启动并正在运行:

$ java -jar $PROJECT_HOME/footballplayermicroservice/target/footballplayermicroservice-1.0.0-SNAPSHOT-fat.jar

在这里,$PROJECT_HOME 变量是您必须解压缩 Vert.x 项目生成器实用程序生成的项目的路径。您将可视化这样的输出:

HTTP server started on http://localhost:8080
Oct 11, 2018 10:41:06 AM io.vertx.core.impl.launcher.commands.VertxIsolatedDeployer
INFO: Succeeded in deploying verticle

http://localhost:8080 URL 放入浏览器,您将看到以下消息:Hello from Vert.x!

现在让我们使用 Ctrl + C 命令停止我们的应用程序,并开始更新我们的项目。

The data access layer

Vert.x 没有使用 JPA 规范的模块,因此我们不会创建实体。

相反,我们需要一个将足球运动员的属性从数据库传输到数据库的对象。为此,我们创建了一个名为 FootballPlayer 的数据传输对象,以便执行此操作:

package com.packtpub.vertx.footballplayermicroservice.model;

import java.io.Serializable;

import java.math.BigInteger;

public class FootballPlayer implements Serializable {

    private static final long serialVersionUID = -92346781936044228L;

    private Integer id;

    private String name;

    private String surname;

    private int age;

    private String team;

    private String position;

    private BigInteger price;

    public FootballPlayer() {
    }

    public FootballPlayer(Integer id, String name, String surname, int age,
        String team, String position, BigInteger price) {
        this.id = id;
        this.name = name;
        this.surname = surname;
        this.age = age;
        this.team = team;
        this.position = position;
        this.price = price;
    }

    // Getters and Setters
    ...
}

第二步是创建一个配置文件,其中包含与数据库建立 JDBC 连接所需的参数。在 src/main/conf 目录中,放置一个名为 my-application-conf.json 的文件,其参数如下:

  • url: The JDBC URL to connect to the database
  • driver_class: The JDBC driver needed to use JDBC API
  • user: The username used to connect to the database
  • password: The password used to connect to the database

这是一个简单的场景——在生产环境中,您必须屏蔽 JDBC 凭据。

以下是该文件的快照:

{
    "url": "jdbc:postgresql://localhost:5532/football_players_registry",
    "driver_class": "org.postgresql.Driver",
    "user": "postgres",
    "password": "postgresPwd"
}

然后我们将在 src/main/resources 文件夹中创建 schema.sqldata.sql 两个文件,以便创建所需的表结构并将数据预加载到我们的数据库中。

您将在 GitHub 存储库中找到代码。

最后一步是构建一个负责与数据库交互的类,以执行以下操作:

  • Create database tables
  • Preload a set of data
  • CRUD operations plus the findAll method

以下片段显示了我们的类:

public class FootballPlayerDAO {

    public Future<FootballPlayer> insert(SQLConnection connection,
        FootballPlayer footballPlayer, boolean closeConnection) {
        
        Future<FootballPlayer> future = Future.future();
        String sql = "INSERT INTO football_player (name, surname, age, team,
            position, price) VALUES (?, ?, ?, ?, ?, ?)";

        connection.updateWithParams(sql, new JsonArray().add(footballPlayer.getName())
            .add(footballPlayer.getSurname())
            .add(footballPlayer.getAge()).add(footballPlayer.getTeam())
            .add(footballPlayer.getPosition())
            .add(footballPlayer.getPrice().intValue()),
             ar -> { 
                 if (closeConnection) {
                     connection.close();
                 }
                 future.handle(ar.map(res -> new FootballPlayer(res.getKeys().getInteger(0),
                     footballPlayer.getName(), footballPlayer.getSurname(),
                     footballPlayer.getAge(), footballPlayer.getTeam(),
                     footballPlayer.getPosition(), footballPlayer.getPrice())));
             });
         return future;
     }

     public Future<SQLConnection> connect(JDBCClient jdbc) {
         Future<SQLConnection> future = Future.future();
         jdbc.getConnection(ar -> future.handle(ar.map(c -> c.setOptions(
             new SQLOptions().setAutoGeneratedKeys(true))))
         );
         return future;
     }

...

}

如您所见,Vert.x 使用不同的方法进行数据库交互。该模式不同于您通常与 Java JDBC API 一起使用的传统模式。

以下是全球范围内的传统方法:

String sql = "SELECT * FROM MY_TABLE";
ResultSet rs = stmt.executeQuery(sql);

然后变成如下:

connection.query("SELECT * FROM Products", result -> {
// do something with the result
});

在 Vert.x 中,通常在所有反应式架构中,所有操作都是异步的,它们由 Future 类处理。

The RESTful web service

在本节中,我们将探讨如何公开 RESTful API。

Vert.x 中的主要单元是 verticle——使用这个类,我们将能够始终以异步方式处理对数据的请求。

我们将创建两个类:构建与特定 HTTP 动词关联的响应所需的 helper 和公开 API 的 verticle 类。

ActionHelper 类是一个简单的实用程序类,可以帮助我们构建响应:

public class ActionHelper {
    /**
     * Returns a handler writing the received {@link AsyncResult} to the routing
     * context and setting the HTTP status to the given status.
     *
     * @param context the routing context
     * @param status the status
     * @return the handler
     */
     private static <T> Handler<AsyncResult<T>> writeJsonResponse(RoutingContext context, int  
         status) {
         return ar -> {
             if (ar.failed()) {
                 if (ar.cause() instanceof NoSuchElementException) {
                     context.response().setStatusCode(404).end(ar.cause().getMessage());
                 } else {
                     context.fail(ar.cause());
                 }
             } else {
                 context.response().setStatusCode(status).putHeader("content-type",   
                     "application/json;charset=utf-8")
                     .end(Json.encodePrettily(ar.result()));
             }
         };
     }

     public static <T> Handler<AsyncResult<T>> ok(RoutingContext rc) {
         return writeJsonResponse(rc, 200);
     }

     public static <T> Handler<AsyncResult<T>> created(RoutingContext rc) {
         return writeJsonResponse(rc, 201);
     }

     public static Handler<AsyncResult<Void>> noContent(RoutingContext rc) {
         return ar -> {
             if (ar.failed()) {
                 if (ar.cause() instanceof NoSuchElementException) {
                     rc.response().setStatusCode(404).end(ar.cause().getMessage());
                 } else {
                     rc.fail(ar.cause());
                 }
             } else {
                 rc.response().setStatusCode(204).end();
             }
         };
     }

     private ActionHelper() {
     }
}

此类使用 RoutingContext 的实例来处理请求并构建响应,其中包含 HTTP 返回代码和对象表示,基于请求使用的 HTTP 动词。

helper 类将被 verticle 类使用,在我们的例子中是 FootballPlayerVerticle,它实现路由并公开 API。

让我们开始分析它。在代码的第一部分,我们将执行以下操作:

  1. Create an HTTP server that listens to the 8080 port.
  1. Make it available to handle requests to the path to our APIs.
  1. Also, define a way to create our database tables, and preload data:
public class FootballPlayerVerticle extends AbstractVerticle {

    private JDBCClient jdbc;

    @Override
    public void start(Future<Void> fut) {
        // Create a router object.
        Router router = Router.router(vertx);

        // Point 2
        router.route("/").handler(routingContext -> {
            HttpServerResponse response = routingContext.response();
            response.putHeader("content-type", "text/html")
            .end("<h1>Football Player Vert.x 3 microservice application</h1>");
        });

        router.get("/footballplayer").handler(this::getAll);
        router.get("/footballplayer/show/:id").handler(this::getOne);
        router.route("/footballplayer*").handler(BodyHandler.create());
        router.post("/footballplayer/save").handler(this::addOne);
        router.delete("/footballplayer/delete/:id").handler(this::deleteOne);
        router.put("/footballplayer/update/:id").handler(this::updateOne);

        // Point 3
        ConfigStoreOptions fileStore = new ConfigStoreOptions().setType("file")
            .setFormat("json").setConfig(new JsonObject().put("path",
                "src/main/conf/my-application-conf.json"));

        ConfigRetrieverOptions options = new ConfigRetrieverOptions().addStore(fileStore);
        ConfigRetriever retriever = ConfigRetriever.create(vertx, options);

        // Start sequence:
        // 1 - Retrieve the configuration
        // |- 2 - Create the JDBC client
        // |- 3 - Connect to the database (retrieve a connection)
        // |- 4 - Create table if needed
        // |- 5 - Add some data if needed
        // |- 6 - Close connection when done
        // |- 7 - Start HTTP server
        // |- 8 - we are done!
        ConfigRetriever.getConfigAsFuture(retriever).compose(config -> {
            jdbc = JDBCClient.createShared(vertx, config, "Players-List");
            FootballPlayerDAO dao = new FootballPlayerDAO();
            return dao.connect(jdbc).compose(connection -> {
                Future<Void> future = Future.future();
                createTableIfNeeded(connection).compose(this::createSomeDataIfNone)
                    .setHandler(x -> {
                connection.close();
                future.handle(x.mapEmpty());
            });
            return future;
        })

        // Point 1
        .compose(v -> createHttpServer(config, router));
        })
       .setHandler(fut);
    }

    // Point 1
    private Future<Void> createHttpServer(JsonObject config, Router router) {
        Future<Void> future = Future.future();
        vertx.createHttpServer().requestHandler(router::accept)
            .listen(config.getInteger("HTTP_PORT", 8080),
                res -> future.handle(res.mapEmpty()));
        return future;
    }

    // Point 3
    private Future<SQLConnection> createTableIfNeeded(SQLConnection connection) {
        FootballPlayerDAO dao = new FootballPlayerDAO();
        return dao.createTableIfNeeded(vertx.fileSystem(), connection);
    }   

    // Point 3
    private Future<SQLConnection> createSomeDataIfNone(SQLConnection connection) {
        FootballPlayerDAO dao = new FootballPlayerDAO();
        return dao.createSomeDataIfNone(vertx.fileSystem(), connection);
    }

    ...
}

在前面描述的代码中,我设置了 // Point X 注释以突出显示我们实现了前面列表中描述的点的位置。像往常一样,所有操作都是异步的,并返回一个 Future 类型。

对于 HTTP 服务器处理的所有路径,都有一个方法负责拦截请求、联系数据库、执行操作并返回响应。

例如:对 /footballplayer/show/:id 路径的调用是这样映射的:

router.get("/footballplayer/show/:id").handler(this::getOne);

这意味着有一个方法,getOne,它将处理请求并返回响应:

private void getOne(RoutingContext rc) {
    String id = rc.pathParam("id");
    FootballPlayerDAO dao = new FootballPlayerDAO();
    dao.connect(jdbc).compose(connection -> dao.queryOne(connection, id)).setHandler(ok(rc));
}

行为很简单:DAO 类将查询数据库,使用 queryOne 方法,并使用我们实用程序 ok 方法>helper 类,将使用 200 HTTP 代码和我们足球运动员的 JSON 表示来构建响应。

让我们尝试一下——使用以下命令启动应用程序:

$ java -jar $PROJECT_HOME/target/footballplayermicroservice-1.0.0-SNAPSHOT-fat.jar

然后调用 API:

$ curl http://localhost:8080/footballplayer/show/1 | json_pp

您将找回您的足球运动员:

{
    "team" : "Paris Saint Germain",
    "id" : 1,
    "name" : "Gianluigi",
    "age" : 40,
    "price" : 2,
    "surname" : "Buffon",
    "position" : "goalkeeper"
}

Creating the test code

我们已经实现了我们的微服务,但是为了提高它的质量并使未来的代码进化更容易,测试它是非常重要的。

Vert.x 让开发人员有机会轻松集成 JUnit 以实现强大的测试套件。在我们的示例中,我们将实现一个集成测试,以验证我们代码的良好行为,并查看它的实际效果。

要使用 JUnit,您需要在 Maven pom.xml 中有这些依赖项:

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-unit</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>${junit-platform-launcher.version}</version>
    <scope>test</scope>
</dependency>

然后我们应该准备好创建我们的 test 类来验证我们的 API 的结果:

@ExtendWith(VertxExtension.class)
public class TestFootballPlayerVerticle {

    @BeforeEach
    void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
        vertx.deployVerticle(new FootballPlayerVerticle(), testContext.
        succeeding(id -> testContext.completeNow()));
    }

    @Test
    @DisplayName("Should start a Web Server on port 8080 and the GET all API"
        + "returns an array of 24 elements")
    @Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
    void findAll(Vertx vertx, VertxTestContext testContext) throws Throwable {
        System.out.println("FIND ALL *****************");
        vertx.createHttpClient().getNow(8080, "localhost", "/footballplayer",
        response -> testContext.verify(() -> {
        assertTrue(response.statusCode() == 200);
        response.bodyHandler(body -> {
            JsonArray array = new JsonArray(body);
            assertTrue(23 == array.size());
            testContext.completeNow();
            });
        }));
    } 

    @Test
    @DisplayName(
        "Should start a Web Server on port 8080 and, using the POST API,"
        + "insert a new football player")
    @Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
    public void create(Vertx vertx, VertxTestContext context) {
        System.out.println("CREATE *****************");
        final String json = Json.encodePrettily(new FootballPlayer(null, "Mauro", "Vocale", 38,
                "Juventus", "central midfielder", new BigInteger("100")));
        final String length = Integer.toString(json.length());

        vertx.createHttpClient().post(8080, "localhost", "/footballplayer/save")
            .putHeader("content-type", "application/json")
            .putHeader("content-length", length)
            .handler(response -> {
                 assertTrue(response.statusCode() == 201);
                 assertTrue(response.headers().get("content-type").contains("application/json"));
                 response.bodyHandler(body -> {
                     final FootballPlayer footballPlayer = Json.decodeValue(
                         body.toString(), FootballPlayer.class);
                     assertTrue(footballPlayer.getName().equalsIgnoreCase("Mauro"));
                     assertTrue(footballPlayer.getAge() == 38);
                     assertTrue(footballPlayer.getId() != null);
                     context.completeNow();
                 });
             }).write(json).end();
    }
...

}

为了运行我们的测试,我们使用了 VertxExtension 类,它允许注入 Vert.x 和 VertxTestContext 参数,以及在 VertxTestContext 上的自动生命周期 实例。

使用 @BeforeEach 注释,我们将例程设置为在每个测试执行之前执行 verticle 类的部署,以便获得执行所需的数据。

然后,在每个方法中,我们使用 vertx.createHttpClient() 方法创建了一个 HTTP 客户端,我们对它执行 HTTP 动词操作(GETPOST PUTDELETE)。

我们设置了调用所需的参数并验证了断言。

现在您已准备好使用以下命令启动测试:

$ mvn test

你会看到所有的测试都通过了:

[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.677 s - in com.packtpub.vertx.footballplayermicroservice.TestFootballPlayerVerticle
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

The football player microservice – Vert.x + RxJava

我们已经实现了我们的足球运动员微服务,以使其异步且非 I/O 阻塞。但是如果我们一起使用 Vert.x 和 RxJava,我们可以获得更多的东西。

RxJava 是用于 Java 编程语言的 Rx 的出色实现,并为您提供了一些将 Vert.x Future 的强大功能与 Rx 运算符的优点相结合的功能。

为了使用 RxJava,您必须在 Maven pom.xml 中设置以下依赖项:

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-rx-java2</artifactId>
</dependency>

让我们从一个简单的例子开始。

我们可以创建三个新类,它们实现了前面描述的相同方法,但以反应方式:

  • FootballPlayerReactiveVerticle
  • ActionHelperReactive
  • FootballPlayerReactiveDAO

FootballPlayerVerticle 类中,我们创建了构建 HTTP 服务器所需的方法,该服务器处理对我们 API 的请求:

private Future<Void> createHttpServer(JsonObject config, Router router) {
    Future<Void> future = Future.future();
    vertx.createHttpServer().requestHandler(router::accept).listen(config.getInteger("HTTP_PORT",  
        8080),res -> future.handle(res.mapEmpty()));
    return future;
}

我们修改 FootballPlayerReactiveVerticle 类以返回一个 Rx Completable 类,这是一个指示其完成的流:

private Completable createHttpServerReactive(JsonObject config, Router router) {
    return vertx.createHttpServer().requestHandler(router::accept)
        .rxListen(config.getInteger("HTTP_PORT", 8080)).toCompletable();
}

另一个重要元素是我们连接到数据库的方式。

在我们的 DAO 中,我们创建了以下方法来执行此操作:

public Future<SQLConnection> connect(JDBCClient jdbc) {
    Future<SQLConnection> future = Future.future();
    jdbc.getConnection(ar -> future.handle(ar.map(c -> c.setOptions(
        new SQLOptions().setAutoGeneratedKeys(true))))
    );
    return future;
}

FootballPlayerReactiveDAO 类中,我们可以更改方法以返回 Rx Single 而不是 FutureSingle 类似于 Observable 的对象,但它不是发出一系列值,而是发出一个值或错误通知。以下是重新审视的方法:

public Single<SQLConnection> connectReactive(JDBCClient jdbc) {
    return jdbc.rxGetConnection().map(c -> c.setOptions(new    
        SQLOptions().setAutoGeneratedKeys(true)));
}

所有交互都将以反应方式完成:例如,要读取包含创建数据库表的指令并填充它们的文件,我们可以使用 FileSystem 中的 rxReadFile班级:

public Single<SQLConnection> createTableIfNeeded(FileSystem fileSystem, SQLConnection connection) {
    return fileSystem.rxReadFile("schema.sql").map(Buffer::toString)
        .flatMapCompletable(connection::rxExecute).toSingleDefault(connection);
}

public Single<SQLConnection> createSomeDataIfNone(FileSystem fileSystem, SQLConnection 
    connection) {
    return connection.rxQuery("SELECT * FROM football_player").flatMap(rs -> {
        if (rs.getResults().isEmpty()) {
            return fileSystem.rxReadFile("data.sql")
                .map(Buffer::toString)
                .flatMapCompletable(connection::rxExecute)
                .toSingleDefault(connection);
        } else {
            return Single.just(connection);
        }
    });
}

我们实现的响应式版本中使用的所有类都属于 io.vertx.reactivex 包。

此外,可以以反应方式重新访问执行 CRUD 操作所需的方法。例如,update 方法可以这样重写:

public Single<FootballPlayer> update(SQLConnection connection, String id, FootballPlayer 
    footballPlayer) {
    String sql = "UPDATE football_player SET name = ?, surname = ?, age = ?, team = ?, position =     
        ?, price = ? WHERE id = ?";
    return connection.rxUpdateWithParams(sql, new JsonArray().add(
        footballPlayer.getName()).add(footballPlayer.getSurname())
            .add(footballPlayer.getAge()).add(footballPlayer.getTeam())
            .add(footballPlayer.getPosition()).add(footballPlayer.
                getPrice().intValue()).add(Integer.valueOf(id)))
            .map(res -> new FootballPlayer(res.getKeys().getInteger(0),
                footballPlayer.getName(), footballPlayer.getSurname(),
                footballPlayer.getAge(), footballPlayer.getTeam(),
                footballPlayer.getPosition(), footballPlayer.getPrice()))
            .doFinally(() -> {
                connection.close();
    });
}

FootballPlayerDAO 中实现的版本的主要区别在于使用 rxUpdateWithParams,它将执行 SQL UPDATE 操作并返回一个 Single 对象而不是 Future,在方法结束时关闭 JDBC 连接。

使用 Config 文件中定义的配置构建 HTTP 服务器的最终管道,必须能够处理我们的 API,如下所示:

retriever.rxGetConfig().doOnSuccess(config -> jdbc = JDBCClient.createShared(vertx,
    config, "My-Reading-List"))
    .flatMap(config -> dao.connect(jdbc)
    .flatMap(connection -> this.createTableIfNeeded(connection)
    .flatMap(this::createSomeDataIfNone)
    .doAfterTerminate(connection::close))
    .map(x -> config))
    .flatMapCompletable(config -> createHttpServer(config, router))
    .subscribe(CompletableHelper.toObserver(fut));

我使用 flatMap 方法连接操作,并使用 doOnSuccess 方法从观察到的流中接收项目并实现与它们相关的逻辑。

管道的关键部分是 subscribe 方法:如果不调用它,则不会发生任何事情,因为流是惰性的。