Quarkus-和-Kubernetes-的-Java-微服务高级教程-全-

Quarkus 和 Kubernetes 的 Java 微服务高级教程(全)

原文:Pro Java Microservices with Quarkus and Kubernetes

协议:CC BY-NC-SA 4.0

一、容器化入门

引入容器化

在 Java 世界中,应用在部署到运行时之前被打包成多种格式。这些环境可以是物理机(裸机)或虚拟机。

软件应用的主要风险是运行时更新可能会破坏应用。例如,操作系统更新可能包括其他更新,其库与正在运行的应用不兼容。

通常,软件可以与应用共存于同一主机上,如数据库或代理。它们都共享相同的操作系统和库。因此,即使更新不会直接导致应用崩溃,任何其他服务都可能受到负面影响。

尽管这些升级有风险,但我们不能忽视它们,因为它们关系到安全和稳定。它们包括错误修复和增强。但是我们可以测试我们的应用及其上下文,看看更新是否会导致问题。这项任务可能会令人望而生畏,尤其是当应用非常庞大的时候。

保持冷静!有一个极好的解决方案。我们可以使用容器,它是单个操作系统中的一种隔离分区。它们提供了许多与虚拟机相同的优势,如安全性、存储和网络隔离,但它们需要的硬件资源却少得多。容器很棒,因为它们启动和终止更快。

容器化允许隔离和跟踪资源利用。这种隔离保护我们的应用免受与主机操作系统更新相关的许多风险。

img/509649_1_En_1_Figa_HTML.jpg

容器有很多好处:

  • 一致的环境:容器是打包应用及其所有依赖项的最佳方式。这为我们在开发和测试阶段准确定义生产环境提供了一个独特的机会。

  • 一次编写,随处运行:容器可以在任何地方执行:在任何机器、任何操作系统和任何云提供商上。

  • 隔离和抽象:容器使用操作系统级隔离来抽象资源。

有许多可用的容器解决方案。Docker 是一种流行的开源容器格式。

介绍 Docker

Docker 是一个面向开发者的平台,旨在将应用打包、部署和运行为容器。采用容器作为新的应用打包格式被称为容器化

img/509649_1_En_1_Figd_HTML.jpg

文档文件

一个 Dockerfile 是一个 Docker 容器的源代码。这是一个描述符文件,其中包含将生成 Docker 映像的指令。

图像和容器

一个 Docker 图像是一个 Docker 容器的起源。当我们构建 Docker 文件时,我们得到图像,当我们运行 Docker 图像时,我们得到 Docker 容器。

安装 Docker

要开始玩 Docker,你需要安装它。您可以从 https://docs.docker.com/install/ 中抓取与您的平台兼容的版本。

当你访问那个页面时,你会发现两个版本:Docker CE 和 Docker EE:

  • Docker CE 适合开发和基本需求。

  • Docker EE 是一个企业级版本,具有许多使用高度扩展的生产级 Docker 集群所需的额外功能。

安装 Docker 非常容易,你不需要教程来做。img/509649_1_En_1_Fige_HTML.gif

img/509649_1_En_1_Figf_HTML.gif对于img/509649_1_En_1_Figg_HTML.gif Windows 用户,确保img/509649_1_En_1_Figh_HTML.gif Docker Desktop for Windows 使用img/509649_1_En_1_Figi_HTML.gifLinux 容器。

要检查 Docker 是否安装正确,您可以通过运行 Docker version命令来检查安装的版本:

Client: Docker Engine - Community                   ②
 Cloud integration: 1.0.7
 Version:           20.10.2
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        2291f61
 Built:             Mon Dec 28 16:12:42 2020
 OS/Arch:           darwin/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community                   ①
 Engine:
  Version:          20.10.2
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       8891c58
  Built:            Mon Dec 28 16:15:28 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

运行您的第一个容器

与所有编程语言一样,我们将从创建一个 Hello World 示例开始。在 Docker 世界中,我们有hello-world图像,它可以作为一个起点:img/509649_1_En_1_Figj_HTML.gif

$ docker run hello-world

Unable to find image "hello-world:latest" locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:31b9c7d48790f0d8c50ab433d9c3b7e17666d6993084c002c2ff1ca09b96391d
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
...

当运行这个 Docker 映像时,我们得到典型的Hello from Docker!消息。这张图片的源代码可以在Docker Library GitHub repository中找到。

这张图片来自 Docker Hub,因为在本地找不到。

WHAT IS DOCKER HUB?

Docker Hub 是一款公共 Docker 产品,为 Docker 用户提供多种服务:

  • 将 Docker 图像存储在公共和私有存储库中。

  • 当源代码更新时,创建连续的集成管道来构建映像。

  • 处理用户和组的授权和访问管理。

  • 包括 GitHub 和 BitBucket 集成,用于自动构建。

要列出现有的本地 Docker 映像,请使用以下命令:

$ docker images

REPOSITORY         TAG        IMAGE ID           CREATED             SIZE
hello-world        latest     bf756fb1ae65       13 months ago       13.3kB

img/509649_1_En_1_Figk_HTML.gif Image ID是一个随机生成的十六进制值,用于标识每幅图像。

要列出所有 Docker 容器,请使用以下命令:

$ docker ps -a

CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                     PORTS  NAMES
42feaf1ce560   hello-world   "/hello"   37 minutes ago   Exited (0) 37 minutes ago          vigilant

这里解释了一些细节:

  • Container ID是一个随机生成的十六进制值,用于标识容器。

  • 您在这里看到的Container Name是 Docker 守护进程为您创建的一个随机生成的字符串名称,因为您没有指定它。

了解 Docker 架构

这一切都太棒了!但是 Docker 是如何工作的呢?img/509649_1_En_1_Figl_HTML.gif

Docker 使用客户端-服务器架构:

  • 服务器端,Docker 公开了一个 REST API,它将接收来自客户端调用者的命令。然后,REST API 会将请求转发给一个名为 Docker 守护进程 ( dockerd)的核心组件,它将执行操作并将响应发送回 REST API。然后 API 将它们发送回调用者。

  • 客户端上,我们使用 Docker CLI 来键入 Docker 命令。

img/509649_1_En_1_Figm_HTML.png

Docker 对象

我们已经简要讨论了 Docker 图像和容器。在本节中,我们将更详细地讨论这些和许多其他 Docker 对象。

形象

如前所述,Docker 映像是 Docker 容器的起源。当我们构建 Docker 文件时,我们得到图像,当我们运行 Docker 图像时,我们得到 Docker 容器。

每个 Docker 图像都基于一个特定的图像。例如,我们可以使用openjdk作为基于 Java 的 Docker 图像的基础图像。否则,它可以基于 Ubuntu 映像,然后在映像指令中安装 Java 运行时。

使用 Dockerfile 构建图像,Dockerfile 是一个简单的文本文件,包含组成图像的指令。Docker 文件中的每条指令都会在 Docker 映像中创建一个单独的层。因此,当我们更改 docker 文件中的指令并重建映像时,只构建新指令的层。通过这种方式,我们可以获得可以快速更新的轻量级映像,这是其他虚拟化技术所不具备的。

容器

当我们运行一个图像时,我们得到一个容器。容器结构和行为由图像内容定义。

Docker 容器通过 Docker CLI 进行管理。我们可以有多种容器设计。例如,我们可以将存储插入容器,甚至将其连接到网络。

当一个容器被删除时,如果它没有保存到存储器中,它的状态将会丢失。

码头机器

Docker Machine 是一种工具,可以轻松地远程供应和管理多台 Docker 主机。这个工具使用docker-machine命令管理主机:我们可以启动、检查、停止和重启一个被管理的主机,并升级 Docker 安装。我们甚至可以用它来配置 Docker 客户机与给定的主机对话。然后,我们可以直接在该远程主机上使用本地 CLI。

比如说我们在 Azure 上有一个 Docker 机器叫做azure-env。如果我们做了docker-machine env azure-env,我们将本地的docker命令指向那个azure-env Docker 引擎。

潜入码头容器

了解 Docker 的最好方法是用 Docker 的方式编写一个应用。

开发环境中的 Docker 容器

在许多项目中,开发环境不同于生产环境。这可能会导致在为生产部署应用时出现问题。有时,环境之间的微小差异会造成巨大的问题。这就是 Docker 可以提供帮助的地方:您可以在两种环境中使用相同的映像。开发人员和测试人员可以使用与生产环境相同的映像。

除了打包应用代码库,Docker 映像还会打包所有需要的依赖项。这可以保证环境之间的完美匹配。

用 Dockerfile 定义一个容器

如前所述,Dockerfile 定义了容器并决定了它将如何被执行。考虑清单 1-1 中的例子。

# Use an OpenJDK Runtime as a parent image
FROM openjdk:11-jre-alpine

# Add Maintainer Info
LABEL maintainer="lnibrass@gmail.com"

# Define environment variables
ENV JAVA_OPTS="-Xmx2048m"

# Set the working directory to /app
WORKDIR /app

# Copy the artifact into the container at /app
ADD some-application-built-somewhere.jar app.jar

# Make port 8080 available to the world outside this container
EXPOSE 8080

# Run app.jar when the container launches
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar"]

Listing 1-1Example of a Dockerfile

该 Dockerfile 文件将:

  • 基于 OpenJDK 11 创建一个容器

  • 将 JVM 的最大内存分配池的环境变量定义为 2GB

  • 将工作目录定义为/app

  • 将本地路径中的some-application-built-somewhere.jar文件复制为app.jar

  • 打开端口 8080

  • 定义容器启动时将执行的startup命令

创建一个示例应用

我们将使用 code.quarkus.io 创建sample-app:hello world quar kus 应用。我们只需要定义一下GroupIdArtifactId。对于扩展,让我们选择RESTEasy JAX-RS:

img/509649_1_En_1_Fign_HTML.jpg

生成的应用附带了一个示例 Rest API:

@Path("/hello-resteasy")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello RESTEasy";
    }
}

当我们使用mvn clean install编译这个应用时,我们的 JAR 将在target文件夹中可用。

target文件夹中运行该命令将只显示当前文件夹中的文件:

$ ls -p target/ | grep -v /

quarkus-artifact.properties
sample-app-1.0.0-SNAPSHOT.jar

Quarkus 没有在生成新项目时编写自己的 Dockerfile,而是在src/main/docker文件夹中提供了一组 docker file:

$ ls -p src/main/docker | grep -v /

Dockerfile.jvm                  ①
Dockerfile.legacy-jar           ②
Dockerfile.native               ③
Dockerfile.native-distroless    ④

这些 docker 文件用于构建运行 Quarkus 应用的容器:

  • ①在 JVM 模式下,使用fast-jar打包格式,旨在提供更快的启动时间。

  • ②在 JVM 模式下使用legacy-jar打包。

  • ③在本机模式下(无 JVM)。在接下来的章节中,我们将会谈到很多关于本地模式的内容。img/509649_1_En_1_Figp_HTML.gif

  • ④在一个不含酒精的容器中。它包含应用及其运行时依赖项,没有任何额外的组件,如包管理器、shells 或任何标准 Linux 发行版中通常提供的任何其他程序。

让我们构建基于 JVM 模式的 Docker 容器,并将其标记为nebrass/sample-jvm:1.0.0-SNAPSHOT:

  • ①由于 Docker 在本地没有找到ubi8/ubi-minimal图像,它从registry.access.redhat.com/ubi8/ubi-minimal:8.3下载。

  • ②docker 文件中的每个指令都是在一个专用步骤中构建的,它在映像中生成一个单独的层。十六进制代码显示在每个步骤的末尾,是层的 ID。

  • ③构建的图像 ID。

  • ④我们构建的图像用nebrass/sample-jvm:latest标记;我们指定了名称(nebrass/sample-jvm:latest),Docker 自动添加了最新的版本标签。

> mvn package && docker build -f src/main/docker/Dockerfile.jvm -t nebrass/sample-jvm:1.0.0-SNAPSHOT .

Sending build context to Docker daemon  51.09MB
Step 1/11 : FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3       ①
8.3: Pulling from ubi8/ubi-minimal                                     ①
77a02d8cede1: Pull complete                                            ①
7777f1ac6191: Pull complete                                            ①
Digest: sha256:e72e188c6b20281e241fb3cf6f8fc974dec4cc6ed0c9d8f2d5460c30c35893b3                                                               ①
Status: Downloaded newer image for registry.access.redhat.com/ubi8/ubi-minimal:8.3                                                        ①
 ---> 91d23a64fdf2                                                     ②
Step 2/11 : ARG JAVA_PACKAGE=java-11-openjdk-headless
 ---> Running in 6f73b83ed808
Removing intermediate container 6f73b83ed808
 ---> 35ba9340154b                                                      ②
Step 3/11 : ARG RUN_JAVA_VERSION=1.3.8
 ---> Running in 695d7dcf4639
Removing intermediate container 695d7dcf4639
 ---> 04e28e22951e                                                      ②
Step 4/11 : ENV LANG="en_US.UTF-8" LANGUAGE="en_US:en"
 ---> Running in 71dc02dbee31
Removing intermediate container 71dc02dbee31
 ---> 7c7c69eead06                                                      ②
Step 5/11 : RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
    && microdnf update \
    && microdnf clean all \
    && mkdir /deployments \
    && chown 1001 /deployments \
    && chmod "g+rwX" /deployments \
    && chown 1001:root /deployments \
    && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
    && chown 1001 /deployments/run-java.sh \
    && chmod 540 /deployments/run-java.sh \
    && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
 ---> Running in 2274fdc94d6f
Removing intermediate container 2274fdc94d6f
 ---> 9fd48c2d9482                                                     ②
Step 6/11 : ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
 ---> Running in c0e3ddc80993
Removing intermediate container c0e3ddc80993
 ---> 26f287fde6f6                                                     ②
Step 7/11 : COPY target/lib/* /deployments/lib/
 ---> 1c3aa9a683a6                                                     ②
Step 8/11 : COPY target/*-runner.jar /deployments/app.jar
 ---> d1bdd5e96e5e                                                      ②
Step 9/11 : EXPOSE 8080
 ---> Running in 728f82b270d2
Removing intermediate container 728f82b270d2
 ---> 704cd49fd439                                                      ②
Step 10/11 : USER 1001
 ---> Running in 5f7aef93c3d7
Removing intermediate container 5f7aef93c3d7
 ---> 5a773add2a6d                                                      ②
Step 11/11 : ENTRYPOINT [ "/deployments/run-java.sh" ]
 ---> Running in cb6d917592bc
Removing intermediate container cb6d917592bc
 ---> 9bc81f158728                                                      ②
Successfully built 9bc81f158728                                         ③
Successfully tagged nebrass/sample-jvm:latest                           ④

WHAT IS THE UBI BASE IMAGE?

提供的 docker 文件使用 UBI ( 通用基础映像)作为父映像。这个基本图像已经被裁剪为在容器中完美地工作。docker 文件使用基本映像的最小版本来减小生成的映像的大小。

你刚刚建立的形象在哪里?它位于您机器的本地 Docker 映像注册表中:

$ docker images

REPOSITORY           TAG              IMAGE ID       CREATED          SIZE
nebrass/sample-jvm   1.0.0-SNAPSHOT   9bc81f158728   5 minutes ago    501MB
ubi8/ubi-minimal     8.3              ccfb0c83b2fe   4 weeks ago      107MB

在本地,有两个映像— ubi8/ubi-minimal是基础映像,nebrass/sample-jvm是构建映像。

运行应用

现在运行构建好的容器,使用-p将机器的端口 28080 映射到容器发布的端口 8080:

$ docker run --rm -p 28080:8080 nebrass/sample-jvm:1.0.0-SNAPSHOT

  • 第 1 行显示了在容器中启动 Java 应用的命令。

  • 第 2-8 行列出了 Quarkus 应用的日志。在那里你会看到一条提到Listening on: http://0.0.0.0:8080 的消息。这个日志是由打包在容器中的实例打印出来的,我们的调整并没有意识到这一点:我们将容器端口 8080 映射到我们的自定义端口 28080,这显然使 URL 变成了http://localhost:28080

1 exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/app.jar
2 __  ____  __  _____   ___  __ ____  ______
3  --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
4  -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
5 --\___\_\____/_/ |_/_/|_/_/|_|\____/___/
6 2020-08-05 13:53:44,198 INFO  [io.quarkus] (main) sample-app 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.6.1.Final) started in 0.588s. Listening on: http://0.0.0.0:8080
7 2020-08-05 13:53:44,218 INFO  [io.quarkus] (main) Profile prod activated.
8 2020-08-05 13:53:44,219 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

如果您在 web 浏览器中打开 URL http://localhost:28080,您将进入默认的 Quarkus index.html文件,如下图所示:

img/509649_1_En_1_Figr_HTML.jpg

如果您在 web 浏览器中转到http://localhost:28080/hello,您将看到ExampleResource REST API 的响应。

img/509649_1_En_1_Figs_HTML.jpg

img/509649_1_En_1_Figt_HTML.gif如果您在 Windows 7 上使用 Docker Toolbox,并且无法使用 localhost 访问容器,则需要使用 Docker 机器 IP。使用命令docker-machine ip找到它。

您还可以在终端中使用curl命令来调用 REST API:

$ curl http://localhost:28080/hello

hello%

WHAT IS cURL?

cURL(客户端 URL 工具)是一个命令行工具,用于使用 HTTP、FTP 等各种协议进行客户端-服务器请求。

端口映射 28080:8080 是用-p参数运行docker run时定义的EXPOSED/PUBLISHED对。

要退出容器连接的控制台会话,只需按 Ctrl+C。

要以分离模式(在后台)运行 Docker 容器,只需将-d添加到run命令中:

$ docker run -d -p 28080:8080 nebrass/sample-jvm:1.0.0-SNAPSHOT

fbf2fba8e9b14a43e3b25aea1cb94b751bbbb6b73af05b84aab3f686ba5019c8

在这里,我们获得应用的长容器 ID,然后被踢回终端。img/509649_1_En_1_Figv_HTML.gif容器在后台运行。

我们还可以看到有一个缩写的容器标识符,带有docker ps。运行任何命令时,我们都可以使用长格式或短格式:

$ docker ps

CONTAINER ID  IMAGE                 COMMAND                   STATUS  PORTS
fbf2fba8e9b1  nebrass/sample-jvm..  "/deployments/run-ja..."  Up      28080->8080/tcp

我们可以看到,我们有一个正在运行的容器,其中有我们的自定义端口映射。我们将使用它的CONTAINER ID来管理它。例如,要停止该容器,我们将运行:

docker stop fbf2fba8e9b1

运行命令后,为了确认操作,Docker 将再次打印容器 ID。

发布您的图像

局部图像一点用都没有。图像需要存储在一个集中的位置,以便从不同的环境中提取。

Docker 图像存储在一个名为 Docker 注册表的集中位置。Docker Hub 的主要职责是注册。每个图像集合都作为存储库托管在注册表中。

默认情况下,Docker CLI 位于 Docker Hub 上。

使用您的 Docker ID 登录

在本节中,我们将使用 Docker Hub 的免费层。如果您没有免费账户,可以在 https://hub.docker.com/ 创建一个。

要向 Docker Hub 验证您的 Docker 客户机,只需输入以下命令:

docker login -u <username> -p <password>

img/509649_1_En_1_Figx_HTML.gif回想一下,Docker Hub 默认是 Docker 注册中心。如果您想改变这一点,只需使用docker login命令指定一个注册表 URL。

给图像加标签

存储在注册表中的图像具有类似于username/repository:tag的名称格式。tag包含版本信息,这是可选的,但是强烈推荐。如果不显式指定标签,Docker 会将标签定义为latest,它不能提供任何关于打包的应用版本的信息。

用于定义图像标签的 Docker 命令如下:

docker tag image username/repository:tag

例如,如果您想将一个SNAPSHOT版本升级到Final,您可以这样做:

docker tag nebrass/sample-jvm:1.0.0-SNAPSHOT nebrass/sample-jvm:1.0.0-Final

运行docker images命令查看您新标记的图像:

$ docker images

REPOSITORY          TAG             IMAGE ID       CREATED          SIZE
nebrass/sample-jvm  1.0.0-Final     598712377440   43 minutes ago   501MB
nebrass/sample-jvm  1.0.0-SNAPSHOT  598712377440   43 minutes ago   501MB
ubi8/ubi-minimal    8.1             91d23a64fdf2   4 weeks ago      107MB

发布图像

发布一个图像就是让它在某个容器的注册表中可用。在我们的例子中,我们将标记的图像推送到注册表:

docker push username/repository:tag

标记的图像现在在 Docker Hub 上可用。如果你去 https://hub.docker.com/r/username/repository ,你会发现那里的新形象。

img/509649_1_En_1_Figy_HTML.gif在推送图像之前,您需要使用docker login命令进行身份验证。

让我们推送nebrass/sample-jvm:1.0.0-Final标签图像:

docker push nebrass/sample-jvm:1.0.0-Final

The push refers to repository [docker.io/nebrass/sample-jvm]
0f8895a56cf0: Pushed
9c443a7a1622: Pushed
fb6a9f86c4e7: Pushed
eddba477a8ae: Pushed
f80c95f61fff: Pushed
1.0.0-Final: digest: sha256:30342f5f4a432a2818040438a24525c8ef9d046f29e3283ed2e84fdbdbe3af55 size: 1371

从 Docker Hub 获取并运行映像

打包在nebrass/sample-jvm Docker 映像中的应用现在可以在任何 Docker 主机上执行。当您运行docker run nebrass/sample-jvm时,如果映像在本地不可用,它将从 Docker Hub 中取出。让我们删除所有与nebrass/sample-jvm相关的本地容器:

docker ps -a | awk '{ print $1,$2 }' | grep nebrass/sample-jvm | awk '{print $1 }' | xargs -I {} docker rm {}

让我们删除所有与nebrass/sample-jvm相关的本地图像:

docker images | awk '{ print $1":"$2 }' | grep nebrass/sample-jvm | xargs -I {} docker rmi {}

如果映像在本地不可用,将从 Docker Hub 中提取:

$ docker run -p 28080:8080 nebrass/sample-jvm:1.0.0-Final

Unable to find image 'nebrass/sample-jvm:1.0.0-Final' locally
1.0.0-Final: Pulling from nebrass/sample-jvm
b26afdf22be4: Already exists
218f593046ab: Already exists
284bc7c3a139: Pull complete
775c3b820c36: Pull complete
4d033ca6332d: Pull complete
Digest: sha256:30342f5f4a432a2818040438a24525c8ef9d046f29e3283ed2e84fdbdbe3af55
Status: Downloaded newer image for nebrass/sample-jvm:1.0.0-Final
...

打包在nebrass/sample-jvm:1.0.0-Final映像中的应用及其所有依赖项现在都可以在您的机器中使用了。不需要安装 Java 运行时或进行任何配置。一切准备就绪!

玩谷歌 Jib

Google 创建了 Jib Maven 插件来为 Java 应用构建 Docker 映像。使用 Jib,您不需要为您的应用创建 docker 文件。你甚至不需要在本地机器上安装 Docker。Jib 将自动分析、构建、制作和推送 Docker 映像。

Docker 工作流程如下所示:

img/509649_1_En_1_Figz_HTML.jpg

Jib 工作流程如下所示:

img/509649_1_En_1_Figaa_HTML.jpg

Jib 不仅适用于 Maven,也适用于 Gradle。它有多种配置和调整的可能性,可以帮助您覆盖任何默认配置或满足特定需求,例如将构建的映像推送到私有 Docker 注册表中。

img/509649_1_En_1_Figab_HTML.gif要使用带有私有注册表的 Google Jib 插件,你需要给 Jib 提供一个凭证助手,比如在的官方 Jib 文档中描述的 https://goo.gl/gDs66G

在 Quarkus 生态系统中,我们有一个随时可以使用的 Jib 扩展,称为quarkus-container-image-jib。这个扩展由 Jib 提供支持,用于执行容器映像构建。使用 Jib 和 Quarkus 的主要好处是所有的依赖项(在target/lib下找到的所有东西)都缓存在与实际应用不同的层中,因此使得重建非常快速和小(当涉及到推送时)。使用该扩展的另一个重要好处是,它提供了创建容器映像的能力,而不需要任何专用的客户端工具(如 Docker)或运行守护进程(如 Docker 守护进程),而所需要的只是推送容器映像注册中心的能力。

若要使用此功能,请将以下扩展添加到项目中:

./mvnw quarkus:add-extension -Dextensions="container-image-jib"

img/509649_1_En_1_Figac_HTML.gif当您需要做的只是构建一个容器映像,而不是推送到注册表时(基本上是通过设置quarkus.container-image.build=true并保留quarkus.container-image.push未设置;默认为false,这个扩展创建一个容器映像,并向 Docker 守护进程注册它。这意味着,尽管 Docker 不用于构建图像,但它仍然是必要的。

还要注意的是,当使用该模式时,构建的容器图像将在执行docker images时出现。

使用 Google Jib 构建

使用以下命令构建容器映像,而不将其推送到容器映像注册表:

mvn clean package -Dquarkus.container-image.build=true

如果您想要构建一个映像并将其推送到经过身份验证的容器映像注册中心,请使用以下命令:

mvn clean package -Dquarkus.container-image.build=true -Dquarkus.container-image.push=true

有许多配置选项可用于自定义映像或执行特定操作:

|

配置属性

|

目的

|
| --- | --- |
| quarkus.container-image.group | 容器图像所属的组。如果未设置,则默认为登录用户。 |
| quarkus.container-image.name | 容器图像的名称。如果未设置,则默认为应用名称。 |
| quarkus.container-image.tag | 容器图像的标签。如果未设置,则默认为应用版本。 |
| quarkus.container-image.registry | 要使用的容器注册表。如果未设置,则默认为经过身份验证的注册表。 |
| quarkus.container-image.username | 用于向将推送构建映像的注册表进行身份验证的用户名。 |
| quarkus.container-image.password | 用于向推送构建映像的注册表进行身份验证的密码。 |

你可以使用" Quarkus 容器图片指南来深入了解大吊臂延伸部

满足码头服务

在企业世界中,应用由许多服务组成。例如,一个企业管理应用有许多服务:库存管理服务、员工管理服务等等。在码头化的环境中,这些服务是作为容器运送的。这种选择提供了许多优势,比如伸缩性。

Docker 有一个很棒的工具,叫做 Docker Compose,用于定义具有高级网络和存储选项等强大功能的容器。Docker Compose 对于本地开发非常有用。

创建第一个 docker-compose.yml 文件

Docker Compose 的输入是一个docker-compose.yml文件,这是一个普通的 YAML 文件,描述了 Docker 容器的设计,以及许多选项,如分配的资源、网络、存储等。参见清单 1-2 。

version: "3"
services:
  web:
    image: nebrass/sample-jvm:1.0.0-Final
    deploy:
      replicas: 5
      restart_policy:
        condition: on-failure
    ports:
      - "8080:8080"
    networks:
      - webnetwork
networks:
  webnetwork:

Listing 1-2Example of a docker-compose.yml File

这个docker-compose.yml文件将从 Docker Hub 下载nebrass/sample-jvm:1.0.0-Final映像,并从这个映像创建五个实例。它会将端口 8080 映射到 8080。如果失败,这些容器将重新启动。这些容器将在webnetwork网络中提供。

用 Docker 实现更多

Docker 在许多方面优化了开发人员的体验,提高了工作效率。例如,您可以使用 Docker 容器来:

  • 非常快速地获得所需的软件和工具。例如,您可以使用 Docker 来获取本地数据库或 SonarQube 实例。

  • 构建、调试和运行源代码,即使您没有合适的环境。例如,如果您没有在本地安装 JDK,或者即使您没有与项目相同的所需版本,您也可以在容器中运行和测试它。

  • 解决一些技术需求。例如,如果您有一个只能在您的机器上已经使用的特定端口上执行的应用,您可以使用容器端口映射特性来使用通过不同的公开端口容器化的相同应用。

  • 还有更多!img/509649_1_En_1_Figad_HTML.gif

快速获得所需的工具

您可以在几秒钟内获得一个数据库实例,而不需要任何安装,也不需要接触本地环境,甚至不需要修改本地文件。你不必担心一个 MySQL 守护进程会一直使用端口 3306,甚至在卸载之后。img/509649_1_En_1_Figae_HTML.gif

获取一个 Dockerized PostgreSQL 实例

例如,要拥有本地 PostgreSQL 实例,请运行以下命令:

  • ①在分离模式下运行名为demo-postgres的容器(作为后台守护进程)。

  • ②您定义环境变量:

    • Postgres 用户:developer

    • Postgres 密码:someCrazyPassword

    • Postgres 数据库:demo

  • ③你转发端口54325432,使用官方 PostgreSQL 镜像 v13。

docker run -d --name demo-postgres \                ①
        -e POSTGRES_USER=developer \                ②
        -e POSTGRES_PASSWORD=someCrazyPassword \    ②
        -e POSTGRES_DB=demo \                       ②
        -p 5432:5432 postgres:13                    ③

您可以在 Java 应用中使用这些凭证,就像任何其他独立的 PostgreSQL 一样。

不幸的是,数据将被专门存储在容器内部。如果它崩溃或被删除,所有的数据都将丢失。

如果希望持久化容器内部的可用数据,需要将本地挂载点作为 Docker 数据卷映射到容器内部的路径。

我在我的主目录中创建一个volumes文件夹(您可以给这个文件夹起任何您喜欢的名字),然后为我需要创建数据卷挂载点的每个应用创建子文件夹。

让我们首先创建一个文件夹,用于在主机中存储数据:

mkdir -p $HOME/docker-volumes/postgres

这个$HOME/docker-volumes/postgres文件夹将被映射到 PostgreSQL 容器的/var/lib/postgresql/data文件夹,PostgreSQL 在这里存储物理数据文件。

运行持久 PostgreSQL 容器的 Docker 命令现在将是:

docker run -d --name demo-postgres \
        -e POSTGRES_USER=developer \
        -e POSTGRES_PASSWORD=someCrazyPassword \
        -e POSTGRES_DB=demo \
        -p 5432:5432 \
        -v $HOME/docker/volumes/postgres:/var/lib/postgresql/data \
        postgres:13

让我们继续讨论我在讨论 Docker 好处时喜欢谈论的另一个用例:拥有一个本地 SonarQube 服务器。img/509649_1_En_1_Figaf_HTML.gif

获取一个 Dockerized SonarQube 实例

只需使用一个 Docker 命令,就可以轻松拥有 SonarQube 实例:

docker run -d --name sonarqube \
        -p 9000:9000 \
        -p 9092:9092 \
        sonarqube:8.4.1-community

该命令将基于 Docker 映像sonarqube:8.4.1-community运行一个容器,并公开两个 SonarQube 端口 9000 和 9092。

如果您习惯于 SonarQube,您可以拥有您的本地实例,并且您可以导入您的团队正在使用的所有质量概要文件(又名 gates )。

释放需求链

在搜索用例时,我们不会在这里走得太远。我们可以把 Quarkus 需求作为一个用例:先决条件之一是拥有 GraalVM。如果您没有在本地安装它,您可以使用一个 GraalVM Docker 映像来获得一个容器,该容器允许您进行基于 GraalVM 的构建。

让我们回到之前生成的sample-app。如果我们想构建一个基于 GraalVM 的 JAR 文件,而不在本地安装 GraalVM,我们可以使用 Dockerfile 的这个例子,我们将它保存为src/main/docker/Dockerfile.multistage。参见清单 1-3 。

 1 ## Stage 1 : build with maven builder image with native capabilities
 2 FROM quay.io/quarkus/centos-quarkus-maven:20.1.0-java11 AS build
 3 COPY pom.xml /usr/src/app/
 4 RUN mvn -f /usr/src/app/pom.xml -B de.qaware.maven:go-offline-maven-plugin:1.2.5:resolve-dependencies
 5 COPY src /usr/src/app/src
 6 USER root
 7 RUN chown -R quarkus /usr/src/app
 8 USER quarkus
 9 RUN mvn -f /usr/src/app/pom.xml -Pnative clean package
10
11 ## Stage 2 : create the docker final image
12 FROM registry.access.redhat.com/ubi8/ubi-minimal
13 WORKDIR /work/
14 COPY --from=build /usr/src/app/target/*-runner /work/application
15
16 # set up permissions for user `1001`
17 RUN chmod 775 /work /work/application \
18   && chown -R 1001 /work \
19   && chmod -R "g+rwX" /work \
20   && chown -R 1001:root /work
21
22 EXPOSE 8080
23 USER 1001
24
25 CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Listing 1-3src/main/docker/Dockerfile.multistage

这个 Dockerfile 文件包含两个嵌入的 Dockerfile 文件。注意已经有两个FROM指令。这个 docker 文件的每个部分被称为一个阶段,这就是为什么我们给 docker 文件加上了扩展名multistage

在第一阶段,我们使用 Maven 生成本地 Quarkus 可执行文件,在第二阶段,我们使用构建的 JAR 文件创建 Docker 运行时映像。我们使用安装的 Maven 和 Docker 运行时来完成这项工作。

传统上,我们有两个 docker 文件。第一个专用于开发,第二个专用于生产。开发 Dockerfile 带来了 JDK 和构建工具,而第二个 docker file 只包含应用二进制文件和运行时。这种方法提高了生产率,但提高的幅度不大。我们仍然需要管理两个 Dockerfiles,如果我们有很多应用,这将是很痛苦的。

接下来是多阶段构建。我们只有一个 Dockerfile,它有一个特殊的名字:Dockerfile.multistage。在里面,我们将有两个(或更多个)FROM指令来定义每个阶段的基础图像。对于开发和生产环境,我们可以有不同的基础映像。我们可以在阶段之间进行互动。例如,我们可以将在开发阶段构建的文件转移到生产阶段。

我们构建多阶段 docker 文件的方式与构建常规(单阶段)docker 文件的方式相同:

docker build -f Dockerfile.multistage -t nebrass/sample-app-multistaged.

如果您运行它,它与我们在前面的步骤中构建的 Docker 映像相同,但是更小:

$ docker images | grep nebrass

REPOSITORY                   TAG     IMAGE ID      CREATED         SIZE
nebrass/sample-app-jvm       latest  5e1111eeae2b  13 minutes ago  501MB
nebrass/quarkus-multistaged  latest  50591fb707e7  58 minutes ago  199MB

同一个应用打包在两个映像中,为什么它们的大小不同呢?

在多阶段构建时代之前,我们曾经编写将要执行的指令,docker 文件中的每条指令都向映像添加了一层。因此,即使我们有一些清洁说明,他们有一个专门的重量。在某些情况下,就像许多 Red Hat Docker 图像一样,我们过去常常创建脚本,在 Docker 文件中仅用一条指令运行这些脚本。

在多阶段构建变得可用之后,当我们跨不同阶段构建时,除了必需的工件之外,最终的图像不会有任何不必要的内容。

BUILDING THE NATIVE EXECUTABLE WITHOUT GRAALVM

Quarkus 有一个很棒的特性,使开发人员无需安装 GraalVM 就可以构建一个本机可执行文件。您需要像前面的步骤一样安装 Docker,但是不必在 Docker 文件上花费精力。该命令只是一个 Maven 版本:

$ mvn package -Pnative -Dquarkus.native.container-build=true

构建使用 Docker。您已经可以注意到正在记录的步骤:

[INFO] --- quarkus-maven-plugin:1.13.2.Final:build (default) @ example ---
...
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildContainerRunner] Using docker to run the native image builder
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildContainerRunner] Checking image status quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
21.0-java11: Pulling from quarkus/ubi-quarkus-native-image
57de4da701b5: Pull complete
...
Status: Downloaded newer image for quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
...
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin..
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] docker run --env LANG=C --rm -v ..
[example-1.0.0-SNAPSHOT-runner:26]    classlist:   5 267,45 ms,  0,96 GB
[example-1.0.0-SNAPSHOT-runner:26]        (cap):     446,08 ms,  0,94 GB
[example-1.0.0-SNAPSHOT-runner:26]        setup:   2 105,02 ms,  0,94 GB
...
[example-1.0.0-SNAPSHOT-runner:26]      compile:  30 414,02 ms,  2,10 GB
[example-1.0.0-SNAPSHOT-runner:26]        image:   2 805,44 ms,  2,09 GB
[example-1.0.0-SNAPSHOT-runner:26]        write:   1 604,06 ms,  2,09 GB
[example-1.0.0-SNAPSHOT-runner:26]      [total]:  72 346,32 ms,  2,09 GB
...
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 117871ms

容器化不仅仅是码头

Docker 并不是市场上唯一可用的容器化解决方案。现在有很多选择。其中许多解决了某些 Docker 限制,如 Podman 和 Buildah。

Docker 有哪些局限性?

安装 Docker 后,我们有了一个docker.service系统,它将保持运行。要检查其状态,只需键入以下命令:

$ sudo service docker status

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2020-08-06 09:23:19 CEST; 8h ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 1659 (dockerd)
      Tasks: 30
     Memory: 3.5G
     CGroup: /system.slice/docker.service
             └─1659 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
...

img/509649_1_En_1_Figag_HTML.gif您也可以使用操作系统实用程序,如sudo systemctl is-active dockersudo status docker,或者使用 Windows 任务管理器等检查服务状态。

这个永久运行的守护进程是与 Docker 引擎通信的唯一方式。不幸的是,这是一个单点故障。这个守护进程将是所有正在运行的容器进程的父进程。因此,如果这个守护进程被终止或损坏,我们将失去与引擎的通信,并且正在运行的容器进程将保持孤立状态。更有甚者,如果这个过程是对资源的贪婪,我们不能抱怨。img/509649_1_En_1_Figah_HTML.gif

同一个守护进程需要 root 权限才能完成它的工作,当开发人员没有被授予对其工作站的完全 root 权限时,这可能会很烦人。

这些限制为其他工具开始流行创造了机会。接下来我们将讨论两个这样的工具。

见见波德曼和 buildhr

Podman 是 Docker 的替代产品,它提供了一种更简单的方法来完成我们通常用 Docker 完成的所有容器化任务,但是不依赖于守护进程。由波德曼处理和制作的图像和容器符合 开放容器倡议

img/509649_1_En_1_Figaj_HTML.jpg

Podman 通过runC容器运行时进程(不是守护进程)直接与映像注册表、容器和映像存储以及 Linux 内核进行交互。

WHAT IS runC?

runC ( https://github.com/opencontainers/runc )是 Docker 的容器格式和运行时,由 Docker 捐赠给开放容器倡议。

WHAT IS THE OPEN CONTAINERS INITIATIVE?

开放容器倡议(OCI)是由 Linux 基金会主办的一个项目,旨在定义容器的所有标准和规范。它最初于 2015 年由 Docker 等许多容器化领先公司推出。

该项目有两个主要分支。图像规范定义了图像的所有标准和要求。运行时规范定义了容器运行时的所有标准和要求。

任何寻求符合 OCI 的解决方案都需要符合它的两个分支规范。

有了波德曼,你可以做所有你在 Docker 上做的事情。您可以对 Podman 使用相同的 Docker 命令,只需将单词docker改为podman。你甚至可以给podman起一个docker的别名,一切都会变得非常神奇!img/509649_1_En_1_Figak_HTML.gif

没有特权的用户最终可以使用 Podman 运行容器,绝对需要root用户。

你可以像使用 Docker 一样使用 Podman 进行构建。Podman 在构建过程中使用了与 Buildah 相同的代码。

img/509649_1_En_1_Figal_HTML.png

使用 Buildah ,您可以:

  • 从 Dockerfile 文件创建图像。

  • 从现有容器创建图像。您甚至可以对容器进行修改,并将它们发布为新图像。

Buildah 是创建和管理容器图像的代码包装器,具有高级图像管理功能。

Buildah 的优势包括:

  • 对创建图像层的强大控制。你甚至可以在一个层上提交许多修改,而不是像经典的 Docker 世界那样每层提交一条指令。

  • Buildah CLI 用于编写图像指令,就像创建 Linux 脚本一样。如果您查看 Buildah CLI 帮助,您会发现 CLI 命令在任何 Dockerfile 指令中都有:

  add                    Add content to the container
  commit                 Create an image from a working container
  copy                   Copy content into the container
  from                   Create a working container based on an image
  images                 List images in local storage
  info                   Display Buildah system information
  mount                  Mount a working container's root filesystem
  pull                   Pull an image from the specified location
  push                   Push an image to a specified destination
  rename                 Rename a container
  run                    Run a command inside of the container
  tag                    Add an additional name to a local image
  umount                 Unmount the root file system of the specified working containers
  unshare                Run a command in a modified user namespace

波德曼受欢迎有两个主要原因:

  • 它使用与 Docker 相同的命令。

  • 它使用与 Docker 相同的工具,比如图像注册和容器托管解决方案。

所以每个 Docker 用户都可以转投波德曼。我们可以保留我们和 Docker 做的一切。在有些情况下,你不能使用波德曼作为 Docker 的替代品。例如,在我的例子中,我使用TestContainers为我的 JUnit 测试获取数据库的轻量级实例。这个伟大的库非常依赖 Docker 来为测试提供数据库,不幸的是,没有将它迁移到 Podman 的活动任务。img/509649_1_En_1_Figam_HTML.gif img/509649_1_En_1_Figan_HTML.gif你可以在 testcontainers-java#2088 查看这一期。

结论

容器化是进入开发者世界的最强大的技术之一。许多其他的技术都是基于容器诞生的,比如 Podman、Buildah 等等。当然,你不会全部用到它们,但是你可以挑选一些合适的来帮助你完成工作。

二、单体架构简介

对实际情况的介绍

如今,我们使用许多方法来访问在线应用。例如,我们可以使用浏览器或客户端应用访问脸书或推特。这可以通过它们公开的应用编程接口(API)来实现。API 是一个软件边界,它允许两个应用使用特定的协议(例如 HTTP)相互通信。在脸书环境中,当人们使用移动应用发送消息时,应用使用脸书 API 发出请求。接下来,应用使用其业务逻辑,进行所有必要的数据库查询,并通过返回 HTTP 响应来完成请求。

我们的主要竞争对手亚马逊、易贝和全球速卖通就是这种情况。img/509649_1_En_2_Figa_HTML.gif是的!您将在本书中使用您的 Java 技能来开发一个电子商务应用,这将打败他们所有人!img/509649_1_En_2_Figb_HTML.gif

img/509649_1_En_2_Figc_HTML.gif是的!这本书涵盖了一个网上商店用例。img/509649_1_En_2_Figd_HTML.gif

这个电子商务应用将有许多组件来管理客户,订单,产品,评论,购物车,认证等。

我们正在为不同的团队招募许多开发人员。每个团队都将致力于一个特定的业务领域,每个业务领域都将有其专用的代码。我们有一个架构师,他将负责打包和构建应用。

Java 应用将被部署为一个巨大的WAR文件,该文件包装了来自不同团队的所有代码以及项目的所有外部依赖项,比如框架和库。为了将这个应用交付给生产团队,项目经理必须确保所有的软件团队按时交付他们的代码。如果其中一个团队因为任何原因延迟,应用肯定会延迟,因为它不能交付未完成的部分。

→如何解决这个问题?img/509649_1_En_2_Fige_HTML.gif

我们还听说了敏捷性。我们的项目可以采用敏捷吗?我们能从敏捷过程中获益吗?

→如何解决这个问题?img/509649_1_En_2_Figf_HTML.gif

我们知道,在很多时候,我们的电子商务网站会受到顾客的狂轰滥炸,比如黑色星期五、圣诞节前几天、情人节等。我们要求我们的生产团队为此应用提供高可用性。解决方案是让应用有许多正在运行的实例,以确保它能够处理负载。但是这个解决方案是最好的吗?我们当前的架构在高可用性方面有优势吗?

→如何解决这个问题?img/509649_1_En_2_Figg_HTML.gif

介绍背景

该应用的实际架构是单体,这意味着它由一个应用组成,该应用:

  • 设计简单:我们可以轻松地设计和重构组件,因为我们对整个生态系统有一个全面的了解。

  • 开发简单:当前开发工具和 ide 的目标是支持单体应用的开发。

  • 简单部署:我们需要将WAR文件(或目录层次结构)部署到适当的运行时。

  • 易于扩展:我们可以通过在负载平衡器后运行应用的多个副本来扩展应用。

然而,一旦应用变得很大,团队的规模也变大了,这种方法就有了许多越来越明显的缺点:

  • 日益增加的复杂性:一个庞大的整体代码库让开发人员感到害怕,尤其是新开发人员。源代码可能难以阅读/理解和重构。因此,开发通常会变慢。很难理解如何正确地实现变更,这会降低代码的质量。

  • 过载的开发机器:大型应用会导致开发机器负载过重,从而降低生产率。

  • 过载的服务器:大型应用很难监控,需要大量的服务器资源,这会影响工作效率。

  • 连续部署很困难:大型的、单一的应用也很难部署,因为它需要从事不同服务的团队之间强有力的同步。此外,要更新一个组件,您必须重新部署整个应用。与重新部署相关的风险增加了,这阻碍了频繁的更新。这对于用户界面开发人员来说尤其成问题,因为他们通常需要快速迭代和频繁重新部署。

  • 扩展应用可能很困难:整体架构只能在一个维度上扩展:仅仅通过创建整体的副本。应用实例的每个副本都将访问所有数据,这使得缓存效率降低,并增加了内存消耗和 I/O 流量。此外,不同的应用组件有不同的资源需求。一个可能是 CPU 密集型,另一个可能是内存密集型。对于整体架构,我们无法独立扩展每个组件。

  • 规模化发展的障碍:单体应用也是规模化发展的障碍。一旦应用达到一定的规模,将工程组织分成专注于特定功能领域的团队是很有用的。单一应用的问题在于它阻止了团队独立工作。团队必须协调他们的开发和部署工作。对于一个团队来说,做出改变和更新产品要困难得多。

解决这些问题

为了解决这些问题,我们将讨论微服务:这本书的主题,也是当今最时髦的词汇之一,因为我正在写这本书。这是我们在接下来的几章中要讨论的内容。敬请关注!img/509649_1_En_2_Figh_HTML.gif

祝你阅读愉快。祝你好运!img/509649_1_En_2_Figj_HTML.gif

三、编写单体应用

呈现域

注册用户可以从网上商店购买他们在目录中找到的产品。顾客也可以阅读和发表关于他们购买的文章的评论。为了简化支付用例,我们将使用 Stripe 或 PayPal 作为支付网关。

在本书中,我们将不涉及支付事务。

用例图

该应用提供以下功能:

  • 对于客户:搜索和浏览产品,浏览和撰写评论,创建,查看和更新购物车,更新个人资料,结帐和支付。

  • 对于商店经理:添加产品、更新产品和删除产品。

img/509649_1_En_3_Figb_HTML.png

类图

类图将如下所示:

img/509649_1_En_3_Figc_HTML.jpg

程序表

典型购物旅行的序列图如下所示:

img/509649_1_En_3_Figd_HTML.png

编写应用代码

差不多是时候开始编写应用了。在攻击代码之前,让我们看看我们将使用的技术堆栈。

展示技术堆栈

在本书中,我们正在实现一个电子商务应用的后端,所以我们的堆栈将是:

  • img/509649_1_En_3_Fige_HTML.gif PostgreSQL 13

  • img/509649_1_En_3_Figf_HTML.gif Java 11 与 GraalVM 21.0.x

  • Maven 3.6.2+版

  • Quarkus 1.13 以上

  • 最新版本的img/509649_1_En_3_Figg_HTML.gif Docker

PostgreSQL 数据库

作为一项要求,您需要有一个img/509649_1_En_3_Figh_HTML.gif PostgreSQL 实例。您可以使用您的img/509649_1_En_3_Figi_HTML.gif Docker 技能快速创建它,只需输入:

docker run -d --name demo-postgres \          ①
        -e POSTGRES_USER=developer \          ②
        -e POSTGRES_PASSWORD=p4SSW0rd \       ③
        -e POSTGRES_DB=demo \                 ④
        -p 5432:5432 postgres:13              ⑤

  • ③转发端口54325432,使用官方 PostgreSQL 镜像 v13。

  • Postgres 用户:developer

  • Postgres 密码:p4SSW0rd

  • Postgres 数据库:demo

  • ①在分离模式下运行名为demo-postgres的容器(作为后台守护程序)。

  • ②定义环境变量:

Java 11

img/509649_1_En_3_Figj_HTML.gif Java Standard Edition 11 是一个主要特性版本,于 2018 年 9 月 25 日发布。

img/509649_1_En_3_Figk_HTML.gif你可以在 https://openjdk.java.net/projects/jdk/11/ 查看img/509649_1_En_3_Figl_HTML.gif Java 11 新特性。

你能看出为什么我们使用 Java 11 而不是 16(写这本书时的最新版本)吗?这是因为 Java 11 是 GraalVM 可用的最高版本。

Quarkus 团队强烈建议使用 Java 11,因为 Quarkus 2.x 不支持 Java 8。

专家

下面是 Maven 的官方介绍,来自其网站:

  • Maven 是一个意第绪语单词,意思是知识的积累者,最初是为了简化 Jakarta 涡轮机项目的建造过程。有几个项目,每个项目都有自己的 Ant 构建文件,这些文件略有不同,jar 被签入 CVS。我们需要一个标准的方法来构建项目,一个清晰的项目组成定义,一个发布项目信息的简单方法,以及一个在几个项目间共享 jar 的方法。

  • 结果是一个工具,现在可以用于构建和管理任何基于 Java 的项目。我们希望我们已经创造了一些东西,使 Java 开发人员的日常工作变得更容易,并且通常有助于理解任何基于 Java 的项目。

Maven 的主要目标是让开发人员在最短的时间内理解开发工作的完整状态。为了达到这个目标,Maven 试图解决几个方面的问题:

  • 简化构建过程

  • 提供统一的构建系统

  • 提供高质量的项目信息

  • 为最佳实践开发提供指南

  • 允许透明迁移到新功能

img/509649_1_En_3_Figo_HTML.jpg

quartus 框架

Quarkus 是一个为 JVM 和原生编译开发的全栈、云原生 Java 框架,专门针对容器优化 Java,使其成为无服务器、云和 Kubernetes 环境的有效平台。

Quarkus 旨在与流行的 Java 标准、框架和库一起工作,如 Eclipse MicroProfile 和 Spring,以及 Apache Kafka、RESTEasy (JAX-RS)、Hibernate ORM (JPA)、Spring、Infinispan 等等。

Quarkus 中的依赖注入机制基于 CDI(contexts and dependency injection ),包括一个扩展框架,用于扩展功能以及配置、引导和集成框架到您的应用中。添加扩展就像添加 Maven 依赖项一样简单。

它还使用构建本机二进制文件所需的必要元数据来配置 GraalVM。

Quarkus 从一开始就被设计为易于使用,其功能只需很少甚至不需要配置就能很好地工作。

开发人员可以为他们的应用选择他们想要的 Java 框架,这些框架可以在 JVM 模式下运行,也可以在本机模式下编译并运行。

Quarkus 还包括以下功能:

  • 实时编码,以便开发人员可以立即检查代码更改的效果,并快速解决问题

  • 具有嵌入式管理事件总线的统一命令式和反应式编程

  • 统一配置

  • 轻松生成本机可执行文件

img/509649_1_En_3_Figp_HTML.jpg

JetBrains IntelliJ 想法

IntelliJ IDEA 是 JVM 语言的旗舰 JetBrains IDE,旨在最大限度地提高开发人员的工作效率。

img/509649_1_En_3_Figq_HTML.png

IntelliJ IDEA 通过一系列功能帮助您保持高效开发,如智能编码辅助、可靠重构、动态代码分析、智能代码导航、内置开发工具、web 和企业开发支持等。

IntelliJ IDEA Ultimate 为微服务框架和技术提供一流的支持,包括 Quarkus、Micronaut、Spring、Helidon 和 OpenAPI。

专门针对 Quarkus,IntelliJ IDEA 包括 Quarkus 项目向导,它将引导您完成新项目的初始配置,并允许您指定其名称、Java 版本、构建工具、扩展等等。IDE 为 Quarkus 提供了智能代码洞察。属性和 YAML 配置文件。它还允许您创建 Quarkus 运行配置。您可以从一个位置(服务工具窗口)运行和调试配置、应用服务器、数据库会话、Docker 连接等。

IntelliJ IDEA Ultimate 在“终结点”工具窗口中为 HTTP 和 WebSocket 协议提供了项目中使用的客户端和服务器 API 的聚合视图。

使用集成的基于编辑器的 HTTP 客户端,您可以在测试 web 服务时在编辑器中编写、编辑和执行 HTTP 请求。

IntelliJ IDEA 允许您连接到本地运行的 Docker 机器来管理图像、容器和 Docker 组合服务。此外,IDE 还提供了对 Kubernetes 资源配置文件的支持。

JetBrains 向我所有的读者提供 IntelliJ IDEA Ultimate 的延长试用许可,有效期为三个月,而不是常规的一个月试用许可。img/509649_1_En_3_Figs_HTML.gif

您可以使用优惠券代码 IJBOOK202 赎回您的延长试用许可证。前往 https://www.jetbrains.com/store/redeem/ 赎回。

感谢 JetBrains 的支持!

优惠券代码仅对新用户有效。img/509649_1_En_3_Figu_HTML.gif

实施 QuarkuShop

现在我们将开始实现这个应用。我们将使用组成一个典型的 Java EE 应用的层来拆分实现。对于这种划分,我使用了一个旧的架构模式,称为实体控制边界,最初由 Ivar Jacobson 在 1992 年发表。该模式旨在根据职责对每个软件组件进行分类。

  • 实体持久层保存实体、JPA 存储库和相关的类。

  • 控制服务层保存服务、配置、批处理等。

  • 边界Web 层持有 Web 服务端点。

img/509649_1_En_3_Figv_HTML.png

生成头骨计划

开始了。在本节中,您将开始发现并使用 Quarkus 框架提供的强大特性和选项。

为了避免创建新项目和启动时的困难,Quarkus 团队创建了 Code Quarkus 项目。这是一个在线工具,用于轻松生成 Quarkus 应用结构。它提供了选择构建工具(Maven 或 Gradle)的能力,并挑选您想要添加到项目中的扩展。

Quarkus Extension

将 Quarkus 扩展视为项目依赖。扩展配置、引导和集成一个框架或技术到您的 Quarkus 应用中。他们还负责为 GraalVM 提供正确的信息,以便应用进行本地编译。

如果你习惯于 Spring Boot 的生态系统,Quarkus 代码相当于 Spring Initializr,Quarkus 扩展大致类似于 Spring Boot 启动器。

您可以通过几种方式生成基于 Quarkus 的应用:

mvn io.quarkus:quarkus-maven-plugin:1.13.2.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=getting-started \
    -DclassName="org.acme.getting.started.GreetingResource" \
    -Dpath="/hello"

要运行生成的应用,只需运行mvn quarkus:dev,如下所示:

$ mvn quarkus:dev
...
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< org.acme:getting-started >------------------
[INFO] Building getting-started 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]-----------------------------
[INFO]
[INFO] --- quarkus-maven-plugin:1.13.2.Final:dev (default-cli) @ getting-started ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] Nothing to compile - all classes are up to date
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-04-24 17:07:20,323 INFO  [io.quarkus] (Quarkus Main Thread) getting-started 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.2.Final) started in 1.476s. Listening on: http://localhost:8080
2021-04-24 17:07:20,336 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-04-24 17:07:20,336 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy]

What is the Role of MVN Quarkus:DEV?

您可以使用mvn quarkus:dev来运行它,它支持后台编译的热部署,这意味着当您修改 Java 文件或资源文件并刷新浏览器时,这些更改将自动生效。

这也适用于像配置属性文件这样的资源文件。

刷新浏览器的动作会触发对工作区的扫描,如果检测到任何更改,就会编译 Java 文件,并重新部署应用。然后,重新部署的应用将为您的请求提供服务。如果编译或部署有任何问题,错误页面会让您知道。

从 Quarkus 1.11 开始,mvn quarkus:dev将启用 Quarkus Dev UI,这是一个开发人员的控制台,可视化当前加载的所有扩展及其状态,以及到它们的文档的直接链接。开发人员用户界面如下所示:

img/509649_1_En_3_Figx_HTML.png

例如,我们可以看到由在http://localhost:8080/q/dev/io.quarkus.quarkus-arc/beans可用的 Arc 扩展列出的 Beans:

img/509649_1_En_3_Figy_HTML.png

当你去http://localhost:8080/index.html:

img/509649_1_En_3_Figz_HTML.jpg

对于这个 Quarkus 项目,您将使用 web 界面来生成项目 skull:

img/509649_1_En_3_Figaa_HTML.jpg

选择这些扩展:

  • RESTEasy JSON-B :为 RESTEasy 增加 JSON-B 序列化库支持。

  • SmallRye OpenAPI :基于 OpenAPI 规范记录您的 REST APIs,附带 Swagger UI。

  • Hibernate ORM :添加了用 Hibernate ORM 定义持久模型的所有需求,作为 JPA 实现。

  • Hibernate Validator :添加用于验证 REST APIs 的输入/输出和/或业务服务方法的参数和返回值的机制。

  • JDBC 驱动程序- PostgreSQL :添加了帮助您通过 JDBC 连接到 PostgreSQL 数据库的要求。

  • Quarkus 对 Spring 数据 JPA API 的扩展:将 Spring 数据 JPA 引入 Quarkus 来创建您的数据访问层,就像您在 Spring Boot 所习惯的那样。

  • Flyway :处理数据库模式迁移。

您需要将 Lombok 添加到pom.xml文件中。Project Lombok 是一个 Java 库,可以自动插入到您的编辑器和构建工具中,为您的 Java 增添趣味。Lombok 节省了编写 getter/setter/constructors/等样板代码的时间和精力。

下面是 Lombok Maven 的依赖关系:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.16</version>
</dependency>

当我们需要时,我们将在这个例子中添加更多的依赖项。

一旦创建了空白项目,就可以开始构建整体层了。让我们从持久层开始。

创建持久层

如果你回头看类图,你会看到这些Entity类:

  • Address

  • Cart

  • Category

  • Customer

  • Order

  • OrderItem

  • Payment

  • Product

  • Review

以下是一些列举:

  • CartStatus

  • OrderStatus

  • ProductStatus

  • PaymentStatus

下图说明了类别之间的关系:

img/509649_1_En_3_Figac_HTML.jpg

对于列表中的每个实体,您将创建:

  • JPA 实体

  • Spring 数据 JPA 存储库

这些实体将共享一些通常使用的属性,如idcreated date等。这些属性将位于由我们的实体扩展的AbstractEntity类中。

AbstractEntity看起来是这样的:

  • Lombok 注释为AbstractEntity类生成 getters 和 setters。

  • ②将该类声明为 JPA 基类,它包含将由子类实体【1】继承的属性。

  • ③使用AuditingEntityListener[2] 激活实体审计:

@Getter ①
@Setter ①
@MappedSuperclass ②
@EntityListeners(AuditingEntityListener.class) ③
public abstract class AbstractEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "created_date", nullable = false)
    private Instant createdDate;

    @Column(name = "last_modified_date")
    private Instant lastModifiedDate;
}

  • ①指定在将实体保存在数据库中之前,将调用带注释的方法。

  • ②指定在更新数据库中的实体之前调用带注释的方法。

public class AuditingEntityListener {
    @PrePersist ①
    void preCreate(AbstractEntity auditable) {
        Instant now = Instant.now();
        auditable.setCreatedDate(now);
        auditable.setLastModifiedDate(now);
    }

    @PreUpdate ②
    void preUpdate(AbstractEntity auditable) {
        Instant now = Instant.now();
        auditable.setLastModifiedDate(now);
    }
}

手推车

Cart实体看起来像这样:

  • ①这个 Lombok 注释为所有字段生成 getter/setter。

  • ②这个 Lombok 注释生成了一个无参数的构造函数,这是 JPA 需要的

  • ③这个 Lombok 注释基于当前的类字段并包括超类字段来生成toString()方法。

  • ④这是一个@Entity,它对应的@Table将被命名为carts

  • ⑤用于控制数据完整性的验证注释。如果状态为 null,将引发验证异常。【3】

  • ⑥列定义:名称、长度和为空性约束的定义。

@Getter ①
@Setter ①
@NoArgsConstructor ②
@ToString(callSuper = true) ③
@Entity ④
@Table(name = "carts") ④
public class Cart extends AbstractEntity {

    @ManyToOne
    private final Customer customer;

    @NotNull ⑤
    @Column(nullable = false) ⑥
    @Enumerated(EnumType.STRING)
    private final CartStatus status;

    public Cart(Customer customer, @NotNull CartStatus status) {
        this.customer = customer;
        this.status = status;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cart cart = (Cart) o;
        return Objects.equals(customer, cart.customer) &&
                status == cart.status;
    }

    @Override
    public int hashCode() {
        return Objects.hash(customer, status);
    }
}

img/509649_1_En_3_Figad_HTML.gif验证注释和@Column中定义的约束之间的区别在于,验证注释是应用范围的,而约束是数据库范围的。

CartRepository看起来是这样的:

  • ①表示一个带注释的类是一个存储库,最初由域驱动设计 (Eric Evans,2003)定义为“一种封装存储、检索和搜索行为的机制,它模拟一组对象。”

  • ②JPA Spring Data JPA 中某个知识库的特定扩展。这将使 Spring Data 能够找到这个接口,并自动为它创建一个实现。

  • ③这些方法使用 Spring 数据查询方法构建器机制自动实现查询。

@Repository ①
public interface CartRepository extends JpaRepository<Cart, Long> { ②

    List<Cart> findByStatus(CartStatus status); ③

    List<Cart> findByStatusAndCustomerId(CartStatus status, Long customerId); ③
}

What is a Spring Data JpaRepository?

JpaRepository延伸PagingAndSortingRepository,?? 又延伸CrudRepository。他们的主要职能是:

  • CrudRepository主要提供 CRUD 功能。

  • 提供了对记录进行分页和排序的方法。

  • JpaRepository提供了一些 JPA 相关的方法,比如批量刷新持久上下文和删除记录。

因为这里提到的继承,JpaRepository会拥有CrudRepositoryPagingAndSortingRepository的所有功能。所以如果你不需要存储库具备JpaRepositoryPagingAndSortingRepository提供的功能,就用CrudRepository

What is the Spring Data Query Methods Builder Mechanism?

Spring 数据存储库基础设施中内置的查询构建器机制对于在存储库的实体上构建约束查询非常有用。该机制从方法中去掉前缀findByreadByqueryBycountBygetBy,并开始解析其余部分。introducing 子句可以包含更多的表达式,比如在要创建的查询上设置 distinct 标志的Distinct。然而,第一个By作为一个定界符来指示实际标准的开始。在非常基本的层面上,您可以定义实体属性的条件,并用AndOr将它们连接起来。

img/509649_1_En_3_Figae_HTML.gif无需编写定制的 JPQL 查询。这就是为什么我使用 Spring Data JPA API 的 Quarkus 扩展来享受 Spring Data JPA 的这些强大特性。

汽车状况

CartStatus枚举类如下所示:

public enum CartStatus {
    NEW, CANCELED, CONFIRMED
}

地址

Address类看起来像这样:

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Embeddable
public class Address {

    @Column(name = "address_1")
    private String address1;

    @Column(name = "address_2")
    private String address2;

    @Column(name = "city")
    private String city;

    @NotNull
    @Size(max = 10)
    @Column(name = "postcode", length = 10, nullable = false)
    private String postcode;

    @NotNull
    @Size(max = 2)
    @Column(name = "country", length = 2, nullable = false)
    private String country;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(address1, address.address1) &&
                Objects.equals(address2, address.address2) &&
                Objects.equals(city, address.city) &&
                Objects.equals(postcode, address.postcode) &&
                Objects.equals(country, address.country);
    }

    @Override
    public int hashCode() {
        return Objects.hash(address1, address2, city, postcode, country);
    }
}

Address类将被用作可嵌入类。与实体类不同,可嵌入类用于表示实体的状态,但没有自己的持久身份。可嵌入类的实例共享拥有它的实体的身份。可嵌入的类只作为另一个实体的状态存在。

种类

Category实体看起来像这样:

@Getter
@NoArgsConstructor
@ToString(callSuper = true)
@Entity
@Table(name = "categories")
public class Category extends AbstractEntity {

    @NotNull
    @Column(name = "name", nullable = false)
    private String name;

    @NotNull
    @Column(name = "description", nullable = false)
    private String description;

    public Category(@NotNull String name, @NotNull String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Category category = (Category) o;
        return Objects.equals(name, category.name) &&
                Objects.equals(description, category.description);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, description);
    }
}

CategoryRepository看起来如下:

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
}

顾客

Customer实体如下:

@Getter @Setter
@NoArgsConstructor
@ToString(callSuper = true)
@Entity
@Table(name = "customers")
public class Customer extends AbstractEntity {

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Email
    @Column(name = "email")
    private String email;

    @Column(name = "telephone")
    private String telephone;

    @OneToMany(mappedBy = "customer")
    private Set<Cart> carts;

    @Column(name = "enabled", nullable = false)
    private Boolean enabled;

    public Customer(String firstName, String lastName, @Email String email,
                    String telephone, Set<Cart> carts, Boolean enabled) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.telephone = telephone;
        this.carts = carts;
        this.enabled = enabled;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Customer customer = (Customer) o;
        return Objects.equals(firstName, customer.firstName) &&
                Objects.equals(lastName, customer.lastName) &&
                Objects.equals(email, customer.email) &&
                Objects.equals(telephone, customer.telephone) &&
                Objects.equals(carts, customer.carts) &&
                Objects.equals(enabled, customer.enabled);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName, email, telephone, enabled);
    }
}

CustomerRepository如下所示:

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
    List<Customer> findAllByEnabled(Boolean enabled);
}

命令

Order实体如下:

@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@Entity
@Table(name = "orders")
public class Order extends AbstractEntity {

    @NotNull
    @Column(name = "total_price", precision = 10, scale = 2, nullable = false)
    private BigDecimal price;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private OrderStatus status;

    @Column(name = "shipped")
    private ZonedDateTime shipped;

    @OneToOne(cascade = CascadeType.REMOVE)
    @JoinColumn(unique = true)
    private Payment payment;

    @Embedded
    private Address shipmentAddress;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    private Set<OrderItem> orderItems;

    @OneToOne
    private Cart cart;

    public Order(@NotNull BigDecimal price, @NotNull OrderStatus status,
                 ZonedDateTime shipped, Payment payment, Address shipmentAddress,
                 Set<OrderItem> orderItems, Cart cart) {
        this.price = price;
        this.status = status;
        this.shipped = shipped;
        this.payment = payment;
        this.shipmentAddress = shipmentAddress;
        this.orderItems = orderItems;
        this.cart = cart;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Order order = (Order) o;
        return Objects.equals(price, order.price) && status == order.status &&
                Objects.equals(shipped, order.shipped) &&
                Objects.equals(payment, order.payment) &&
                Objects.equals(shipmentAddress, order.shipmentAddress) &&
                Objects.equals(orderItems, order.orderItems) &&
                Objects.equals(cart, order.cart);
    }

    @Override
    public int hashCode() {
        return Objects.hash(price, status, shipped, payment, shipmentAddress, cart);
    }
}

OrderRepository如下所示:

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCartCustomerId(Long customerId);
    Optional<Order> findByPaymentId(Long id);
}

OrderItem(订单项)

OrderItem实体如下:

@Getter @NoArgsConstructor
@ToString(callSuper = true)
@Entity @Table(name = "order_items")
public class OrderItem extends AbstractEntity {

    @NotNull
    @Column(name = "quantity", nullable = false)
    private Long quantity;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    public OrderItem(@NotNull Long quantity, Product product, Order order) {
        this.quantity = quantity;
        this.product = product;
        this.order = order;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderItem orderItem = (OrderItem) o;
        return Objects.equals(quantity, orderItem.quantity) &&
                Objects.equals(product, orderItem.product) &&
                Objects.equals(order, orderItem.order);
    }

    @Override
    public int hashCode() { return Objects.hash(quantity, product, order); }
}

OrderItemRepository如下所示:

@Repository
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
    List<OrderItem> findAllByOrderId(Long id);
}

支付

Payment实体如下:

@Getter @NoArgsConstructor
@ToString(callSuper = true)
@Entity @Table(name = "payments")
public class Payment extends AbstractEntity {

    @Column(name = "paypal_payment_id")
    private String paypalPaymentId;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private PaymentStatus status;

    @NotNull
    @Column(name = "amount", nullable = false)
    private BigDecimal amount;

    public Payment(String paypalPaymentId, @NotNull PaymentStatus status, @NotNull BigDecimal amount) {
        this.paypalPaymentId = paypalPaymentId;
        this.status = status;
        this.amount = amount;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Payment payment = (Payment) o;
        return Objects.equals(paypalPaymentId, payment.paypalPaymentId);
    }

    @Override
    public int hashCode() { return Objects.hash(paypalPaymentId); }
}

PaymentRepository如下所示:

@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
    List<Payment> findAllByAmountBetween(BigDecimal min, BigDecimal max);
}

PaymentStatus如下所示:

public enum PaymentStatus {
    ACCEPTED, PENDING, REFUSED, ERROR
}

产品

Product实体如下:

@Getter
@NoArgsConstructor
@ToString(callSuper = true)
@Entity
@Table(name = "products")
public class Product extends AbstractEntity {

    @NotNull
    @Column(name = "name", nullable = false)
    private String name;

    @NotNull
    @Column(name = "description", nullable = false)
    private String description;

    @NotNull
    @Column(name = "price", precision = 10, scale = 2, nullable = false)
    private BigDecimal price;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private ProductStatus status;

    @Column(name = "sales_counter")
    private Integer salesCounter;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
    @JoinTable(name = "products_reviews",
            joinColumns = @JoinColumn(name = "product_id"),
            inverseJoinColumns = @JoinColumn(name = "reviews_id"))
    private Set<Review> reviews = new HashSet<>();

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;

    public Product(@NotNull String name, @NotNull String description,
                   @NotNull BigDecimal price, @NotNull ProductStatus status,
                   Integer salesCounter, Set<Review> reviews, Category category) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.status = status;
        this.salesCounter = salesCounter;
        this.reviews = reviews;
        this.category = category;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Product product = (Product) o;
        return Objects.equals(name, product.name) &&
                Objects.equals(description, product.description) &&
                Objects.equals(price, product.price) && status == product.status &&
                Objects.equals(salesCounter, product.salesCounter) &&
                Objects.equals(reviews, product.reviews) &&
                Objects.equals(category, product.category);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, description, price, category);
    }
}

ProductRepository如下所示:

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByCategoryId(Long categoryId);

    Long countAllByCategoryId(Long categoryId);

    @Query("select p from Product p JOIN p.reviews r WHERE r.id = ?1")
    Product findProductByReviewId(Long reviewId);

    void deleteAllByCategoryId(Long id);

    List<Product> findAllByCategoryId(Long id);
}

产品状态

ProductStatus枚举类如下所示:

public enum ProductStatus {
    AVAILABLE, DISCONTINUED
}

回顾

Review实体如下:

@Getter
@NoArgsConstructor
@ToString(callSuper = true)
@Entity
@Table(name = "reviews")
public class Review extends AbstractEntity {

    @NotNull
    @Column(name = "title", nullable = false)
    private String title;

    @NotNull
    @Column(name = "description", nullable = false)
    private String description;

    @NotNull
    @Column(name = "rating", nullable = false)
    private Long rating;

    public Review(@NotNull String title, @NotNull String description, @NotNull Long rating) {
        this.title = title;
        this.description = description;
        this.rating = rating;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Review review = (Review) o;
        return Objects.equals(title, review.title) &&
                Objects.equals(description, review.description) &&
                Objects.equals(rating, review.rating);
    }

    @Override
    public int hashCode() {
        return Objects.hash(title, description, rating);
    }
}

ReviewRepository如下所示:

@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {

    @Query("select p.reviews from Product p where p.id = ?1")
    List<Review> findReviewsByProductId(Long id);
}

在这个阶段,您已经完成了实体和存储库的创建。实体图现在看起来像这样:

img/509649_1_En_3_Figaf_HTML.jpg

我用 IntelliJ IDEA 生成了这张图。要生成图表,只需右键单击包含目标类的包,然后选择“图表”“➤显示图表”。接下来,在打开的列表中,选择 Java 类图:

img/509649_1_En_3_Figah_HTML.jpg

现在您需要为 Quarkus 应用提供 Hibernate/JPA 配置和数据库凭证。

就像 Spring Boot 一样,Quarkus 将其配置和属性存储在位于src/main/resourcesapplication.properties文件中。

application.properties将在本地存储属性。我们可以用很多方法覆盖这些属性,例如,使用环境变量。

QuarkuShop 现在需要两种配置:

  • 数据源配置:访问数据库所需的所有属性和凭证,包括驱动程序类型、URL、用户名、密码和模式名:
# Datasource config properties
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=developer
quarkus.datasource.password=p4SSW0rd
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/demo

这些是我们在开始审查源代码之前刚刚创建的 Dockerized PostgreSQL 数据库的凭证。img/509649_1_En_3_Figaj_HTML.gif

  • fly way 配置:如果您回到您在生成应用时选择的扩展,我们选择 Flyway ,它将用于版本化数据库。您需要在应用中激活它。
# Flyway minimal config properties
quarkus.flyway.migrate-at-start=true

What is Database Versioning?

版本化数据库意味着共享应用正常运行所需的数据库的所有更改。数据库版本控制从初始数据库模式开始,也可以从一些数据开始。当应用的新版本中有数据库更改时,我们会发布一个新的补丁文件来对现有数据库执行更改,而不是从更新的转储文件开始,因此在部署新版本时,应用将正常运行。补丁文件描述了如何将现有数据库转换到新状态,以及如何将其恢复到旧状态。

在这种情况下,我们将有两个 Flyway 脚本,它们必须位于默认的 Flyway 文件夹src/main/resources/db/migration:

  • src/main/resources/db/migration/V1.0__Init_app.sql:初始框架创建脚本,包含创建表的 SQL 查询以及我们在源代码中定义的约束,比如主键、实体间的外键和不可空的列。这个 Flyway 脚本还将带来 ORM 所需的 Hibernate SQL 序列,以便在持久化时为实体提供 id。

  • src/main/resources/db/migration/V1.1__Insert_samples.sql:初始样本数据插入脚本,包含我们将插入到数据库中的样本数据,以便在执行过程中有样本数据。

img/509649_1_En_3_Figak_HTML.gif注意这里使用的版本控制——V1.0V1.1——这用于保证 Flyway 脚本的执行顺序。

现在我们可以进入下一层:服务层。

创建服务层

既然您已经创建了实体,那么是时候创建服务了。

一个服务是一个包装业务逻辑的组件。在这一点上,我们还没有讨论业务逻辑,我们只有 CRUD 操作。您将在服务中实现这些 CRUD 操作。

您需要为每个实体提供单独的服务,以便将单一责任实践应用到您的服务中。

服务层是持久层和 Web 层之间的粘合剂。服务将从存储库中获取数据,并将其业务逻辑应用于加载的数据。它将计算出的数据封装到一个包装器中,用于在服务和 Web 层之间传输数据。这个包装器被称为数据传输对象(DTO)。

Do You Really Need DTOs?

事实上,在许多情况下,您的应用中确实需要 dto。

让我们设想这样一种情况,您有一个列出数据库中可用用户的服务。如果您不使用 d to,而是发送回User类,您将把您的用户凭证作为User实体的封装字段中的密码传输给 web 服务以及它们背后的调用者。

典型服务:CartService

CartService看起来是这样的:

  • ①用于在类中生成一个logger的 Lombok 注释。使用时,您有一个static final log字段,初始化为您的类名,然后您可以使用它来编写日志语句。

  • ②指定该类是应用范围的。

  • @Transactional注释为应用提供了声明式控制事务边界的能力。

  • Java EE 界最著名的注释!它用于请求注释字段类型的实例。

@Slf4j ①
@ApplicationScoped ②
@Transactional ③
public class CartService {

    @Inject ④
    CartRepository cartRepository;

    @Inject ④
    CustomerRepository customerRepository;

    public List<CartDto> findAll() {
        log.debug("Request to get all Carts");
        return this.cartRepository.findAll()
                .stream()
                .map(CartService::mapToDto)
                .collect(Collectors.toList());
    }

    public List<CartDto> findAllActiveCarts() {
        return this.cartRepository.findByStatus(CartStatus.NEW)
                .stream()
                .map(CartService::mapToDto)
                .collect(Collectors.toList());
    }

    public Cart create(Long customerId) {
        if (this.getActiveCart(customerId) == null) {
            var customer =
                    this.customerRepository.findById(customerId).orElseThrow(() ->
                            new IllegalStateException("The Customer does not exist!"));

            var cart = new Cart(customer, CartStatus.NEW);

            return this.cartRepository.save(cart);
        } else {
            throw new IllegalStateException("There is already an active cart");
        }
    }

    public CartDto createDto(Long customerId) {
        return mapToDto(this.create(customerId));
    }

    @Transactional(SUPPORTS)
    public CartDto findById(Long id) {
        log.debug("Request to get Cart : {}", id);
        return this.cartRepository.findById(id).map(CartService::mapToDto).orElse(null);
    }

    public void delete(Long id) {
        log.debug("Request to delete Cart : {}", id);
        Cart cart = this.cartRepository.findById(id)
                .orElseThrow(() -> new IllegalStateException("Cannot find cart with id " + id));

        cart.setStatus(CartStatus.CANCELED);

        this.cartRepository.save(cart);
    }

    public CartDto getActiveCart(Long customerId) {
        List<Cart> carts = this.cartRepository
                .findByStatusAndCustomerId(CartStatus.NEW, customerId);
        if (carts != null) {

            if (carts.size() == 1) {
                return mapToDto(carts.get(0));
            }
            if (carts.size() > 1) {
                throw new IllegalStateException("Many active carts detected !!!");
            }
        }
        return null;
    }

    public static CartDto mapToDto(Cart cart) {
        return new CartDto(
                cart.getId(),
                CustomerService.mapToDto(cart.getCustomer()),
                cart.getStatus().name()
        );
    }
}

What is @ApplicationScoped?

@ApplicationScoped标注的对象在应用生命周期中创建一次。

Quarkus 支持 for Java 2.0 的上下文和依赖注入中定义的所有内置作用域,除了@ConversationScoped:

  • @ApplicationScoped

  • @Singleton

  • @RequestScoped

  • @Dependent

  • @SessionScoped

要了解更多关于 CDI 示波器的信息以及它们之间的区别,你可以在 https://quarkus.io/guides/cdi 查看 Quarkus CDI 指南。

CartDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CartDto {
    private Long id;
    private CustomerDto customer;
    private String status;
}

地址服务

AddressService类看起来像这样:

@ApplicationScoped
public class AddressService {

    public static Address createFromDto(AddressDto addressDto) {
        return new Address(
                addressDto.getAddress1(),
                addressDto.getAddress2(),
                addressDto.getCity(),
                addressDto.getPostcode(),
                addressDto.getCountry()
        );
    }

    public static AddressDto mapToDto(Address address) {
        return new AddressDto(
                address.getAddress1(),
                address.getAddress2(),
                address.getCity(),
                address.getPostcode(),
                address.getCountry()
        );
    }
}

AddressDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddressDto {
    private String address1;
    private String address2;
    private String city;
    private String postcode;
    @Size(min = 2, max = 2)
    private String country;
}

类别服务

CategoryService类看起来像这样:

@Slf4j
@ApplicationScoped
@Transactional
public class CategoryService {
    @Inject
    CategoryRepository categoryRepository;
    @Inject
    ProductRepository productRepository;

    public static CategoryDto mapToDto(Category category, Long productsCount) {
        return new CategoryDto(
                category.getId(),
                category.getName(),
                category.getDescription(),
                productsCount);
    }

    public List<CategoryDto> findAll() {
        log.debug("Request to get all Categories");
        return this.categoryRepository.findAll()
                .stream().map(category ->
                        mapToDto(category,
                                productRepository
                                        .countAllByCategoryId(category.getId())))
                .collect(Collectors.toList());
    }

    public CategoryDto findById(Long id) {
        log.debug("Request to get Category : {}", id);
        return this.categoryRepository.findById(id).map(category ->
                        mapToDto(category,
                                productRepository
                                        .countAllByCategoryId(category.getId())))
                .orElse(null);
    }

    public CategoryDto create(CategoryDto categoryDto) {
        log.debug("Request to create Category : {}", categoryDto);
        return mapToDto(this.categoryRepository
                .save(new Category(
                                categoryDto.getName(),
                                categoryDto.getDescription())
                ), 0L);
    }

    public void delete(Long id) {
        log.debug("Request to delete Category : {}", id);
        log.debug("Deleting all products for the Category : {}", id);
        this.productRepository.deleteAllByCategoryId(id);
        log.debug("Deleting Category : {}", id);
        this.categoryRepository.deleteById(id);
    }

    public List<ProductDto> findProductsByCategoryId(Long id) {
        return this.productRepository.findAllByCategoryId(id)
                .stream()
                .map(ProductService::mapToDto)
                .collect(Collectors.toList());
    }
}

CategoryDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CategoryDto {
    private Long id;
    private String name;
    private String description;
    private Long products;
}

客户服务

CustomerService类看起来像这样:

@Slf4j
@ApplicationScoped
@Transactional
public class CustomerService {

    @Inject
    CustomerRepository customerRepository;

    public CustomerDto create(CustomerDto customerDto) {
        log.debug("Request to create Customer : {}", customerDto);
        return mapToDto(this.customerRepository.save(
                        new Customer(customerDto.getFirstName(),
                                     customerDto.getLastName(),
                                     customerDto.getEmail(),
                                     customerDto.getTelephone(),
                                     Collections.emptySet(),
                                     Boolean.TRUE)
                ));
    }

    public List<CustomerDto> findAll() {
        log.debug("Request to get all Customers");
        return this.customerRepository.findAll()
                .stream()
                .map(CustomerService::mapToDto)
                .collect(Collectors.toList());
    }

    @Transactional
    public CustomerDto findById(Long id) {
        log.debug("Request to get Customer : {}", id);
        return this.customerRepository.findById(id)
                .map(CustomerService::mapToDto).orElse(null);
    }

    public List<CustomerDto> findAllActive() {
        log.debug("Request to get all active customers");
        return this.customerRepository.findAllByEnabled(true)
                .stream().map(CustomerService::mapToDto)
                .collect(Collectors.toList());
    }

    public List<CustomerDto> findAllInactive() {
        log.debug("Request to get all inactive customers");
        return this.customerRepository.findAllByEnabled(false)
                .stream().map(CustomerService::mapToDto)
                .collect(Collectors.toList());
    }

    public void delete(Long id) {
        log.debug("Request to delete Customer : {}", id);

        Customer customer = this.customerRepository.findById(id)
                .orElseThrow(() ->
                        new IllegalStateException("Cannot find Customer with id " + id));

        customer.setEnabled(false);
        this.customerRepository.save(customer);
    }

    public static CustomerDto mapToDto(Customer customer) {
        return new CustomerDto(customer.getId(),
                customer.getFirstName(),
                customer.getLastName(),
                customer.getEmail(),
                customer.getTelephone()
        );
    }
}

CustomerDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CustomerDto {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private String telephone;
}

订单服务

OrderItemService类看起来像这样:

@Slf4j
@ApplicationScoped
@Transactional
public class OrderItemService {

    @Inject
    OrderItemRepository orderItemRepository;
    @Inject
    OrderRepository orderRepository;
    @Inject
    ProductRepository productRepository;

    public static OrderItemDto mapToDto(OrderItem orderItem) {
        return new OrderItemDto(
                orderItem.getId(),
                orderItem.getQuantity(),
                orderItem.getProduct().getId(),
                orderItem.getOrder().getId()
        );
    }

    public OrderItemDto findById(Long id) {
        log.debug("Request to get OrderItem : {}", id);
        return this.orderItemRepository.findById(id)
                .map(OrderItemService::mapToDto).orElse(null);
    }

    public OrderItemDto create(OrderItemDto orderItemDto) {
        log.debug("Request to create OrderItem : {}", orderItemDto);
        var order =
                this.orderRepository
                        .findById(orderItemDto.getOrderId())
                        .orElseThrow(() ->
                           new IllegalStateException("The Order does not exist!"));

        var product =
                this.productRepository
                        .findById(orderItemDto.getProductId())
                        .orElseThrow(() ->
                           new IllegalStateException("The Product does not exist!"));

        var orderItem = this.orderItemRepository.save(
                new OrderItem(
                        orderItemDto.getQuantity(),
                        product,
                        order
                ));
        order.setPrice(order.getPrice().add(orderItem.getProduct().getPrice()));
        this.orderRepository.save(order);

        return mapToDto(orderItem);
    }

    public void delete(Long id) {
        log.debug("Request to delete OrderItem : {}", id);

        var orderItem = this.orderItemRepository.findById(id)
                .orElseThrow(() ->
                        new IllegalStateException("The OrderItem does not exist!"));

        var order = orderItem.getOrder();
        order.setPrice(order.getPrice().subtract(orderItem.getProduct().getPrice()));

        this.orderItemRepository.deleteById(id);

        order.getOrderItems().remove(orderItem);

        this.orderRepository.save(order);
    }

    public List<OrderItemDto> findByOrderId(Long id) {
        log.debug("Request to get all OrderItems of OrderId {}", id);
        return this.orderItemRepository.findAllByOrderId(id)
                .stream()
                .map(OrderItemService::mapToDto)
                .collect(Collectors.toList());
    }
}

OrderItemDto类看起来像这样:

@Data @NoArgsConstructor @AllArgsConstructor
public class OrderItemDto {
    private Long id;
    private Long quantity;
    private Long productId;
    private Long orderId;
}

订单服务

OrderService类看起来像这样:

@Slf4j
@ApplicationScoped @Transactional
public class OrderService {

    @Inject OrderRepository orderRepository;
    @Inject PaymentRepository paymentRepository;
    @Inject CartRepository cartRepository;

    public List<OrderDto> findAll() {
        log.debug("Request to get all Orders");
        return this.orderRepository.findAll().stream().map(OrderService::mapToDto)
                .collect(Collectors.toList());
    }

    public OrderDto findById(Long id) {
        log.debug("Request to get Order : {}", id);
        return this.orderRepository.findById(id)
                .map(OrderService::mapToDto).orElse(null);
    }

    public List<OrderDto> findAllByUser(Long id) {
        return this.orderRepository.findByCartCustomerId(id)
                .stream().map(OrderService::mapToDto).collect(Collectors.toList());
    }

    public OrderDto create(OrderDto orderDto) {
        log.debug("Request to create Order : {}", orderDto);

        Long cartId = orderDto.getCart().getId();
        Cart cart = this.cartRepository.findById(cartId)
                .orElseThrow(() -> new IllegalStateException(
                            "The Cart with ID[" + cartId + "] was not found !"));

        return mapToDto(this.orderRepository.save(new Order(BigDecimal.ZERO,
                           OrderStatus.CREATION, null, null,
                           AddressService.createFromDto(orderDto.getShipmentAddress()),
                           Collections.emptySet(), cart)));
    }

    @Transactional
    public void delete(Long id) {
        log.debug("Request to delete Order : {}", id);

        Order order = this.orderRepository.findById(id)
                .orElseThrow(() ->
                    new IllegalStateException(
                        "Order with ID[" + id + "] cannot be found!"));

        Optional.ofNullable(order.getPayment())
                .ifPresent(paymentRepository::delete);

        orderRepository.delete(order);
    }

    public boolean existsById(Long id) {
        return this.orderRepository.existsById(id);
    }

    public static OrderDto mapToDto(Order order) {
        Set<OrderItemDto> orderItems = order.getOrderItems()
                .stream().map(OrderItemService::mapToDto).collect(Collectors.toSet());

        return new OrderDto(
                order.getId(),
                order.getPrice(),
                order.getStatus().name(),
                order.getShipped(),
                order.getPayment() != null ? order.getPayment().getId() : null,
                AddressService.mapToDto(order.getShipmentAddress()),
                orderItems,
                CartService.mapToDto(order.getCart())
        );
    }
}

OrderDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderDto {
    private Long id;
    private BigDecimal totalPrice;
    private String status;
    private ZonedDateTime shipped;
    private Long paymentId;
    private AddressDto shipmentAddress;
    private Set<OrderItemDto> orderItems;
    private CartDto cart;
}

支付服务

PaymentService类看起来像这样:

@Slf4j
@ApplicationScoped @Transactional
public class PaymentService {

    @Inject
    PaymentRepository paymentRepository;
    @Inject
    OrderRepository orderRepository;

    public List<PaymentDto> findByPriceRange(Double max) {
        return this.paymentRepository
                .findAllByAmountBetween(BigDecimal.ZERO, BigDecimal.valueOf(max))
                .stream().map(payment -> mapToDto(payment,
                                    findOrderByPaymentId(payment.getId()).getId()))
                .collect(Collectors.toList());
    }

    public List<PaymentDto> findAll() {
        return this.paymentRepository.findAll().stream()
                .map(payment -> findById(payment.getId())).collect(Collectors.toList());
    }

    public PaymentDto findById(Long id) {
        log.debug("Request to get Payment : {}", id);
        Order order = findOrderByPaymentId(id).orElseThrow(() ->
                            new IllegalStateException("The Order does not exist!"));

        return this.paymentRepository.findById(id)
                .map(payment -> mapToDto(payment, order.getId())).orElse(null);
    }

    public PaymentDto create(PaymentDto paymentDto) {
        log.debug("Request to create Payment : {}", paymentDto);

        Order order = this.orderRepository.findById(paymentDto.getOrderId())
                        .orElseThrow(() ->
                            new IllegalStateException("The Order does not exist!"));
        order.setStatus(OrderStatus.PAID);

        Payment payment = this.paymentRepository.saveAndFlush(new Payment(
                paymentDto.getPaypalPaymentId(),
                PaymentStatus.valueOf(paymentDto.getStatus()),
                order.getPrice()
        ));

        this.orderRepository.saveAndFlush(order);

        return mapToDto(payment, order.getId());
    }

    private Order findOrderByPaymentId(Long id) {
        return this.orderRepository.findByPaymentId(id).orElseThrow(() ->
            new IllegalStateException("No Order exists for the Payment ID " + id));
    }

    public void delete(Long id) {
        log.debug("Request to delete Payment : {}", id);
        this.paymentRepository.deleteById(id);
    }

    public static PaymentDto mapToDto(Payment payment, Long orderId) {
        if (payment != null) {
            return new PaymentDto(
                    payment.getId(),
                    payment.getPaypalPaymentId(),
                    payment.getStatus().name(),
                    orderId);
        }
        return null;
    }
}

PaymentDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PaymentDto {
    private Long id;
    private String paypalPaymentId;
    private String status;
    private Long orderId;
}

产品服务

ProductService类看起来像这样:

@Slf4j
@ApplicationScoped
@Transactional
public class ProductService {

    @Inject
    ProductRepository productRepository;
    @Inject
    CategoryRepository categoryRepository;

    public List<ProductDto> findAll() {
        log.debug("Request to get all Products");
        return this.productRepository.findAll()
                .stream().map(ProductService::mapToDto)
                .collect(Collectors.toList());
    }

    public ProductDto findById(Long id) {
        log.debug("Request to get Product : {}", id);
        return this.productRepository.findById(id)
                    .map(ProductService::mapToDto).orElse(null);
    }

    public Long countAll() {
        return this.productRepository.count();
    }

    public Long countByCategoryId(Long id) {
        return this.productRepository.countAllByCategoryId(id);
    }

    public ProductDto create(ProductDto productDto) {
        log.debug("Request to create Product : {}", productDto);

        return mapToDto(this.productRepository.save(
                new Product(
                        productDto.getName(),
                        productDto.getDescription(),
                        productDto.getPrice(),
                        ProductStatus.valueOf(productDto.getStatus()),
                        productDto.getSalesCounter(),
                        Collections.emptySet(),
                        categoryRepository.findById(productDto.getCategoryId())
                                          .orElse(null)
                )));
    }

    public void delete(Long id) {
        log.debug("Request to delete Product : {}", id);
        this.productRepository.deleteById(id);
    }

    public List<ProductDto> findByCategoryId(Long id) {
        return this.productRepository.findByCategoryId(id).stream()
                .map(ProductService::mapToDto).collect(Collectors.toList());
    }

    public static ProductDto mapToDto(Product product) {
        return new ProductDto(
                product.getId(),
                product.getName(),
                product.getDescription(),
                product.getPrice(),
                product.getStatus().name(),
                product.getSalesCounter(),
                product.getReviews().stream().map(ReviewService::mapToDto)
                                    .collect(Collectors.toSet()),
                product.getCategory().getId()
        );
    }
}

ProductDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductDto {
    private Long id;
    private String name;
    private String description;
    private BigDecimal price;
    private String status;
    private Integer salesCounter;
    private Set<ReviewDto> reviews;
    private Long categoryId;
}

审查服务

ReviewService类看起来像这样:

@Slf4j
@ApplicationScoped
@Transactional
public class ReviewService {

    @Inject
    ReviewRepository reviewRepository;

    @Inject
    ProductRepository productRepository;

    public List<ReviewDto> findReviewsByProductId(Long id) {
        log.debug("Request to get all Reviews");
        return this.reviewRepository.findReviewsByProductId(id)
                .stream()
                .map(ReviewService::mapToDto)
                .collect(Collectors.toList());
    }

    public ReviewDto findById(Long id) {
        log.debug("Request to get Review : {}", id);
        return this.reviewRepository
                    .findById(id)
                    .map(ReviewService::mapToDto)
                    .orElse(null);
    }

    public ReviewDto create(ReviewDto reviewDto, Long productId) {
        log.debug("Request to create Review : {} ofr the Product {}",
                reviewDto, productId);

        Product product = this.productRepository.findById(productId)
                .orElseThrow(() ->
                    new IllegalStateException(
                        "Product with ID:" + productId + " was not found !"));

        Review savedReview = this.reviewRepository.saveAndFlush(
                new Review(
                        reviewDto.getTitle(),
                        reviewDto.getDescription(),
                        reviewDto.getRating()));

        product.getReviews().add(savedReview);
        this.productRepository.saveAndFlush(product);

        return mapToDto(savedReview);
    }

    public void delete(Long reviewId) {
        log.debug("Request to delete Review : {}", reviewId);

        Review review = this.reviewRepository.findById(reviewId)
                .orElseThrow(() ->
                    new IllegalStateException(
                        "Product with ID:" + reviewId + " was not found !"));

        Product product = this.productRepository.findProductByReviewId(reviewId);

        product.getReviews().remove(review);

        this.productRepository.saveAndFlush(product);
        this.reviewRepository.delete(review);
    }

    public static ReviewDto mapToDto(Review review) {
        return new ReviewDto(
                review.getId(),
                review.getTitle(),
                review.getDescription(),
                review.getRating()
        );
    }
}

ReviewDto类看起来像这样:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReviewDto {
    private Long id;
    private String title;
    private String description;
    private Long rating;
}

创建 Web 图层

本节将我们在服务类中实现的操作公开为 REST web 服务。

在 Spring 框架中,可以使用RestController实现 REST web 服务。

img/509649_1_En_3_Figal_HTML.gif * REST API 基路径*

我们希望我们的 RESTful web 服务可以通过/api/carts/api/orders等被访问。,所以我们需要将基路径/api定义为根路径,供所有 REST web 服务重用。

这可以在 Quarkus 中使用以下属性进行配置:

quarkus.http.root-path=/api

典型的 RestController: CartResource

CartResource看起来是这样的:

  • ①标识资源类或类方法将用于服务请求的 URI 路径。

  • ②默认情况下,所有方法生成的所有内容都在 JSON 中。如果您想改变这一点,或者如果您想添加另一种序列化格式,请使用@Produces注释。

@Path("/carts") ①
public class CartResource { ②

    @Inject CartService cartService;

    @GET
    public List<CartDto> findAll() {
        return this.cartService.findAll();
    }

    @GET @Path("/active")
    public List<CartDto> findAllActiveCarts() {
        return this.cartService.findAllActiveCarts();
    }

    @GET @Path("/customer/{id}")
    public CartDto getActiveCartForCustomer(@PathParam("id") Long customerId) {
        return this.cartService.getActiveCart(customerId);
    }

    @GET @Path("/{id}")
    public CartDto findById(@PathParam("id") Long id) {
        return this.cartService.findById(id);
    }

    @POST @Path("/customer/{id}")
    public CartDto create(@PathParam("id") Long customerId) {
        return this.cartService.createDto(customerId);
    }

    @DELETE @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.cartService.delete(id);
    }
}

在这个 REST web 服务中,我们:

  • 列出所有购物车:HTTP GET on /api/carts

  • 列出活动购物车:HTTP GET on ``/api/carts/active`。

  • 为客户列出活动的购物车:HTTP GET on /api/carts/customer/{id} with {id},它保存了客户的 ID。

  • 列出购物车的所有细节:HTTP GET on api/carts/{id} with {id},保存购物车的 ID。

  • 为给定的客户创建一个新的购物车:HTTP POST on /api/carts/customer/{id} with {id},其中包含客户的 ID。

  • 删除购物车:HTTP DELETE on /api/carts/{id} with {id}

使用 OpenAPI 规范来描述这些操作,以方便外部调用者使用 REST web 服务。API 描述是自动生成的。img/509649_1_En_3_Figam_HTML.gif

类别资源

CategoryResource类看起来像这样:

@Path("/categories")
public class CategoryResource {
    @Inject CategoryService categoryService;
    @GET
    public List<CategoryDto> findAll() {
        return this.categoryService.findAll();
    }

    @GET @Path("/{id}")
    public CategoryDto findById(@PathParam("id") Long id) {
        return this.categoryService.findById(id);
    }

    @GET @Path("/{id}/products")
    public List<ProductDto> findProductsByCategoryId(@PathParam("id") Long id) {
        return this.categoryService.findProductsByCategoryId(id);
    }

    @POST     public CategoryDto create(CategoryDto categoryDto) {
        return this.categoryService.create(categoryDto);
    }

    @DELETE @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.categoryService.delete(id);
    }
}

客户资源

CustomerResource类看起来像这样:

@Path("/customers")
public class CustomerResource {

    @Inject
    CustomerService customerService;

    @GET
    public List<CustomerDto> findAll() {
        return this.customerService.findAll();
    }

    @GET
    @Path("/{id}")
    public CustomerDto findById(@PathParam("id") Long id) {
        return this.customerService.findById(id);
    }

    @GET
    @Path("/active")
    public List<CustomerDto> findAllActive() {
        return this.customerService.findAllActive();
    }

    @GET
    @Path("/inactive")
    public List<CustomerDto> findAllInactive() {
        return this.customerService.findAllInactive();
    }

    @POST
        public CustomerDto create(CustomerDto customerDto) {
        return this.customerService.create(customerDto);
    }

    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.customerService.delete(id);
    }
}

订单项资源

OrderItemResource类看起来像这样:

@Path("/order-items")
public class OrderItemResource {

    @Inject
    OrderItemService itemService;

    @GET
    @Path("/order/{id}")
    public List<OrderItemDto> findByOrderId(@PathParam("id") Long id) {
        return this.itemService.findByOrderId(id);
    }

    @GET
    @Path("/{id}")
    public OrderItemDto findById(@PathParam("id") Long id) {
        return this.itemService.findById(id);
    }

    @POST
        public OrderItemDto create(OrderItemDto orderItemDto) {
        return this.itemService.create(orderItemDto);
    }

    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.itemService.delete(id);
    }
}

订单资源

OrderResource类看起来像这样:

@Path("/orders")
public class OrderResource {

    @Inject
    OrderService orderService;

    @GET
    public List<OrderDto> findAll() {
        return this.orderService.findAll();
    }

    @GET
    @Path("/customer/{id}")
    public List<OrderDto> findAllByUser(@PathParam("id") Long id) {
        return this.orderService.findAllByUser(id);
    }

    @GET
    @Path("/{id}")
    public OrderDto findById(@PathParam("id") Long id) {
        return this.orderService.findById(id);
    }

    @POST
        public OrderDto create(OrderDto orderDto) {
        return this.orderService.create(orderDto);
    }

    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.orderService.delete(id);
    }

    @GET
    @Path("/exists/{id}")
    public boolean existsById(@PathParam("id") Long id) {
        return this.orderService.existsById(id);
    }
}

支付资源

PaymentResource类看起来像这样:

@Path("/payments")
public class PaymentResource {

    @Inject
    PaymentService paymentService;

    @GET
    public List<PaymentDto> findAll() {
        return this.paymentService.findAll();
    }

    @GET
    @Path("/{id}")
    public PaymentDto findById(@PathParam("id") Long id) {
        return this.paymentService.findById(id);
    }

    @POST
        public PaymentDto create(PaymentDto orderItemDto) {
        return this.paymentService.create(orderItemDto);
    }

    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.paymentService.delete(id);
    }

    @GET
    @Path("/price/{max}")
    public List<PaymentDto> findPaymentsByAmountRangeMax(@PathParam("max") double max) {
        return this.paymentService.findByPriceRange(max);
    }
}

产品资源

ProductResource类看起来像这样:

@Path("/products")
public class ProductResource {

    @Inject ProductService productService;

    @GET
    public List<ProductDto> findAll() {
        return this.productService.findAll();
    }

    @GET @Path("/count")
    public Long countAllProducts() {
        return this.productService.countAll();
    }

    @GET @Path("/{id}")
    public ProductDto findById(@PathParam("id") Long id) {
        return this.productService.findById(id);
    }

    @POST     public ProductDto create(ProductDto productDto) {
        return this.productService.create(productDto);
    }

    @DELETE @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.productService.delete(id);
    }

    @GET @Path("/category/{id}")
    public List<ProductDto> findByCategoryId(@PathParam("id") Long id) {
        return this.productService.findByCategoryId(id);
    }

    @GET @Path("/count/category/{id}")
    public Long countByCategoryId(@PathParam("id") Long id) {
        return this.productService.countByCategoryId(id);
    }
}

查看资源

ReviewResource类看起来像这样:

@Path("/reviews")
public class ReviewResource {

    @Inject ReviewService reviewService;

    @GET @Path("/product/{id}")
    public List<ReviewDto> findAllByProduct(@PathParam("id") Long id) {
        return this.reviewService.findReviewsByProductId(id);
    }

    @GET @Path("/{id}")
    public ReviewDto findById(@PathParam("id") Long id) {
        return this.reviewService.findById(id);
    }

    @POST @Path("/product/{id}")
        public ReviewDto create(ReviewDto reviewDto, @PathParam("id") Long id) {
        return this.reviewService.create(reviewDto, id);
    }

    @DELETE @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.reviewService.delete(id);
    }
}

自动化 API 文档

Swagger 2 是一个开源项目,用于描述和记录 RESTful APIs。它是语言无关的,可以扩展到 HTTP 之外的新技术和协议。当前版本定义了一组 HTML、JavaScript 和 CSS 资产,以便从符合 Swagger 的 API 动态生成文档。这些文件被 Swagger UI 项目捆绑在一起,以在浏览器上显示 API。除了呈现文档,Swagger UI 还允许其他 API 开发人员和消费者与 API 的资源进行交互,而不需要任何实现逻辑。

Swagger 2 规范,也称为 OpenAPI 规范,有几个实现。我们将在这个项目中使用 SmallRye OpenAPI 实现。

SmallRye OpenAPI 自动生成 API 文档。SmallRye OpenAPI 的工作方式是在构建时检查一次应用,根据 Quarkus 配置、类结构和各种编译时 Java 注释来推断 API 语义。

您已经将quarkus-smallrye-openapi依赖项添加到项目中。不需要进行任何额外的配置或开发。这个扩展将很容易生成 OpenAPI 描述符和 Swagger UI!

你好,世界大摇大摆!

要运行 Quarkus 应用,只需运行mvn quarkus:dev命令。

该应用将在 8080 端口上运行。要访问 Swagger UI,请转到http://localhost:8080/api/swagger-ui/

img/509649_1_En_3_Figan_HTML.jpg

您还可以检查生成的 OpenAPI 描述符,可在http://localhost:8080/api/openapi获得:

---
openapi: 3.0.1
info:
  title: Generated API
  version: "1.0"
paths:
  /api/carts:
    get:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCartDto'
  /api/carts/active:
    get:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCartDto'
...

Swagger UI 仅在非生产环境中可用。要永久启用它,您需要向application.properties添加一个参数:

# Swagger UI
quarkus.swagger-ui.always-include=true

最后,为了更好地组织代码,为所有 REST APIs 类添加一个 OpenAPI @Tag注释,以便描述每个类。这有助于将属于同一个 REST API 的所有方法重新组合到一个部分中。考虑一下CartResource的例子:

@Path("/carts")
@Tag(name = "cart", description = "All the cart methods")
public class CartResource {
...
}

img/509649_1_En_3_Figao_HTML.gif别忘了给每个类加上@Tag

当您重新启动应用并再次访问 Swagger UI 时,您可以看到对每个 REST API 的描述都出现了,并且这些方法都分组在同一个标记名下:

img/509649_1_En_3_Figap_HTML.jpg

自定义 quartus 横幅

本章的最后一部分将向您展示如何定制应用横幅,就像在 Spring Boot 所做的那样。这个操作在 Quarkus 非常容易。首先,你需要在src/main/resources中创建一个banner.txt文件,如清单 3-1 所示。

___  ____                       __          _____  __
_ __/ __ \ __  __ ____   _____ / /__ __  __/ ___/ / /_   ____   ____
 --/ / / // / / // __ \ / ___// //_// / / /\__ \ / __ \ / __ \ / __ \
 -/ /_/ // /_/ // /_/ // /   / ,<  / /_/ /___/ // / / // /_/ // /_/ /
--\___\_\\____/ \__,_//_/   /_/|_| \____//____//_/ /_/ \____// ,___/
                                                            /_/ Part of the #PlayingWith Series

Listing 3-1src/main/resources/banner.txt

然后,您只需要告诉应用您在banner.txt文件中有一个定制的横幅:

# Define the custom banner
quarkus.banner.path=banner.txt

结论

现在您已经拥有了项目的所有必要组件——源代码、API 文档和数据库。img/509649_1_En_3_Figaq_HTML.gif

在下一章中,您将创建必要的测试来保护您的代码免受未来的修改或重构。本章还深入探讨了使用最佳 CI/CD 管道构建和部署您的应用。

四、升级单体应用

编写源代码并运行它并不是真正的胜利!真正的成功是编写覆盖测试的代码,保证业务逻辑被正确实现。

测试覆盖率是一个非常重要的指标,它显示了代码的阴暗面。覆盖面越大,我们就越能确保我们的代码免受任何草率或肮脏的更新或重构。

在这个例子中,测试将是防止将这个单体应用拆分为微服务时出现问题的保护屏障。

实施 QuarkuShop 测试

quartus 中的测试库简介

在 Java 生态系统中,JUnit 是最常见的测试框架。这就是 Quarkus 在生成新项目时自动提供它作为测试依赖项的原因:

<dependencies>
...
    <!-- Test dependencies -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

请注意放心库,它是随 JUnit 5 一起推出的。

What is Rest Assured?

Rest Assured 是一个非常容易用 Java 测试和验证 REST web 服务的库。它只是发出 HTTP 请求并验证 HTTP 响应。它有一组非常丰富的匹配器和方法来获取数据和解析请求/响应。它与构建工具(如 Maven)和 ide 有很好的集成。

放心框架使用核心 Java 知识使 API 自动化测试变得非常简单,这是一件非常值得做的事情。

对于这些测试,我们需要另一个库:AssertJ。

What is AssertJ?

AssertJ 是一个开源的社区驱动库,它提供了丰富的断言和真正有用的错误消息。它提高了测试代码的可读性,并且在任何 IDE 或构建工具中都非常容易使用。

下面是要添加到您的pom.xml文件中的 AssertJ Maven 依赖项:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>

要使用 Maven 运行测试,只需键入mvn verify。应用将在test概要文件下执行测试。为了定义测试配置属性,比如将用于测试的数据库,您需要将这些属性添加到带有前缀%testapplication.properties中。这个前缀通知应用这些属性是用于test概要文件的。

What is An Application Profile?

应用的开发生命周期有不同的阶段;最常见的有开发测试生产。Quarkus 概要文件对应用配置的各个部分进行分组,使它们只在特定的环境中可用。

一个配置文件是一组配置设置。Quarkus 允许您使用属性的前缀%profile来定义特定于概要文件的属性。然后,它会根据激活的配置文件自动加载属性。参见清单 4-1 。

img/509649_1_En_4_Figa_HTML.gif当没有%profile出现时,该属性与所有配置文件相关联。

img/509649_1_En_4_Figb_HTML.gif在测试这本书的概念验证时,我发现有多个application.properties存在一个问题,就像我们过去对 Spring Boot 那样。我在夸库斯 GitHub #11072 中打开了一个问题。Quarkus 团队负责人之一圣乔治·安德里亚纳基斯告诉我,强烈建议只有一个application.properties文件。

  • ①您需要定义专用test数据库实例的参数和凭证。

  • ②您需要激活 Flyway 进行测试。

...=
# Test Datasource config properties
%test.quarkus.datasource.db-kind=postgresql     ①
%test.quarkus.datasource.username=developer
%test.quarkus.datasource.password=p4SSW0rd
%test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/test

# Test Flyway minimal config properties
%test.quarkus.flyway.migrate-at-start=true      ②

Listing 4-1src/main/resources/application.properties

img/509649_1_En_4_Figc_HTML.gif不需要复制src/test文件夹中的 Flyway 迁移脚本。在入住src/test/resources之前,Flyway 会在src/main/resources找到他们。

说到数据库,我们需要一个专门用于测试的数据库。我们将使用TestContainers来提供通用数据库的轻量级实例作为 Docker 容器。我们不需要几千字来定义TestContainers。你会通过实践锻炼发现它,爱上它。

首先添加TestContainers Maven 依赖项,如下所示:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
</dependency>

接下来,您将创建将TestContainers引入 Quarkus 的Glue类。这只是一个测试实用程序类。在测试代码下,在com.targa.labs.quarkushop.utils包中,您需要创建TestContainerResource类,如清单 4-2 所示。

  • TestContainerResource将实现QuarkusTestResourceLifecycleManager,它管理一个测试资源的生命周期。这些资源在第一次测试运行之前启动,在测试套件结束时关闭。使用@QuarkusTestResource(TestContainerResource.class)注释将该资源引入测试类。

  • ②这是这个自定义测试资源的核心元素。postgres:13参数是您将使用的 PostgreSQL Docker 映像的名称。

  • ③当调用start()方法时,首先启动DATABASE容器。

  • ④接下来,您收集由confMap中的TestContainers动态生成的Datasource凭证。当confMap返回时,这些凭证被应用,而不是那些在application.properties中可用的凭证。

  • ⑤当调用close()方法时,关闭DATABASE容器。

public class TestContainerResource implements QuarkusTestResourceLifecycleManager { ①

    private static final PostgreSQLContainer<?> DATABASE =
                                    new PostgreSQLContainer<>("postgres:13");   ②

    @Override
    public Map<String, String> start() {

        DATABASE.start();   ③

        Map<String, String> confMap = new HashMap<>();  ④

        confMap.put("quarkus.datasource.jdbc.url", DATABASE.getJdbcUrl());      ④
        confMap.put("quarkus.datasource.username", DATABASE.getUsername());     ④
        confMap.put("quarkus.datasource.password", DATABASE.getPassword());     ④

        return confMap;     ④
    }

    @Override
    public void stop() {
        DATABASE.close();   ⑤
    }
}

Listing 4-2src/test/com.targa.labs.quarkushop.utils.TestContainerResource

img/509649_1_En_4_Figd_HTML.gif从 Quarkus 1.13 开始,TestContainers不再需要,新特性叫做 DevServices。

DevServices 为您提供开箱即用的零配置数据库。根据您的数据库类型,您可能需要安装 Docker 才能使用该功能。很多数据库都支持 DevServices,比如 PostgreSQL,MySQL 等。

如果您想使用 DevServices,您需要做的就是包含您想要的数据库类型的相关扩展(反应式或 JDBC,或两者都有),并且不要配置数据库 URL、用户名和密码。Quarkus 将提供数据库,您可以开始编码,不用担心配置。

要了解更多关于 DevServices 的信息,请看这里: https://quarkus.io/guides/datasource#dev-services

虽然 Quarkus 默认监听端口 8080,但在运行测试时,它默认监听端口 8081。这允许您在并行运行应用的同时运行测试。这个 HTTP 测试端口可以使用quarkus.http.test-port=9999属性在application.properties中更改为 9999。

img/509649_1_En_4_Fige_HTML.gif如果把quarkus.http.test-port=8888%test.quarkus.http.test-port=9999插入application.properties会怎么样?

放轻松!这里,您处理的是test概要文件的 HTTP 端口。因此带有%test的属性将覆盖在它之前定义的任何值。当运行测试时,您将看到测试的运行时公开了 9999 端口。

您需要的最后一个配置是测量测试覆盖率,这是衡量代码质量的一个非常重要的指标。我们将使用JaCoCo来生成代码覆盖报告。

首先,您需要在pom.xml文件中添加JaCoCo Quarkus 扩展名:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jacoco</artifactId>
    <scope>test</scope>
</dependency>

不需要更多的 Maven 配置!夸库斯扩展将会完成所有的魔法!img/509649_1_En_4_Figf_HTML.gif

稍后您将使用这个JaCoCo报告来查看您的测试覆盖了多少。

编写第一个测试

让我们从测试第一个 REST API 开始:Cart REST API。img/509649_1_En_4_Figg_HTML.gif

基于CartResource或 Swagger UI,注意有六个服务:

img/509649_1_En_4_Figh_HTML.jpg

因此您将至少有六个测试,每个服务一个测试。

在开始测试之前,清单 4-3 展示了一个典型的test类在 Quarkus 世界中的样子。

  • Quarkus 中控制 JUnit 5 测试框架的注释。

  • ②使TestContainerResource对您的CartResourceTest可用的注释。

  • ③用于表示带注释的方法是一个测试。

@QuarkusTest                                        ①
@QuarkusTestResource(TestContainerResource.class)   ②
class CartResourceTest {

    @Test                                           ③
    void testSomeOperationOrFeature() {

    }
}

Listing 4-3src/test/java/com/targa/labs/quarkushop/web

我们将创建测试来验证给定所需的输入,并且在进行正确的调用时,我们将获得预期的结果。

让我们首先创建一个典型的测试:findAllCarts用于在/api/carts上使用 HTTP GET 请求列出所有购物车。这个 REST API 将返回一个CartDto数组。我们可以使用 JUnit 5 将这个用例转化为一个测试,并轻松放心:

  • ①启动一个放心测试用例,向/carts REST API 发送一个 HTTP GET 请求。

  • ②提取前一行中请求的放心ValidatableResponse

  • ③验证响应状态代码是否与200 OK匹配,响应状态代码由OK.getStatusCode()从来自放心的Response.Status枚举中返回。

  • ④验证 JSON 或 XML 响应主体元素size()符合 Hamcrest 匹配器greaterThan(0)

@Test
void testFindAll() {
    get("/carts")    ①
        .then()             ②
            .statusCode(OK.getStatusCode())    ③
            .body("size()", greaterThan(0));   ④
}

总而言之,这个测试将验证在/cart上执行 HTTP GET 将返回以下内容:

  • 包含作为状态代码的200的标题

  • 具有非空元素数组的主体

    img/509649_1_En_4_Figi_HTML.gif在之前的调用中,我们调用了/carts路径而不是/api/carts,因为/api根基础是由quarkus.http.root-path=/api属性添加的,我们之前已经将它添加到了application.properties中。

您使用相同的风格来测试findAllActiveCarts()getActiveCartForCustomer()方法、findById()deleteById():

@Test ①
void testFindAllActiveCarts() {
    get("/carts/active").then()
            .statusCode(OK.getStatusCode());
}

@Test ②
void testGetActiveCartForCustomer() {
    get("/carts/customer/3").then()
            .contentType(ContentType.JSON)
            .statusCode(OK.getStatusCode())
            .body(containsString("Peter"));
}

@Test ③
void testFindById() {
    get("/carts/3").then()
            .statusCode(OK.getStatusCode())
            .body(containsString("status"))
            .body(containsString("NEW"));

    get("/carts/100").then()
            .statusCode(NO_CONTENT.getStatusCode());
}

@Test ④
void testDelete() {
    get("/carts/active").then()
            .statusCode(OK.getStatusCode())
            .body(containsString("Jason"))
            .body(containsString("NEW"));

    delete("/carts/1").then()
            .statusCode(NO_CONTENT.getStatusCode());

    get("/carts/1").then()
            .statusCode(OK.getStatusCode())
            .body(containsString("Jason"))
            .body(containsString("CANCELED"));
}

在这些测试中,我们验证:

  • ①对/carts/active的 HTTP GET 请求的响应将200作为其状态代码。

  • ②对/carts/customer/3的 HTTP GET 请求的响应将200作为其状态代码,并且主体包含"Peter"。Peter Quinn 是 ID 为 3 的客户,他有一个活动的购物车。这个值来自我们在V1.1__Insert_samples.sql脚本中使用 Flyway 导入的样本数据。

  • ③对/carts/3的 HTTP GET 请求的响应将200作为其状态代码,并且主体包含"NEW"作为购物车状态。在/carts/100上的 HTTP GET 请求的响应将404作为它的状态代码,它的主体是空的,因为我们没有 ID 为 100 的购物车。

  • ④对于客户 Jason 拥有的 ID 为 1 的给定活动购物车,在我们于/carts/1执行 HTTP DELETE 后,购物车状态将从"NEW"变为"CANCELED "

现在我们将进入更深层次的测试用例。我们将核实一个不正确的情况。在我们的业务逻辑中,一个客户在任何时候都不能有一个以上的活动购物车。因此,我们将创建一个测试来验证当给定客户有两个活动购物车时,应用将抛出一个错误。

我们需要为 ID 为 3 的客户插入一个额外活动购物车的记录。保持冷静!img/509649_1_En_4_Figj_HTML.gif我们将在测试结束时删除该记录,以保持数据库干净。为了执行这些插入和删除 SQL 查询,我们需要从测试上下文访问数据库。为了使这种交互成为可能,我们需要在测试中获得一个数据源。Quarkus 支持这一点,它允许您通过@Inject注释将 CDI beans 注入到测试中。由于数据源是一个 CDI bean,我们可以在测试中注入它。

Quarkus 中的测试是完整的 CDI beans,因此您可以享受所有的 CDI 特性。img/509649_1_En_4_Figl_HTML.gif

测试看起来像这样:

@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
class CartResourceTest {

    private static final String INSERT_WRONG_CART_IN_DB =
     "insert into carts values (999, current_timestamp, current_timestamp, 'NEW', 3)";

    private static final String DELETE_WRONG_CART_IN_DB =
            "delete from carts where id = 999";

    @Inject
    Datasource datasource;

...

    @Test
    void testGetActiveCartForCustomerWhenThereAreTwoCartsInDB() {
        executeSql(INSERT_WRONG_CART_IN_DB);

        get("/carts/customer/3").then()
                .statusCode(INTERNAL_SERVER_ERROR.getStatusCode())
                .body(containsString(INTERNAL_SERVER_ERROR.getReasonPhrase()))
                .body(containsString("Many active carts detected !!!"));

        executeSql(DELETE_WRONG_CART_IN_DB);
    }

    private void executeSql(String query) {
        try (var connection = dataSource.getConnection()) {
            var statement = connection.createStatement();
            statement.executeUpdate(query);
        } catch (SQLException e) {
            throw new IllegalStateException("Error has occurred while trying to execute SQL Query: " + e.getMessage());
        }
    }
...
}

该测试将验证/carts/customer/3上的 HTTP GET 请求将具有以下内容:

  • 状态代码为500,表示内部服务器错误

  • Body包含"Internal Server Error"

下一个测试将是关于创建一个新的购物车。

要创建购物车,我们需要创建一个客户。然后,基于它的 ID,我们可以创建购物车。这个测试将调用客户 API 来创建客户,并调用购物车 API 来创建购物车。为了数据库的一致性,在测试结束时,我们将删除创建的记录:

  • ①您正在将请求参数打包到一个Map中,它将被放心地序列化到 JSON 中。

  • extract().jsonPath().getInt("id")用于提取响应 JSON 主体中"id"属性的值。

  • extract().jsonPath().getMap("$")用于提取所有 JSON 体并反序列化到一个Map中。

@Test
void testCreateCart() {
    var requestParams = Map.of("firstName", "Saul", "lastName", "Berenson", "email", "call.saul@mail.com"); ①

    var newCustomerId = given()
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
            .body(requestParams).post("/customers").then()
            .statusCode(OK.getStatusCode())
            .extract()          ②
            .jsonPath()         ②
            .getInt("id");      ②

    var response = post("/carts/customer/" + newCustomerId).then()
            .statusCode(OK.getStatusCode())
            .extract()          ③
            .jsonPath()         ③
            .getMap("$");       ③

    assertThat(response.get("id")).isNotNull();
    assertThat(response).containsEntry("status", CartStatus.NEW.name());

    delete("/carts/" + response.get("id")).then()
            .statusCode(NO_CONTENT.getStatusCode());

    delete("/customers/" + newCustomerId).then()
            .statusCode(NO_CONTENT.getStatusCode());
}

购物车 API 的最后一个测试是验证当客户已经有一个活动的购物车时,API 将拒绝为该客户创建另一个购物车:

@Test
void testFailCreateCartWhileHavingAlreadyActiveCart() {

    var requestParams = Map.of("firstName", "Saul", "lastName", "Berenson", "email", "call.saul@mail.com");

    var newCustomerId = given()
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
            .body(requestParams)
            .post("/customers").then()
            .statusCode(OK.getStatusCode())
            .extract()
            .jsonPath()
            .getLong("id");

    var newCartId = post("/carts/customer/" + newCustomerId).then()
            .statusCode(OK.getStatusCode())
            .extract()
            .jsonPath()
            .getLong("id");

    post("/carts/customer/" + newCustomerId).then()
            .statusCode(INTERNAL_SERVER_ERROR.getStatusCode())
            .body(containsString(INTERNAL_SERVER_ERROR.getReasonPhrase()))
            .body(containsString("There is already an active cart"));

    assertThat(newCartId).isNotNull();

    delete("/carts/" + newCartId).then()
            .statusCode(NO_CONTENT.getStatusCode());

    delete("/customers/" + newCustomerId).then()
            .statusCode(NO_CONTENT.getStatusCode());
}

在这个测试中,我们验证了除了Status Code 500和臭名昭著的Internal Server Error之外,响应体还包含消息"There is already an active cart"

img/509649_1_En_4_Figm_HTML.gif我只涉及CartResourceTest,因为它是八个测试类中最全面的测试。你会在我的img/509649_1_En_4_Fign_HTML.gif GitHub 库中找到所有代码。img/509649_1_En_4_Figo_HTML.gif

我们将使用 SonarQube 来检查测试的覆盖率,并分析代码的质量。

发现 SonarQube

SonarQube 是 SonarSource 开发的一个开源平台,用于持续检查代码质量。您可以通过对代码的静态分析来执行自动审查,以检测 20 多种编程语言上的错误、代码气味和安全漏洞。SonarQube 提供关于重复代码、编码标准、单元测试、代码覆盖率、代码复杂性、注释、错误和安全漏洞的报告。

SonarQube 可以记录度量历史并提供演化图。SonarQube 提供与 Maven、Ant、Gradle、MSBuild 和持续集成工具(Atlassian Bamboo、Jenkins、Hudson 等)的全自动分析和集成。).

您需要在您的机器上本地安装 SonarQube 或使用托管版本。例如,您可以使用 SonarCloud,在这里您可以免费分析项目。

What is SonarCloud?

SonarCloud 是领先的在线服务,用于捕获您的拉取请求和整个代码库中的错误和安全漏洞。

SonarCloud 是 SonarQube 基于云的代码质量和安全服务。SonarCloud 的主要特点是:

  • 支持 23 种语言,包括 Java、JS、C#、C/C++、Objective-C、TypeScript、Python、ABAP、PLSQL、T-SQL 等等。

  • 数以千计的规则来跟踪难以发现的错误和质量问题,这要归功于它强大的静态代码分析器。

  • 与 Travis、Azure DevOps、BitBucket、AppVeyor 等的云 CI 集成。

  • 深入的代码分析探索所有的源文件,无论是在分支中还是在拉请求中,以达到绿色质量关并促进构建。

  • 快速且可扩展。

您可以在 SonarCloud 中创建一个免费帐户。

img/509649_1_En_4_Figp_HTML.jpg

接下来,选择使用 GitHub 或 Azure DevOps 甚至 BitBucket 或 GitLab 帐户从 SonarCloud 开始。我在这种情况下使用img/509649_1_En_4_Figq_HTML.gif GitHub。

接下来,单击手动创建项目:

img/509649_1_En_4_Figr_HTML.jpg

接下来,单击在 GitHub 上选择一个组织,将您的组织导入 GitHub:

img/509649_1_En_4_Figs_HTML.jpg

接下来,选择存储项目源代码的组织:

img/509649_1_En_4_Figt_HTML.jpg

接下来,从列表中选择项目:

img/509649_1_En_4_Figu_HTML.jpg

接下来,您需要定义组织名称:

img/509649_1_En_4_Figv_HTML.jpg

您可以选择适用于所有公共存储库和项目的免费计划:

img/509649_1_En_4_Figw_HTML.jpg

现在,您可以选择想要分析的公共存储库。单击设置:

img/509649_1_En_4_Figx_HTML.jpg

您将进入项目配置屏幕旁边:

img/509649_1_En_4_Figy_HTML.jpg

选择手动分析方法,并选择 Maven 作为构建工具:

img/509649_1_En_4_Figz_HTML.jpg

您将获得定制配置,用于在 SonarCloud 上分析您的项目。有些属性需要添加到pom.xml文件中:

<properties>
  <sonar.projectKey>nebrass_quarkushop</sonar.projectKey>
  <sonar.organization>nebrass</sonar.organization>
  <sonar.host.url>https://sonarcloud.io</sonar.host.url>
</properties>

您需要用这里生成的值定义一个名为SONAR_TOKEN的环境变量。这个令牌用于向 SonarCloud 认证 SonarQube Maven 插件。

现在,项目被配置为在 SonarCloud 上进行分析,只需运行mvn verify sonar:sonar:

img/509649_1_En_4_Figaa_HTML.jpg

哇哦!只覆盖了 2.2%?img/509649_1_En_4_Figab_HTML.gif img/509649_1_En_4_Figac_HTML.gif我们以为自己做了足够强大的测试,可以测试一切,但似乎还缺了点什么。原因是,在应用中使用 Lombok 时,需要在项目根文件夹中添加一个额外的配置文件,如清单 4-4 所示。

  • ①告诉 Lombok 这是你的根目录。然后,您可以在任何子目录(通常表示项目或源包)中用不同的设置创建lombok.config文件。

  • ② Lombok 可以配置为对所有生成的节点添加@lombok.Generated注释,这对于JaCoCo(它有内置支持)或者其他样式检查器和代码覆盖工具非常有用。

config.stopBubbling = true                      ①
lombok.addLombokGeneratedAnnotation = true      ②

Listing 4-4lombok.config

通过键入mvn clean verify sonar:sonar再次运行声纳分析仪:

img/509649_1_En_4_Figad_HTML.jpg

耶!现在 Sonar 知道了 Lombok 生成的代码,分析结果也更容易接受。img/509649_1_En_4_Figaf_HTML.gif

建立和经营夸库商店

建造采石场

quartus 中的包装模式

QuarkuShop 使用 Maven 作为构建工具,所以您可以使用mvn package来构建它。该命令将构建以下内容:

  • target/目录中的quarkushop-1.0.0-SNAPSHOT.jar文件,它不是可运行的 JAR。它包含项目类和资源。

  • target/quarkus-app目录中的quarkus-run.jar文件,这是一个可运行的 JAR。但是如果没有target/quarkus-app/lib/文件夹,这个 JAR 文件就不能在任何地方执行,所有需要的库都被复制到这个文件夹中。所以如果你想分发quarkus-run.jar,你需要分发整个quarkus-app目录。

要有一个独立的 JAR 文件来打包 QuarkuShop 和所有必要的文件,可以创建一个 Fat JAR(也称为 UberJAR)。

What Are Fat or Uber Jars?

Maven(尤其是 Spring Boot)推广了这种众所周知的打包方法,它包括在标准 Java 运行时环境中运行整个应用所需的一切(也就是说,你可以用java -jar myapp.jar运行应用)。

要为 QuarkuShop 构建 UberJAR,只需键入以下命令:

mvn clean package -Dquarkus.package.type=uber-jar

如此容易,如此简单!没有要配置或添加到项目中的内容。Quarkus 天生支持这种创作。

UberJAR 的主要缺点是它不能在映像构建期间分层,这会大大增加构建时间和映像大小。

Quarkus 还支持原生模式,这是这个伟大框架中最好的和最受推崇的特性。

遇见夸特斯土著

Quarkus 通过与 GraalVM 深度集成,使得创建原生二进制文件变得非常容易。这些二进制文件也被称为本地映像。GraalVM 可以将 Java 字节码编译成本机映像,从而使应用启动更快,占用空间更小。

安装 GraalVM 时,默认情况下native-image功能不可用。要使用 GraalVM 安装本机映像,请运行以下命令:

gu install native-image

img/509649_1_En_4_Figah_HTML.gif确保配置了GRAALVM_HOME环境变量,并指向您的 GraalVM 安装目录。

要构建本地 QuarkuShop 二进制文件,请运行以下命令:

./mvnw clean package -Pnative

为了构建本机可执行文件,我们使用了pom.xml文件中的native maven 概要文件。Maven 概要文件是在项目生成时添加的:

<profiles>
    <profile>
        <id>native</id>
        <activation>
            <property><name>native</name></property>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>${surefire-plugin.version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                            <configuration>
                                <systemProperties>
                                    <native.image.path>
${project.build.directory}/${project.build.finalName}-runner
                                    </native.image.path>
                                    <java.util.logging.manager>
                                        org.jboss.logmanager.LogManager
                                    </java.util.logging.manager>
                                    <maven.home>${maven.home}</maven.home>
                                </systemProperties>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
        <properties>
            <quarkus.package.type>native</quarkus.package.type>
        </properties>
    </profile>
</profiles>

在本机模式下运行测试

注意,这里配置了两个目标:integration-testverify。这些用于执行为应用的本地版本运行的测试。您可以重用经典 JAR 中的测试,并将它们带到本机映像中。这可以通过创建从每个@QuarkusTest类继承的新 Java 类来实现。继承类需要用@NativeImageTest注释,这表明这个测试应该使用本机映像运行,而不是在 JVM 中运行。

img/509649_1_En_4_Figai_HTML.gif @NativeImageTest将对现有的本地二进制文件执行测试。本地二进制路径在 Maven 故障保护插件configuration块中定义:

<systemProperties>
    <native.image.path>
        ${project.build.directory}/${project.build.finalName}-runner
    </native.image.path>
...
</systemProperties>

要了解更多关于本地测试的信息,请参阅本指南: https://quarkus.io/guides/building-native-image#testing-the-native-executable

例如,对于CartResourceTest:

@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
class CartResourceTest {
    ...
}

您创建了将在本机映像中运行测试的CartResourceIT:

@NativeImageTest
class CartResourceIT extends CartResourceTest {
}

img/509649_1_En_4_Figaj_HTML.gif我使用了与官方 Quarkus 文档相同的命名约定。我使用后缀Test进行 JVM 集成测试,使用后缀IT进行本机映像测试。

创建本机映像测试后,尝试使用本机映像运行测试:

mvn verify -Pnative

除了从CartResourceTest继承的本地测试类之外,所有的测试都通过了。img/509649_1_En_4_Figak_HTML.gif img/509649_1_En_4_Figal_HTML.gif错误信息非常明确:

[ERROR] Errors:
[ERROR]   CartResourceIT » JUnit @Inject is not supported in NativeImageTest tests. Offe...
[INFO]
[ERROR] Tests run: 39, Failures: 0, Errors: 1, Skipped: 0
[INFO]

这是因为缺乏对注入到本机模式的支持。尽管在CartResourceTest中,您将DataSource注入到数据库交互的测试中。这在 JVM 模式下是可能的,但在本机模式下是不可能的。让我们删除CartResourceIT,因为保持它的禁用是没有用的。

img/509649_1_En_4_Figam_HTML.gif要在本机模式下禁用特定的父测试类,只需使用@DisabledOnNativeImage对该类进行注释。

现在,如果您再次运行mvn verify -Pnative命令,您将跳过禁用的测试,所有剩余的测试都将通过:

[INFO] Results:
[INFO]
[WARNING] Tests run: 39, Failures: 0, Errors: 0, Skipped: 1

打包并运行本地 QuarkuShop

使用mvn verify -Pnative命令的本地二进制编译不能被分发,也不能在其他机器上执行。本机可执行文件特定于您的操作系统,它是在那里编译的。img/509649_1_En_4_Figan_HTML.gif

保持冷静!奇妙的容器化和神话般的 Quarkus 团队为您带来了解决方案!解决方案是在 Docker 容器中构建原生二进制文件,这样它将与主机操作系统隔离开来。您可以使用以下命令来完成此操作:

$ mvn package -Pnative -Dquarkus.native.container-build=true

...
[INFO] --- quarkus-maven-plugin:1.13.3.Final:build (default) @ quarkushop ---
[INFO] [org.jboss.threads] JBoss Threads version 3.2.0.Final
[INFO] [io.quarkus.flyway.FlywayProcessor] Adding application migrations in path 'file:/home/nebrass/java/playing-with-java-microservices-monolith-example/target/quarkushop-1.0.0-SNAPSHOT.jar!/db/migration' using protocol 'jar'
[INFO] [org.hibernate.Version] HHH000412: Hibernate ORM core version 5.4.29.Final
[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building native image source jar: ...quarkushop-1.0.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Building native image from ...quarkushop-1.0.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Checking image status quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
21.0-java11: Pulling from quarkus/ubi-quarkus-native-image
57de4da701b5: Pull complete
cf0f3ebe9f53: Pull complete
6d14943d1530: Pull complete
Digest: sha256:176e619ad7cc2881477d04a2b2681fae41db08a92be06cddffd698f9c9546388
Status: Downloaded newer image for quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GraalVM Version 21.0.0.2 (Java Version 11.0.10+8-jvmci-21.0-b06)
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] docker run \
    -v /home/nebrass/java/playing-with-java-microservices-monolith-example/target/quarkushop-1.0.0-SNAPSHOT-native-image-source-jar:/project:z \
    --env LANG=C \
    --user 1000:1000 \
    --rm \
    quay.io/quarkus/ubi-quarkus-native-image:21.0-java11 \
    -J-Dsun.nio.ch.maxUpdateArraySize=100 \
    -J-DCoordinatorEnvironmentBean.transactionStatusManagerEnable=false \
    -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
    -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory \
    -J-Dvertx.disableDnsResolver=true \
    -J-Dio.netty.leakDetection.level=DISABLED \
    -J-Dio.netty.allocator.maxOrder=1 \
    -J-Duser.language=en \
    -J-Dfile.encoding=UTF-8 \
    --initialize-at-build-time= \
    -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
    -H:+JNI -jar quarkushop-1.0.0-SNAPSHOT-runner.jar \
    -H:FallbackThreshold=0 \
    -H:+ReportExceptionStackTraces \
    -H:-AddAllCharsets \
    -H:EnableURLProtocols=http,https \
    --enable-all-security-services \
    --no-server \
    -H:-UseServiceLoaderFeature \
    -H:+StackTrace quarkushop-1.0.0-SNAPSHOT-runner
[quarkushop-1.0.0-SNAPSHOT-runner:25]    classlist:  12 734,03 ms,  1,15 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        (cap):     786,61 ms,  1,15 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        setup:   2 837,21 ms,  1,15 GB
18:02:44,230 INFO  [org.hib.Version] HHH000412: Hibernate ORM core version 5.4.29.Final
18:02:44,258 INFO  [org.hib.ann.com.Version] HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
18:02:44,357 INFO  [org.hib.dia.Dialect] HHH000400: Using dialect: io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect
18:02:44,526 INFO  [org.hib.val.int.uti.Version] HV000001: Hibernate Validator 6.2.0.Final
18:03:20,036 INFO  [org.jbo.threads] JBoss Threads version 3.2.0.Final
[quarkushop-1.0.0-SNAPSHOT-runner:25]     (clinit):   2 685,63 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]   (typeflow):  53 377,69 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]    (objects):  54 520,56 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]   (features):   2 615,98 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]     analysis: 118 704,92 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]     universe:   4 451,62 ms,  3,93 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]      (parse):  21 315,61 ms,  4,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]     (inline):  11 952,68 ms,  6,25 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]    (compile):  40 647,63 ms,  6,54 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]      compile:  79 193,30 ms,  6,54 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        image:  13 638,67 ms,  6,29 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        write:   4 589,92 ms,  6,29 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]      [total]: 236 996,22 ms,  6,29 GB
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] objcopy executable not found in PATH. Debug symbols will not be separated from executable.
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] That will result in a larger native image with debug symbols embedded in it.
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 254801ms
[INFO] --------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] --------------------------------------------------------------------
[INFO] Total time:  04:42 min
[INFO] Finished at: 2021-05-01T20:06:24+02:00
[INFO] --------------------------------------------------------------------

在前面的 Maven 日志中,列出了一个很长的 Docker 命令:

docker run \
-v /home/nebrass/java/playing-with-java-microservices-monolith-example/target/quarkushop-1.0.0-SNAPSHOT-native-image-source-jar:/project:z \
--env LANG=C \
--user 1000:1000 \
--rm \
quay.io/quarkus/ubi-quarkus-native-image:21.0-java11 \
-J-Dsun.nio.ch.maxUpdateArraySize=100 \
-J-DCoordinatorEnvironmentBean.transactionStatusManagerEnable=false \
-J-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory \
-J-Dvertx.disableDnsResolver=true \
-J-Dio.netty.leakDetection.level=DISABLED \
-J-Dio.netty.allocator.maxOrder=1 \
-J-Duser.language=en \
-J-Dfile.encoding=UTF-8 \
--initialize-at-build-time= \
-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
-H:+JNI -jar quarkushop-1.0.0-SNAPSHOT-runner.jar \
-H:FallbackThreshold=0 \
-H:+ReportExceptionStackTraces \
-H:-AddAllCharsets \
-H:EnableURLProtocols=http,https \
--enable-all-security-services \
--no-server \
-H:-UseServiceLoaderFeature \
-H:+StackTrace quarkushop-1.0.0-SNAPSHOT-runner

执行这个很长的命令是为了在 Docker 容器中基于quay.io/quarkus/ubi-quarkus-native-image:21.0-java11映像构建本地可执行文件,该映像支持 GraalVM。因此,即使您没有在本地安装 GraalVM,您也可以毫无问题地构建本机可执行文件。img/509649_1_En_4_Figap_HTML.gif

img/509649_1_En_4_Figaq_HTML.gif您可以使用以下命令明确选择容器化引擎:

# To select Docker
mvn package -Pnative -Dquarkus.native.container-runtime=docker
# To select Podman

mvn package -Pnative -Dquarkus.native.container-runtime=podman

生成的可执行文件将是一个 64 位 Linux 可执行文件,您可以在 Docker 容器中运行它。当我们生成 QuarkuShop 时,我们在src/main/docker目录中得到一个默认的Dockerfile.native文件,其内容如清单 4-5 所示。

FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Listing 4-5src/main/docker/Dockerfile.native

让我们构建并运行Dockerfile.native。在运行容器之前,请确保 PostgreSQL 容器正在运行。img/509649_1_En_4_Figar_HTML.gif

$ docker build -f src/main/docker/Dockerfile.native -t nebrass/quarkushop-native .
...
Successfully built b14f563446d1
Successfully tagged nebrass/quarkushop-native:latest

$ docker run --network host --name quarkushop-native -p 8080:8080 nebrass/quarkushop-native
WARNING: Published ports are discarded when using host network mode

___  ____                       __          _____  __
_ __/ __ \ __  __ ____   _____ / /__ __  __/ ___/ / /_   ____   ____
 --/ / / // / / // __ \ / ___// //_// / / /\__ \ / __ \ / __ \ / __ \
 -/ /_/ // /_/ // /_/ // /   / ,<  / /_/ /___/ // / / // /_/ // /_/ /
--\___\_\\____/ \__,_//_/   /_/|_| \____//____//_/ /_/ \____// ,___/
/_/ Part of the #PlayingWith Series
Powered by Quarkus 1.13.3.Final
2020-08-08 13:42:37,722 INFO  [org.fly.cor.int.lic.VersionPrinter] (main) Flyway Community Edition 6.5.3 by Redgate
2020-08-08 13:42:37,725 INFO  [org.fly.cor.int.dat.DatabaseFactory] (main) Database: jdbc:postgresql://localhost:5432/demo (PostgreSQL 9.6)
2020-08-08 13:42:37,729 INFO  [org.fly.cor.int.com.DbMigrate] (main) Current version of schema "public": 1.1
2020-08-08 13:42:37,729 INFO  [org.fly.cor.int.com.DbMigrate] (main) Schema "public" is up to date. No migration necessary.
2020-08-08 13:42:37,799 INFO  [io.quarkus] (main) quarkushop 1.0.0-SNAPSHOT native (powered by Quarkus 1.13.3.Final) started in 0.085s. Listening on: http://0.0.0.0:8080
2020-08-08 13:42:37,799 INFO  [io.quarkus] (main) Profile prod activated.
...

Hakuna Matata!img/509649_1_En_4_Figas_HTML.gif一切都好!该应用正在端口 8080 上运行并可用。

img/509649_1_En_4_Figat_HTML.gif你可能会问自己,为什么会有(main) Profile prod activatedimg/509649_1_En_4_Figau_HTML.gif我们不是使用本地概要文件构建应用吗?img/509649_1_En_4_Figav_HTML.gif这里为什么会有prodimg/509649_1_En_4_Figaw_HTML.gif

这个概要文件是应用运行时概要文件:prod。如上所述,Quarkus 应用有三个预定义的概要文件:devtestprod。当我们运行打包的应用时,我们处于prod模式,尽管当我们使用mvn quarkus:dev运行源代码时,我们显然处于dev模式。

Maven 本机概要文件用于构建源代码,而prod是运行时概要文件。img/509649_1_En_4_Figax_HTML.gif

很好!我们基于 QuarkuShop 的本机二进制文件构建了这个 Docker 映像:用 Quarkus 构建的超音速亚原子 Java 二进制文件。但是夸库斯的表现如何呢?它真的比 Spring Boot 产生更好的结果吗?原生映像真的优化了启动时间吗?它减少了内存占用吗?

img/509649_1_En_4_Figay_HTML.gif Quarkus 有一个很棒的扩展叫做container-image-docker,它处理 Docker 和src/main/docker文件夹中的 Docker 文件。

JVM 和本机模式之间的差异

我们需要进行科学检验,看看 Quarkus 是否真的如此有效。img/509649_1_En_4_Figaz_HTML.gif性能是 Quarkus 的第一卖点。你会发现无数的比较报告展示了 Quarkus JVM、Quarkus Native 和许多其他框架如 Spring Boot 之间的差异。

让我们先回顾一下 Quarkus 团队制作的最著名的度量图。它将 Quarkus JVM 和原生模式与传统的云原生栈(我认为是 Spring Boot img/509649_1_En_4_Figba_HTML.gif)进行了比较:

img/509649_1_En_4_Figbb_HTML.png

img/509649_1_En_4_Figbc_HTML.gif内存 RSS(代表驻留集大小)是当前由一个进程分配和使用的物理内存量(不包括换出的页面)。它包括代码、数据和共享库(在每个使用它们的进程中都被计算在内)。

但是我们都知道这种东西纯粹是一种营销手段。我们何不自己尝试一下呢!img/509649_1_En_4_Figbe_HTML.gif

我首先考虑使用 Spring Boot、Quarkus JVM 和 Quarkus Native,用一些 Hello World REST APIs 创建一个完整的环境。在开始任务之前,我在img/509649_1_En_4_Figbf_HTML.gif GitHub 里搜索了一下,看看有没有类似的项目。幸运的是,我找到了哈拉尔德·莱因米勒 ( https://github.com/rmh78/quarkus-performance )制作的优秀实验室。该实验室为 sample REST 和 REST plus CRUD 应用进行了基准测试和指标收集,包括许多框架:

  • Payara Micro 公司

  • Spring Boot

  • quartus jvm 和本机

  • 甚至 Python,我将省略它,因为超出了范围img/509649_1_En_4_Figbg_HTML.gif

我分叉了这个项目(在 MIT 许可下)并更新了版本,因此基准测试更加有效。你可以在我的img/509649_1_En_4_Figbh_HTML.gif GitHub 仓库的 nebrass/quarkus-performance 找到这个项目。

我不会详细解释实验室做什么,但我会列出基本步骤:

  1. 所有任务都在基于 CentOs 8 的 Docker 容器中执行。

  2. 第一步是安装所有需要的软件,如 Maven、GraalVM 21.1.0 CE 和企业版、Python 和测试工具如 Jabba(类似于 NVM 的 Java 版本管理器)等。

  3. 构建 Docker 映像并运行它,在访问它的 bash 时,构建所有示例应用(所有 Java 变体和 Python)的源代码。

  4. 通过特定的运行时对每个构建的二进制文件应用基准脚本。基准测试脚本是一个基于 Apache 基准测试工具的负载测试。

  5. 为每种情况生成一个 Python MatplotLib(Python 的绘图库)图形。这些数字包含 CPU 和内存利用率指标的可视化。

What is the Difference Between GraalVM CE and EE?

GraalVM 分社区版和企业版。

GraalVM Community edition 是一款开源软件,构建自 GitHub 上可用的源代码,根据 GNU 通用公共许可证第 2 版分发,但“类路径”除外,这与 Java 的条款相同。

GraalVM 社区可以免费用于任何目的,没有任何附加条件,也没有任何保证或支持。

Oracle GraalVM Enterprise Edition 根据 GraalVM OTN 许可协议(免费用于测试、评估或开发非生产应用)或 Oracle 客户主许可协议的条款获得许可。

—来自甲骨文 www.graalvm.org/docs/why-graal/ 的官方 GraalVM 文档

基准测试结果:

|

诺誓

|

休息+污物

|
| --- | --- |
| img/509649_1_En_4_Figbi_HTML.jpg | img/509649_1_En_4_Figbj_HTML.jpg |
| img/509649_1_En_4_Figbk_HTML.jpg | img/509649_1_En_4_Figbl_HTML.jpg |

  • quartus via graalvm 本机映像
|

诺誓

|

多生一点

|
| --- | --- |
| img/509649_1_En_4_Figbm_HTML.jpg | img/509649_1_En_4_Figbn_HTML.jpg |
| img/509649_1_En_4_Figbo_HTML.jpg | img/509649_1_En_4_Figbp_HTML.jpg |

  • 通过 java 运行时的 quartus
|

诺誓

|

多生一点

|
| --- | --- |
| img/509649_1_En_4_Figbq_HTML.jpg | img/509649_1_En_4_Figbr_HTML.jpg |
| img/509649_1_En_4_Figbs_HTML.jpg | img/509649_1_En_4_Figbt_HTML.jpg |

  • Payara Micro via Java runtime
|

诺誓

|

多生一点

|
| --- | --- |
| img/509649_1_En_4_Figbu_HTML.jpg | img/509649_1_En_4_Figbv_HTML.jpg |
| img/509649_1_En_4_Figbw_HTML.jpg | img/509649_1_En_4_Figbx_HTML.jpg |

  • 通过 Java 运行时的 Spring Boot

对于 QuarkuShop 这样的 REST plus CRUD 应用,您的第一个请求是:

  • Quarkus 原生:响应时间 0.054s,内存 RSS 48MB。

  • Quarkus JVM: 响应时间 1.622s,内存 RSS 413MB。Quarkus 土著速度快 30 倍,重量轻 8 倍。

  • Spring Boot: 响应时间 5.925s,内存 RSS 468MB。Quarkus native 快 109 倍,轻 9 倍。

  • Payara 微:响应时间 6.723s,内存 RSS 607 MB。Quarkus native 快 124 倍,轻 12 倍。

你可以看到在性能上确实有巨大的差异;夸库斯土著是冠军!img/509649_1_En_4_Figby_HTML.gif img/509649_1_En_4_Figbz_HTML.gif

还要注意 Quarkus JVM 和 Spring Boot 的区别;夸库斯比 Spring Boot 快。img/509649_1_En_4_Figca_HTML.gif img/509649_1_En_4_Figcb_HTML.gif

GraalVM 强大功能背后的魔力

在 Java VM 中运行应用会带来启动和内存占用成本。

图像生成过程使用静态分析来查找任何可从main() Java 方法获得的代码,然后执行完全提前(AOT)编译。

这种强大的组合正在创造奇迹!超音速亚原子 Java 故事在这里制作!img/509649_1_En_4_Figcc_HTML.gif

结论

QuarkuShop 已经可以测试、构建和发布了。您可以享受 GraalVM 的强大功能来生成强大且速度极快的本机二进制文件。

因为我们使用 Maven 作为构建工具,所以可以使用 CI/CD 管道(例如通过 Jenkins 或 Azure DevOps)轻松地构建和部署这个应用。

五、构建和部署单体应用

介绍

在 QuarkuShop 中,我们使用 Maven 作为构建工具。可以使用 CI/CD 管道(例如,通过 Jenkins 或 Azure DevOps)轻松构建该应用并将其部署到生产环境中。

将项目导入 Azure DevOps

首先,你需要在 Azure DevOps 中创建一个项目。好吧,但是什么是 Azure DevOps?img/509649_1_En_5_Figa_HTML.gif

Azure DevOps 是来自img/509649_1_En_5_Figb_HTML.gif微软的软件即服务(SaaS)产品,为软件团队提供了许多出色的功能。这些特性涵盖了典型应用的生命周期:

  • Azure pipelines :可以与任何语言、平台和云(不仅仅是 Azure img/509649_1_En_5_Figc_HTML.gif)一起工作的 CI/CD。

  • Azure boards :强大的工作跟踪功能,包括看板、积压订单、团队仪表盘和定制报告。

  • Azure 工件:来自公共和私有来源的 Maven、npm 和 NuGet 包提要。

  • Azure Repos :为您的项目提供无限的云托管私有 Git repos。协作拉取请求、高级文件管理等等。

  • Azure 测试计划:一体化的计划和探索性测试解决方案。

对于 Java 开发人员来说,Azure DevOps(也称为 ADO)是 Jenkins/Hudson 或 GitHub 操作的一个很好的替代品。本章展示了如何轻松地为这个项目创建一个完整的 CI/CD 管道。

首先,进入 Azure DevOps 门户,点击开始免费创建一个帐户:

img/509649_1_En_5_Fige_HTML.jpg

接下来,验证您的 Outlook/Hotmail/Live 帐户(或创建一个新帐户img/509649_1_En_5_Figf_HTML.gif),然后确认注册:

img/509649_1_En_5_Figg_HTML.jpg

接下来,您需要创建一个 Azure DevOps 组织:

img/509649_1_En_5_Figh_HTML.jpg

接下来,创建您的第一个 Azure DevOps 项目:

img/509649_1_En_5_Figi_HTML.jpg

接下来,转到回购➤文件:

img/509649_1_En_5_Figj_HTML.jpg

在这里,您会对从命令行推送现有存储库感兴趣。

ADO 生成将本地项目推向上游所需的git命令。在运行这些命令之前,让本地项目成为支持 git 的项目。

为此,只需运行这些命令,这将启动并添加所有文件,然后进行第一次提交:

git init
git add .
git commit -m "first commit"

接下来,您应该运行将整个项目推送到 ADO 的git命令:

1 git remote add origin https://nebrass-lamouchi@dev.azure.com/nebrass-lamouchi/quarkushop-monolithic-application/_git/quarkushop-monolithic-application
2 git push -u origin --all

源代码现在存放在 ADO 存储库中。现在您将创建 CI/CD 管道。img/509649_1_En_5_Figk_HTML.gif

创建 CI/CD 管道

下一步是配置持续集成管道,它将在每次主开发分支(在我们的例子中是master分支)上有新代码时运行。

创建持续集成管道

要创建第一个 CI 管道,请转到“管道”部分,然后单击“创建管道”:

img/509649_1_En_5_Figl_HTML.jpg

接下来,选择源代码的存储位置。点按“使用经典编辑器:

img/509649_1_En_5_Figm_HTML.jpg

接下来,选择 AzureRepos Git 并选择您刚刚创建的 QuarkuShop 存储库:

img/509649_1_En_5_Fign_HTML.jpg

将出现管道配置屏幕。选择 Maven 管道:

img/509649_1_En_5_Figo_HTML.jpg

然后插入最重要的部分,使用定义 YAML 文件定义管道配置。你有两个选择:

  • 直接在 ADO 中构建基于 Maven 的 Java 项目

  • 使用 Docker 多级构建来构建您的项目

创建基于 Maven 的 CI 管道

对于用 ADO 构建的 Maven 项目来说,这是最常见的情况。这也是 Maven 构建最简单的选择。见清单 5-1 。

不幸的是,这种方法对环境有很强的依赖性。例如,您需要安装 JDK 11 和 Maven 3.6.2+。如果在主机中找不到这些依赖项之一,构建将会失败。

使用这种方法时,您需要根据您的具体需求和工具采用 CI 平台。

  • ①当master分支上有更新时,该流水线将被触发。

  • ②使用 Ubuntu 的最新镜像。写这本书的时候是 20.04。

  • ③CI 场景中的第一个任务是运行所有的测试并生成 Sonar 报告。为 Java 11 定义运行时,并为 Maven 任务分配 3GB 内存。

  • ④将目标文件夹的内容复制到预定义的$(Build.ArtifactStagingDirectory)

  • ⑤最后,将$(Build.ArtifactStagingDirectory)的内容作为 Azure DevOps 神器上传。

trigger:                                                ①
- master

pool:
  vmImage: 'ubuntu-latest'                              ②

steps:
- task: Maven@3
  displayName: 'Maven verify & Sonar Analysis'          ③
  inputs:
    mavenPomFile: 'pom.xml'
    mavenOptions: '-Xmx3072m'
    javaHomeOption: 'JDKVersion'
    jdkVersionOption: '1.11'
    jdkArchitectureOption: 'x64'
    publishJUnitResults: true
    testResultsFiles: '**/surefire-reports/TEST-*.xml'
    goals: 'verify sonar:sonar'

- task: CopyFiles@2
  displayName: 'CopyFiles for Target'                   ④
  inputs:
    SourceFolder: 'target'
    Contents: '*.*'
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact: drop'                 ⑤
  inputs:
    pathtoPublish: '$(Build.ArtifactStagingDirectory)'
    artifactName: drop

Listing 5-1azure-pipelines.yml

创建基于 Docker 多级的 CI 管道

这是构建项目的时髦方式。这是构建具有非常具体需求的项目的最佳选择。与 QuarkuShop 应用一样,您需要安装 JDK 11 和 GraalVM 以及 Maven 3.6.2+。即使在 ADO 中也无法满足这一点,因为 Azure pipelines 中没有 GraalVM(至少到目前为止,在我写这些文字的时候)。

幸运的是,这种构建方法不依赖于环境。每个必需的组件将在不同的 Dockerfile 阶段进行安装和配置。

使用这种方法时,您可以将您特定需要的工具带到 CI 平台。

让我们看看Dockerfile.multistage文件的阶段 1,如清单 5-2 所示。

## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven:20.1.0-java11 AS build

Listing 5-2src/main/docker/Dockerfile.multistage

在构建阶段,我们使用的是centos-quarkus-maven:20.1.0-java11 Docker 基础映像,这是 Java 11 附带的 GraalVM、Maven、Podman 和 Buildah。这些正是我们需要的工具和版本。

你可能注意到了,我正试图说服你采用这种方法。首先,这是唯一可能的方法,因为 ADO 没有 GraalVM,而且这是避免任何意外问题的最合适的方法。img/509649_1_En_5_Figs_HTML.gif

Docker 多级 CI 管道的azure-pipelines.yml文件如清单 5-3 所示。

  • ①使用第一个Docker@2任务:

    • 基于Dockerfile.multistage文件构建 Docker 映像。

    • 将构建的图像命名为nebrass/quarkushop-monolithic-application

    • $(Build.BuildId)latest标签标记构建的图像,?? 是一个 ADO 构建变量。

  • ②使用第二个Docker@2任务将nebrass/quarkushop-monolithic-application图像推送到适当的 Docker Hub 帐户。

  • ③使用名为nebrass@DockerHub的 Azure 服务连接,它存储适当的 Docker Hub 凭证。

    img/509649_1_En_5_Figt_HTML.gifDocker@2中的@定义了 ADO 中 Docker 任务的选定版本。

    img/509649_1_En_5_Figu_HTML.gif要了解更多关于创建到 SonarCloud 的 Azure 服务连接的信息,请在 https://www.azuredevopslabs.com/labs/vstsextend/sonarcloud/ 查看这个优秀的 Azure DevOps 实验室教程

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: Maven@3
  displayName: 'Maven verify & Sonar Analysis'
  inputs:
    mavenPomFile: 'pom.xml'
    mavenOptions: '-Xmx3072m'
    javaHomeOption: 'JDKVersion'
    jdkVersionOption: '1.11'
    jdkArchitectureOption: 'x64'
    publishJUnitResults: true
    testResultsFiles: '**/surefire-reports/TEST-*.xml'
    goals: 'verify sonar:sonar'

- task: Docker@2
  displayName: 'Docker Multistage Build'                      ①
  inputs:
    containerRegistry: 'nebrass@DockerHub'                    ③
    repository: 'nebrass/quarkushop-monolithic-application'
    command: 'build'
    Dockerfile: '**/Dockerfile.multistage'
    buildContext: '.'
    tags: |
      $(Build.BuildId)
      latest

- task: Docker@2
  displayName: 'Push Image to DockerHub'                      ②
  inputs:
    containerRegistry: 'nebrass@DockerHub'                    ③
    repository: 'nebrass/quarkushop-monolithic-application'
    command: 'push'

Listing 5-3azure-pipelines.yml

不要惊讶我没有删除 Maven verify & Sonar 分析任务。不幸的是,我们用于这些集成测试的Testcontainers库不能从 Docker 上下文中调用。这就是为什么我决定使用 Maven 命令运行测试,然后完成 Docker 容器中的所有步骤。

这个 CI 管道缺少一个需求:Maven 将用来向 SonarCloud 认证以发布管道中生成的分析报告的SONAR_TOKEN环境变量。

要在 Azure pipeline 中定义环境变量,请转到 Pipelines ➤选择您的 Pipeline ➤编辑➤变量➤新变量,然后将环境变量定义为SONAR_TOKEN,并给它一个创建项目时获得的 SonarCloud 令牌。

img/509649_1_En_5_Figv_HTML.jpg

img/509649_1_En_5_Figw_HTML.gif如果没有SONAR_TOKEN环境变量,sonar:sonar将失败,并显示错误消息:java.lang.IllegalStateException: You're not authorized to run analysis. Please contact the project administrator

在这一级别,CI 管道已经准备就绪。您现在需要开始查看持续部署管道。

制作持续部署管道

对于部署部分,Docker 容器可以部署到许多产品和位置:

  • Kubernetes 集群:我们还没有到达那里

  • 托管 Docker 托管槽:Azure 容器实例、亚马逊容器服务、Docker Enterprise 等。

  • 虚拟计算机

在这种情况下,我们将使用 Azure VM 来部署 Docker 容器。您可以按照相同的步骤制作相同的 CD 管道。

创建虚拟机

第一步是创建 Azure 资源组,这是一个保存 Azure 资源的逻辑组,就像您想要创建的虚拟机一样。

img/509649_1_En_5_Figy_HTML.jpg

接下来,通过定义以下内容来创建虚拟机:

  • 虚拟机名称 : quarkushop-vm

  • 地区:法国中部

  • 影像:Ubuntu server 18.04 lt

  • 大小:标准 _B2ms 2 个 vCPUS 加 8GB 内存

img/509649_1_En_5_Figz_HTML.jpg

接下来,您需要定义以下内容:

  • 认证类型 : Password

  • 用户名 : nebrass

  • 密码:创建并确认——我不会给你我的img/509649_1_En_5_Figaa_HTML.gif

  • 确保将 SSH 端口设置为allowed

img/509649_1_En_5_Figab_HTML.jpg

通过单击“查看+创建”来确认创建。然后在验证屏幕中,单击创建。

img/509649_1_En_5_Figac_HTML.jpg

通过单击转到资源检查创建的虚拟机:

img/509649_1_En_5_Figad_HTML.png

接下来,您需要在 VM 网络安全规则中为 8080 端口(QuarkuShop HTTP 端口)的传入访问添加一个例外。为此,请转到 Azure 虚拟机的网络部分,并选择添加入站端口规则:

img/509649_1_En_5_Figae_HTML.jpg

您已经有了创建的虚拟机的 IP 地址。要访问它,只需从终端打开一个 SSH 会话。您可以使用定义的凭据来访问虚拟机实例:

$ ssh nebrass@51.103.26.144
The authenticity of host '51.103.26.144 (51.103.26.144)' can't be established.
ECDSA key fingerprint is SHA256:bioO7HNjtKKgy8g7EAgfR+82Pz4gFyEml0QyMjpLNVk.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '51.103.26.144' (ECDSA) to the list of known hosts.
nebrass@51.103.26.144's password:
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 5.3.0-1034-azure x86_64)
...
nebrass@quarkushop-vm:~$

从更新虚拟机开始:

sudo apt update && sudo apt upgrade

安装 Docker 引擎:

$ sudo apt-get install apt-transport-https \
    ca-certificates curl gnupg-agent \
    software-properties-common

添加官方码头工人 GPG 键:

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

使用以下命令设置稳定的存储库:

$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

现在,是时候安装(最后img/509649_1_En_5_Figaf_HTML.gif)Docker 引擎了:

$ sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io

下一步,安装坞站组成:

$ sudo curl -L \
  "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" \
    -o /usr/local/bin/docker-compose

对二进制文件应用可执行权限:

$ sudo chmod +x /usr/local/bin/docker-compose

然后,在/opt/文件夹中创建docker-compose.yml文件:

version: '3'
services:
  quarkushop:
    image: nebrass/quarkushop-monolithic-application:latest
    environment:
      - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/prod
    ports:
      - 8080:8080
  postgresql-db:
    image: postgres:13
    volumes:
      - /opt/postgres-volume:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=developer
      - POSTGRES_PASSWORD=p4SSW0rd
      - POSTGRES_DB=prod
      - POSTGRES_HOST_AUTH_METHOD=trust
    ports:
      - 5432:5432

接下来,您需要在 Azure VM 中创建一个本地文件夹,用作 PostgreSQL 的 Docker 卷:

$ sudo mkdir /opt/postgres-volume
$ sudo chmod 777 /opt/postgres-volume

Azure VM 现在可以用作您的生产运行时了。img/509649_1_En_5_Figag_HTML.gif让我们转向 CD 渠道。

创建持续部署管道

回到 Azure DevOps,然后转到 Pipelines ➤发布:

img/509649_1_En_5_Figah_HTML.jpg

接下来,单击新建管道,然后单击空作业:

img/509649_1_En_5_Figai_HTML.jpg

添加一个工件。选择“生成”作为源类型,并从列表中选择适当的项目和源:

img/509649_1_En_5_Figaj_HTML.jpg

接下来,单击可用的阶段 1,然后单击代理作业。将代理规格更改为ubuntu-20.04

向管道添加任务:

img/509649_1_En_5_Figak_HTML.jpg

单击添加的任务,然后单击 SSH 服务连接附近的管理以访问服务连接管理器:

img/509649_1_En_5_Figal_HTML.jpg

接下来,单击新建服务连接。搜索 SSH 并选择它:

img/509649_1_En_5_Figam_HTML.jpg

在下一个屏幕中,您需要配置对 Azure VM 实例的访问:

img/509649_1_En_5_Figan_HTML.jpg

返回到发布管道屏幕,单击 SSH 服务连接的刷新按钮。选择创建的服务连接。

接下来,给Commands添加这些docker-compose指令:

docker login -u $(docker.username) -p $(docker.password)
docker-compose -f /opt/docker-compose.yml stop
docker-compose -f /opt/docker-compose.yml pull
docker-compose -f /opt/docker-compose.yml rm quarkushop
docker-compose -f /opt/docker-compose.yml up -d

这些命令将停止所有 Docker 合成服务,提取最新的图像,删除 QuarkuShop 服务,然后重新创建它。

img/509649_1_En_5_Figao_HTML.gif$(docker.username)``$(docker.password)是我的 Docker Hub 凭证。我们将它们定义为环境变量。

然后单击保存。

img/509649_1_En_5_Figap_HTML.jpg

转到变量以定义环境变量:

img/509649_1_En_5_Figaq_HTML.jpg

最后一步是激活触发器:

img/509649_1_En_5_Figar_HTML.jpg

最后,保存修改并通过点击img/509649_1_En_5_Figas_HTML.gif创建发布来触发发布:

img/509649_1_En_5_Figat_HTML.jpg

耶!!当释放管道执行完成时,只需在浏览器中打开 URL:IP_ADDRESS:8080

例如,在我的例子中,可以在 51.103.26.144:8080 到达 QuarkuShop。您将得到默认响应:

img/509649_1_En_5_Figau_HTML.jpg

最后,您可以访问预定义的 Quarkus index.html页面:

img/509649_1_En_5_Figav_HTML.jpg

在 QuarkuShop 生产环境中,您还可以通过 51.103.26.144:8080/api/swagger-ui/ 访问 Swagger UI。

img/509649_1_En_5_Figaw_HTML.jpg

img/509649_1_En_5_Figax_HTML.gifSwagger UI 可用于dev/test环境,出于安全考虑,需要在生产环境中禁用它。

很好!img/509649_1_En_5_Figay_HTML.gif现在让我们更改此索引页面中列出的版本,并推送修改,以查看 CI/CD 管道是否如预期那样工作:img/509649_1_En_5_Figaz_HTML.gif

img/509649_1_En_5_Figba_HTML.jpg

耶!这家 CI/CD 工厂运转得非常好!img/509649_1_En_5_Figbb_HTML.gif恭喜你!img/509649_1_En_5_Figbc_HTML.gif

结论

QuarkuShop 现在有一个专门的 CI/CD 渠道:

  • 持续集成管道:在master分支上的每次提交时,管道将运行所有测试并构建本机二进制文件,该文件将被打包在 Docker 映像中。

  • 持续部署管道:当 CI 成功时,Docker 镜像将被部署到 Azure VM。img/509649_1_En_5_Figbd_HTML.gif

在下一章中,您将实现更多的层:

  • 安全:防止未经认证的访客访问应用。

  • 监控:确保应用正确运行,避免任何不良意外。

为什么要等待灾难的发生? img/509649_1_En_5_Figbe_HTML.gif**

六、添加防灾难层

介绍

编写代码,运行单元和集成测试,进行代码质量分析,创建 CI/CD 管道——许多开发人员认为旅程到此结束,新的迭代将重新开始。我们忘记了应用运行时。我不是指在哪里执行这个应用,我们已经说过我们将在 Docker 容器中运行这个应用。我说的是应用将如何运行:

  • 用户会如何使用 QuarkuShop?

  • 如何控制用户对应用的访问?

  • 我们能处理未经授权的访问吗?我们知道哪些该承认,哪些该拒绝吗?

  • 如何测量和跟踪 CPU 和内存资源的消耗?

  • 如果应用耗尽资源会发生什么?

关于运行时还有更多问题要问。这些问题揭示了 QuarkuShop 中缺失的两层:

  • 安全层:所有的认证和授权部分。

  • 监控层:所有的度量,即测量和跟踪组件。

实现安全层

保安!对于开发人员来说,这是最令人痛苦的话题之一,但它可能是任何企业应用中最关键的主题。安全性在 IT 界一直是一个非常具有挑战性的话题:技术和框架在不断发展,黑客也在不断发展。img/509649_1_En_6_Figb_HTML.gif

对于这个 QuarkuShop,我们将使用专用的 Quarkus 组件以及推荐的实践和设计选择。本章讨论如何实现一个典型的认证和授权引擎。

img/509649_1_En_6_Figc_HTML.gif我将认证和授权过程称为auth 2

分析安全要求和需求

在编写任何代码之前,我们从创建设计开始,例如使用 UML 图。安全层也是如此;我们需要在实现代码之前创建设计。但是哪个设计呢?代码在那里。我们将设计什么?

QuarkuShop 的全部功能已经实现,但还有很多需要设计。

我喜欢把建筑软件比作盖房子。到目前为止,我们所做的是:

  • 建了房子,这和写源代码是一样的。

  • 验证建筑与计划的一致性,这与编写测试相同。

  • 将房子连接到电、水和下水道网络,这与配置数据库、SonarCloud 等的访问权限是一样的。

  • 得到家具并装饰房子,这与创建 CI/CD 管道是一样的。

房子现在准备好了,业主希望有一个安全系统。我们从检查窗户和门开始,以定位可能的入口,这就是我们放置锁的地方。只有钥匙持有人可以进入,视人而定,主人会分配钥匙。例如,只有司机才有车库钥匙。园丁将有两把钥匙:一把开外门,一把开存放工具的花园小屋。住在房子里的家庭成员将毫无例外地拥有所有的钥匙。

我们也将有摄像头和传感器来监控和审计进入房子。当我们怀疑有人非法进入房子,我们可以检查摄像头,看看发生了什么。

这个家庭安全系统部署过程在某种程度上与添加应用的安全层是一样的。我们遵循相同的基本步骤:

  1. 我们分析并定位应用的所有访问点。这个过程叫做攻击面分析。

攻击面分析帮你:

  • 确定系统的哪些功能和部分需要检查/测试安全漏洞。

  • 识别需要纵深防御保护的高风险代码区域;你需要保护系统的哪些部分。

  • 确定何时改变了攻击面,何时需要进行某种威胁评估。

—OWASP 小抄系列 https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html

  1. 我们会在这些入口上锁。这些锁是认证过程的一部分。

认证是验证个人、实体或网站是否是其所声称的人的过程。web 应用上下文中的身份验证通常通过提交用户名或 ID 以及一项或多项只有给定用户才应该知道的私有信息来执行。

—OWASP 小抄系列 https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html

  1. 我们将定义一个访问控制机制,以确保只有被允许的人才能访问给定的“门”。这个过程叫做授权

授权是访问特定资源的请求应该被准许或拒绝的过程。应该注意的是,授权并不等同于认证——因为这些术语及其定义经常被混淆。身份验证是提供和验证身份。授权包括确定用户(或主体)可以访问哪些功能和数据的执行规则,确保在身份验证成功后正确分配访问权限。

—OWASP 小抄系列 https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html

QuarkuShop 是一个 Java 企业应用,它公开了 REST APIs,这是与应用用户的唯一通信渠道。

QuarkuShop 的用户可分为三类:

  • 访客或匿名:未经认证的客户

  • 用户:经过认证的客户

  • Admin :应用超级用户

下一步是定义允许哪个用户类别访问每个 REST API 服务。这可以使用授权矩阵来完成。

为 REST APIs 定义授权矩阵

**Cart REST API的授权矩阵

*
|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 获取所有购物车 | img/509649_1_En_6_Figd_HTML.gif | img/509649_1_En_6_Fige_HTML.gif | img/509649_1_En_6_Figf_HTML.gif |
| 获取活动购物车 | img/509649_1_En_6_Figg_HTML.gif | img/509649_1_En_6_Figh_HTML.gif | img/509649_1_En_6_Figi_HTML.gif |
| 按客户 ID 获取购物车 | img/509649_1_En_6_Figj_HTML.gif | img/509649_1_En_6_Figk_HTML.gif | img/509649_1_En_6_Figl_HTML.gif |
| 为给定客户创建新购物车 | img/509649_1_En_6_Figm_HTML.gif | img/509649_1_En_6_Fign_HTML.gif | img/509649_1_En_6_Figo_HTML.gif |
| 按 ID 获取购物车 | img/509649_1_En_6_Figp_HTML.gif | img/509649_1_En_6_Figq_HTML.gif | img/509649_1_En_6_Figr_HTML.gif |
| 按 ID 删除购物车 | img/509649_1_En_6_Figs_HTML.gif | img/509649_1_En_6_Figt_HTML.gif | img/509649_1_En_6_Figu_HTML.gif |

类别 REST API 的授权矩阵

*
|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 列出所有类别 | img/509649_1_En_6_Figv_HTML.gif | img/509649_1_En_6_Figw_HTML.gif | img/509649_1_En_6_Figx_HTML.gif |
| 创建新类别 | img/509649_1_En_6_Figy_HTML.gif | img/509649_1_En_6_Figz_HTML.gif | img/509649_1_En_6_Figaa_HTML.gif |
| 按 ID 获取类别 | img/509649_1_En_6_Figab_HTML.gif | img/509649_1_En_6_Figac_HTML.gif | img/509649_1_En_6_Figad_HTML.gif |
| 按 ID 删除类别 | img/509649_1_En_6_Figae_HTML.gif | img/509649_1_En_6_Figaf_HTML.gif | img/509649_1_En_6_Figag_HTML.gif |
| 按类别 ID 获取产品 | img/509649_1_En_6_Figah_HTML.gif | img/509649_1_En_6_Figai_HTML.gif | img/509649_1_En_6_Figaj_HTML.gif |

客户休息 API 的授权矩阵

*
|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 获取所有客户 | img/509649_1_En_6_Figak_HTML.gif | img/509649_1_En_6_Figal_HTML.gif | img/509649_1_En_6_Figam_HTML.gif |
| 创建新客户 | img/509649_1_En_6_Figan_HTML.gif | img/509649_1_En_6_Figao_HTML.gif | img/509649_1_En_6_Figap_HTML.gif |
| 获得活跃客户 | img/509649_1_En_6_Figaq_HTML.gif | img/509649_1_En_6_Figar_HTML.gif | img/509649_1_En_6_Figas_HTML.gif |
| 获得不活跃的客户 | img/509649_1_En_6_Figat_HTML.gif | img/509649_1_En_6_Figau_HTML.gif | img/509649_1_En_6_Figav_HTML.gif |
| 按 ID 获取客户 | img/509649_1_En_6_Figaw_HTML.gif | img/509649_1_En_6_Figax_HTML.gif | img/509649_1_En_6_Figay_HTML.gif |
| 按 ID 删除客户 | img/509649_1_En_6_Figaz_HTML.gif | img/509649_1_En_6_Figba_HTML.gif | img/509649_1_En_6_Figbb_HTML.gif |

授权矩阵为 订单 REST API

|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 获取所有订单 | img/509649_1_En_6_Figbc_HTML.gif | img/509649_1_En_6_Figbd_HTML.gif | img/509649_1_En_6_Figbe_HTML.gif |
| 创建新订单 | img/509649_1_En_6_Figbf_HTML.gif | img/509649_1_En_6_Figbg_HTML.gif | img/509649_1_En_6_Figbh_HTML.gif |
| 按客户 ID 获取订单 | img/509649_1_En_6_Figbi_HTML.gif | img/509649_1_En_6_Figbj_HTML.gif | img/509649_1_En_6_Figbk_HTML.gif |
| 检查是否有给定 ID 的订单 | img/509649_1_En_6_Figbl_HTML.gif | img/509649_1_En_6_Figbm_HTML.gif | img/509649_1_En_6_Figbn_HTML.gif |
| 按 ID 获取订单 | img/509649_1_En_6_Figbo_HTML.gif | img/509649_1_En_6_Figbp_HTML.gif | img/509649_1_En_6_Figbq_HTML.gif |
| 按 ID 删除订单 | img/509649_1_En_6_Figbr_HTML.gif | img/509649_1_En_6_Figbs_HTML.gif | img/509649_1_En_6_Figbt_HTML.gif |

订单授权矩阵-项目 REST API

*
|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 创建新的订单项目 | img/509649_1_En_6_Figbu_HTML.gif | img/509649_1_En_6_Figbv_HTML.gif | img/509649_1_En_6_Figbw_HTML.gif |
| 按订单 ID 获取订单项目 | img/509649_1_En_6_Figbx_HTML.gif | img/509649_1_En_6_Figby_HTML.gif | img/509649_1_En_6_Figbz_HTML.gif |
| 按 ID 获取订单项目 | img/509649_1_En_6_Figca_HTML.gif | img/509649_1_En_6_Figcb_HTML.gif | img/509649_1_En_6_Figcc_HTML.gif |
| 按 ID 删除订单项目 | img/509649_1_En_6_Figcd_HTML.gif | img/509649_1_En_6_Figce_HTML.gif | img/509649_1_En_6_Figcf_HTML.gif |

授权矩阵为 支付 休息 API

|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 获得所有付款 | img/509649_1_En_6_Figcg_HTML.gif | img/509649_1_En_6_Figch_HTML.gif | img/509649_1_En_6_Figci_HTML.gif |
| 创建新的付款 | img/509649_1_En_6_Figcj_HTML.gif | img/509649_1_En_6_Figck_HTML.gif | img/509649_1_En_6_Figcl_HTML.gif |
| 获得低于或等于限额的付款 | img/509649_1_En_6_Figcm_HTML.gif | img/509649_1_En_6_Figcn_HTML.gif | img/509649_1_En_6_Figco_HTML.gif |
| 通过 ID 获得付款 | img/509649_1_En_6_Figcp_HTML.gif | img/509649_1_En_6_Figcq_HTML.gif | img/509649_1_En_6_Figcr_HTML.gif |
| 按 ID 删除付款 | img/509649_1_En_6_Figcs_HTML.gif | img/509649_1_En_6_Figct_HTML.gif | img/509649_1_En_6_Figcu_HTML.gif |

产品 REST API 的授权矩阵

*
|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 获取所有产品 | img/509649_1_En_6_Figcv_HTML.gif | img/509649_1_En_6_Figcw_HTML.gif | img/509649_1_En_6_Figcx_HTML.gif |
| 创造新产品 | img/509649_1_En_6_Figcy_HTML.gif | img/509649_1_En_6_Figcz_HTML.gif | img/509649_1_En_6_Figda_HTML.gif |
| 按类别 ID 获取产品 | img/509649_1_En_6_Figdb_HTML.gif | img/509649_1_En_6_Figdc_HTML.gif | img/509649_1_En_6_Figdd_HTML.gif |
| 清点所有产品 | img/509649_1_En_6_Figde_HTML.gif | img/509649_1_En_6_Figdf_HTML.gif | img/509649_1_En_6_Figdg_HTML.gif |
| 按类别 ID 计算产品数量 | img/509649_1_En_6_Figdh_HTML.gif | img/509649_1_En_6_Figdi_HTML.gif | img/509649_1_En_6_Figdj_HTML.gif |
| 通过 ID 获取产品 | img/509649_1_En_6_Figdk_HTML.gif | img/509649_1_En_6_Figdl_HTML.gif | img/509649_1_En_6_Figdm_HTML.gif |
| 按 ID 删除产品 | img/509649_1_En_6_Figdn_HTML.gif | img/509649_1_En_6_Figdo_HTML.gif | img/509649_1_En_6_Figdp_HTML.gif |

授权矩阵为 审核 REST API

|

操作

|

匿名的

|

用户

|

管理

|
| --- | --- | --- | --- |
| 按产品 ID 获取评论 | img/509649_1_En_6_Figdq_HTML.gif | img/509649_1_En_6_Figdr_HTML.gif | img/509649_1_En_6_Figds_HTML.gif |
| 按产品 ID 创建新评论 | img/509649_1_En_6_Figdt_HTML.gif | img/509649_1_En_6_Figdu_HTML.gif | img/509649_1_En_6_Figdv_HTML.gif |
| 按 ID 获取评论 | img/509649_1_En_6_Figdw_HTML.gif | img/509649_1_En_6_Figdx_HTML.gif | img/509649_1_En_6_Figdy_HTML.gif |
| 按 ID 删除评论 | img/509649_1_En_6_Figdz_HTML.gif | img/509649_1_En_6_Figea_HTML.gif | img/509649_1_En_6_Figeb_HTML.gif |

实现安全层

我们将使用专用的身份存储来处理 QuarkuShop 用户的凭证。我们使用 Keycloak 来实现这个目的。

What is Keycloak?

Keycloak 是一个开源的身份和访问管理(IAM)解决方案,面向现代应用和服务。它使保护应用和服务变得很容易,只需要很少的代码,甚至不需要代码。

img/509649_1_En_6_Figec_HTML.png

用户使用 Keycloak 进行身份验证,而不是使用单独的应用。这意味着您的应用不必处理登录表单、认证用户和存储用户。一旦登录到 Keycloak,用户无需再次登录即可访问不同的应用。这也适用于注销。

Keycloak 提供单点注销,这意味着用户只需注销一次,就可以从所有使用 Keycloak 的应用中注销。

Keycloak 基于标准协议,并提供对 OpenID Connect、OAuth 2.0 和 SAML 的支持。

如果基于角色的授权不能满足您的需求,Keycloak 还提供了细粒度的授权服务。这允许您从 Keycloak 管理控制台管理所有服务的权限,并赋予您准确定义所需策略的权力。

我们将分四步实施这一安全策略:

  1. 准备和配置 Keycloak。

  2. 在 QuarkuShop 中实现 auth 2 Java 组件。

  3. 更新集成测试以支持授权 2

  4. 向我们的生产环境添加 Keycloak。

准备和配置钥匙锁

安全性实现的第一步是拥有一个 Keycloak 实例。本节逐步讨论如何创建和配置 Keycloak。

img/509649_1_En_6_Figed_HTML.gif很多教程里都有准备好的配置,可以导入到 Keycloak 里轻松上手。你不能在这里这样做。您将逐步执行所有需要的配置。这是最好的学习方法:边做边学。

首先在 Docker 容器中创建 Keycloak 实例:

  • ①默认情况下,没有创建admin用户,因此您将无法登录到admin控制台。要创建一个admin账户,您需要使用环境变量来传递初始用户名和密码。

  • ②使用H2作为 Keycloak 数据库。

  • ③列出暴露的端口。

  • ④这是基于 Keycloak 11.0.0。

docker run -d --name docker-keycloak \
          -e KEYCLOAK_USER=admin \        ①
          -e KEYCLOAK_PASSWORD=admin \    ①
          -e DB_VENDOR=h2 \               ②
          -p 9080:8080 \                  ③
          -p 8443:8443 \                  ③
          -p 9990:9990 \                  ③
          jboss/keycloak:11.0.0           ④

打开http://localhost:9080进入欢迎页面:

img/509649_1_En_6_Figee_HTML.jpg

接下来,单击“管理控制台”向控制台进行身份验证。使用admin凭证作为用户名和密码:

img/509649_1_En_6_Figef_HTML.jpg

接下来,您需要创建一个新的领域。首先单击添加领域:

img/509649_1_En_6_Figeg_HTML.png

What is a Keycloak Realm?

一个领域是 Keycloak 中的核心概念。一个领域管理一组用户、凭据、角色和组。用户属于并登录到一个领域。领域相互隔离,只能管理和验证它们控制的用户。

当你第一次启动 Keycloak 时,它会为你创建一个预定义的领域。这个初始领域就是master领域。它是领域层级中的最高级别。该领域中的管理员帐户有权查看和管理在服务器实例上创建的任何其他领域。当您定义您的初始admin帐户时,您在master领域中创建了一个帐户。您首次登录管理控制台也将通过master领域。

接下来你将踏上创建新领域的第一步。调用领域quarkushop-realm然后点击创建:

img/509649_1_En_6_Figeh_HTML.jpg

接下来,您将看到quarkushop-realm配置页面:

img/509649_1_En_6_Figei_HTML.jpg

quarkushop-realm中,您需要定义您将使用的角色:useradmin角色。为此,只需转到角色➤Add 角色:

img/509649_1_En_6_Figej_HTML.jpg

现在您可以创建admin角色:

img/509649_1_En_6_Figek_HTML.jpg

创建user角色:

img/509649_1_En_6_Figel_HTML.jpg

现在您已经创建了角色,您需要创建用户。只需进入用户菜单:

img/509649_1_En_6_Figem_HTML.jpg

有时在这个屏幕上,当你打开页面时,用户列表并没有载入。如果发生这种情况,只需单击查看所有用户来加载列表。

单击 Add User 创建用户(在本例中为 Nebrass、Jason 和 Marie):

img/509649_1_En_6_Figeo_HTML.jpg

然后在单击 Save 之后,单击 Credentials 选项卡,您将在其中定义用户密码。

img/509649_1_En_6_Figep_HTML.gif不要忘记将Temporary设置为OFF以防止 Keycloak 在你第一次登录时要求你更新密码。然后,单击设置密码:

img/509649_1_En_6_Figeq_HTML.jpg

接下来,转到 Role Mappings 选项卡,将所有角色添加到 Nebrass 中。img/509649_1_En_6_Figer_HTML.gif是的!我将是这个应用的管理员。img/509649_1_En_6_Figes_HTML.gif

img/509649_1_En_6_Figet_HTML.jpg

现在创建 Jason 用户并定义密码:

img/509649_1_En_6_Figeu_HTML.jpg

将用户角色添加到 Jason:

img/509649_1_En_6_Figev_HTML.jpg

接下来,创建 Marie 用户并定义密码:

img/509649_1_En_6_Figew_HTML.jpg

给 Marie 添加user角色:

img/509649_1_En_6_Figex_HTML.jpg

您已完成角色和用户的配置!现在,单击“客户机”列出领域客户机。

img/509649_1_En_6_Figey_HTML.jpg

What is a Realm Client?

客户端是可以请求 Keycloak 对用户进行身份验证的实体。大多数情况下,客户端是希望使用 Keycloak 来保护自己并提供单点登录解决方案的应用和服务。客户端也可以是只是想要请求身份信息或访问令牌的实体,以便它们可以安全地调用网络上由 Keycloak 保护的其他服务。

单击“创建”添加具有以下设置的新客户端:

img/509649_1_En_6_Figez_HTML.jpg

  • 客户端 ID: quarkushop

  • 客户端协议:openid-connect

  • 根 URL: http://localhost:8080/

单击保存以显示客户端配置屏幕:

img/509649_1_En_6_Figfa_HTML.jpg

必须为您的客户端定义一个协议映射器。

What is a Protocol Mapper?

协议映射器对令牌和文档执行转换。他们可以将用户数据映射到协议声明,并转换客户端和auth服务器之间的任何请求。

单击映射器选项卡以配置映射器:

img/509649_1_En_6_Figfb_HTML.jpg

然后单击创建以添加新的映射器:

img/509649_1_En_6_Figfc_HTML.jpg

img/509649_1_En_6_Figfd_HTML.gif 为什么我们将令牌声明名称定义为组?

我们将使用协议映射器将用户领域角色(可以是useradmin)映射到一个名为groups的属性,我们将把它作为一个普通字符串添加到 ID 令牌、访问令牌和 userinfo 中。

选择groups是基于 Quarkus SmallRye JWT 库的MpJwtValidator Java 类(这是用于管理 JWT 的 Quarkus 实现);在这里,我们使用jwtPrincipal.getGroups()来定义SecurityIdentity角色:

JsonWebToken jwtPrincipal = parser.parse(request.getToken().getToken());
uniEmitter.complete(
        QuarkusSecurityIdentity.builder().setPrincipal(jwtPrincipal)
            .addRoles(jwtPrincipal.getGroups())
            .addAttribute(SecurityIdentity.USER_ATTRIBUTE, jwtPrincipal)
            .build()
);

这就是为什么我们定义映射器来影响嵌入在 JWT 令牌中的用户组属性的用户角色。img/509649_1_En_6_Figfe_HTML.gif

现在可以测试 Keycloak 实例是否按预期运行。只需使用cURL命令请求一个access_token:

curl -X POST http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token \
      -H 'content-type: application/x-www-form-urlencoded' \
      -d 'client_id=quarkushop' \
      -d 'username=nebrass' \
      -d 'password=password' \
      -d 'grant_type=password' | jq '.'

img/509649_1_En_6_Figff_HTML.gifURLhttp://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token由以下内容组成:

img/509649_1_En_6_Figfg_HTML.gif

img/509649_1_En_6_Figfh_HTML.gifjq是一个轻量级且灵活的命令行 JSON 处理器,我用它来格式化 JSON 输出。

您将得到如下的 JSON 响应:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJKcmlxWGQzYVBNOS13djhXUmVJekZkRnRJa3Z1WG5uNDd4a0JmTl95R19zIn0.eyJleHAiOjE1OTc0OTEwNTIsImlhdCI6MTU5NzQ5MDc1MiwianRpIjoiZmIxZmQxOWMtNWJlMC00YTgwLWExOTUtOTAxZjFkOTI3NDI5IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDgwL2F1dGgvcmVhbG1zL3F1YXJrdXNob3AtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMzU1ODc3YWQtMjY3Ny00ODJiLWE5NWYtYTI4ZjdmZGI1OTk5IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1c2hvcCIsInNlc3Npb25fc3RhdGUiOiJjM2E4ZmU3Mi02MzRmLTRiNmUtYTZkMS03MTkyOGI2YTBlN2YiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTmVicmFzcyBMYW1vdWNoaSIsImdyb3VwcyI6WyJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl0sInByZWZlcnJlZF91c2VybmFtZSI6Im5lYnJhc3MiLCJnaXZlbl9uYW1lIjoiTmVicmFzcyIsImZhbWlseV9uYW1lIjoiTGFtb3VjaGkiLCJlbWFpbCI6Im5lYnJhc3NAcXVhcmt1c2hvcC5zdG9yZSJ9.HZmicWhE9V8g74of9KGcZOVGvwC_oo2zs4-ElBBuV6XSWDUoiFLJVkSUzOV4WFzwvsM7V7_aZRzihZqq6QTtezweyhZIauo3pjmmtbMnq16WUFV-4oJWzk3P_6T5y74sh93aPuQtnw5hSQ4L68RjwQ6HIcaHJFkqrh6fX7uy0ZiHuPnRzhv38uQrD9YMC_z3tApWKTS2TA9igizZrlJCDfTdfiThUDuXEgOmw-pffYx1BASfL14O0c0apGPqirNkSgSrCpuFvikXlRdeu3YnI1JQ6S7Jn-qQI-bdCD5M0_ynaUiJn_p6sZqI6ioSmLGyA__S5J7nj_BO--fdIl0lUA",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "...",
  "token_type": "bearer",
  "not-before-policy": 0,
  "session_state": "c3a8fe72-634f-4b6e-a6d1-71928b6a0e7f",
  "scope": "email profile"
}

img/509649_1_En_6_Figfi_HTML.gif为了更漂亮的输出,我省略了refresh_token的值,因为它很长而且没用。

复制access_token值。然后去 jwt.io 把 JWT access_token贴在那里解码:

img/509649_1_En_6_Figfj_HTML.jpg

注意,有一个名为groups的 JSON 属性保存着我们在 Keycloak 中分配给 Nebrass 的 Roles 数组。

很好!Keycloak 运行正常,工作正常。现在,您将开始在 Java 端实现安全性。

在 QuarkuShop 中实现 auth 2 Java 组件

Java 配置端

第一步是向 QuarkuShop 添加 Quarkus SmallRye JWT 依赖项:


./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-smallrye-jwt"

接下来,将安全配置添加到application.properties:

  • ①启用 HTTP CORS 过滤器。
1  ### Security
2  quarkus.http.cors=true  ①
3  # MP-JWT Config
4  mp.jwt.verify.issuer=http://localhost:9080/auth/realms/quarkushop-realm   ②
5  mp.jwt.verify.publickey.location=http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs   ③
6  # Keycloak Configuration
7  keycloak.credentials.client-id=quarkushop   ④

What is CORS?

跨源资源共享(CORS)是一种机制,允许从提供第一资源的域之外的另一个域请求网页上的受限资源。

  • ② Config 属性指定服务器将接受为有效的 JWT 令牌的iss(颁发者)声明的值。我们已经在解密的 JWT 令牌的iss字段中获得了这个值。

  • ③ Config 属性允许指定公钥的外部或内部位置。该值可以是相对路径或 URL。当使用 Keycloak 作为身份管理器时,该值将是mp.jwt.verify.issuer加上/protocol/openid-connect/certs

  • ④一个带有名为keycloak.credentials.client-id的键的自定义属性,它保存值quarkushop,这是领域客户端 ID。

Java 源代码端

在 Java 代码中,您需要根据我们为每个服务创建的授权矩阵来保护 REST APIs。

让我们从 Carts REST API 开始,其中所有操作都允许用户和管理员进行。Carts REST API 上只允许经过身份验证的主题。为了满足这个需求,我们从io.quarkus.security包中获得了一个@Authenticated注释,该注释将只授予经过身份验证的主体访问权限。

img/509649_1_En_6_Figfl_HTML.gif为了区分应用用户角色用户,我将应用用户称为主体。术语 subject 来自旧的 good Java 认证和授权服务(JAAS ),用来表示请求访问资源的调用者。主体可以是任何实体,包括作为服务

因为CartResource中的所有操作都需要一个经过认证的主题,所以你必须用@Authenticated来注释CartResource类,这样它将被应用到所有的操作中。见清单 6-1 。

@Authenticated
@Path("/carts")
@Tags(value = @Tag(name = "cart", description = "All the cart methods"))
public class CartResource {
    ...
}

Listing 6-1com.targa.labs.quarkushop.web.CartResource

下一个 REST API 是 Category API,只有管理员可以创建和删除类别。所有人(管理员、用户和匿名用户)都可以进行其他操作。要仅授予特定角色访问权限,请使用javax.annotation.security包中的@RolesAllowed注释。

img/509649_1_En_6_Figfm_HTML.gifJavaDoc of javax . annotation . security . roles allowed

@RolesAllowed:指定允许访问应用中的方法的安全角色列表。

@RolesAllowed注释的值是安全角色名称的列表。可以在类或方法上指定此批注:

  • 在类级别指定它意味着它适用于类中的所有操作。

  • 在方法上指定它意味着它仅适用于该方法。

  • 如果应用于类和操作级别,当两者冲突时,方法值将覆盖类值。

基于 JavaDoc,我们将传递期望的角色作为注释的值。将@RolesAllowed("admin")注释应用于创建和删除类别的操作:

@Path("/categories")
@Tags(value = @Tag(name = "category", description = "All the category methods"))
public class CategoryResource {
    ...

    @RolesAllowed("admin")
    @POST
        public CategoryDto create(CategoryDto categoryDto) {
        return this.categoryService.create(categoryDto);
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.categoryService.delete(id);
    }
}

对于 Customer REST API,只有管理员可以访问所有的服务。用@RolesAllowed("admin")注释CustomerResource类将应用这个策略。

接下来是 Order REST API,通过身份验证的主体可以访问所有操作,除了findAll()方法,该方法只允许用于admin。因此,我们将类级别的两个@Authenticated注释和需要特定角色的方法上的@RolesAllowed注释结合起来:

@Authenticated
@Path("/orders")
@Tag(name = "order", description = "All the order methods")
public class OrderResource {
    ...

    @RolesAllowed("admin")
    @GET
    public List<OrderDto> findAll() {
        return this.orderService.findAll();
    }
    ...
}

img/509649_1_En_6_Figfn_HTML.gif当我们在本地方法中使用@RolesAllowed时,我们覆盖了类级别@Authenticated应用的策略。

OrderResource之后,我们将处理 OrderItem REST API,其中只允许经过身份验证的主体访问所有服务。用@Authenticated注释OrderItemResource类将应用这个策略。

除了删除和列出所有付款仅授权给admin之外,PaymentResource仅授权给经过身份验证的主体访问:

@Authenticated
@Path("/payments")
@Tag(name = "payment", description = "All the payment methods")
public class PaymentResource {
    ...

    @RolesAllowed("admin")
    @GET
    public List<PaymentDto> findAll() {
        return this.paymentService.findAll();
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.paymentService.delete(id);
    }
    ...
}

对于产品 REST API,除了创建和删除产品之外,所有人都可以执行所有操作,只有admin才可以执行这些操作:

@Path("/products")
@Tag(name = "product", description = "All the product methods")
public class ProductResource {
    ...

    @RolesAllowed("admin")
    @POST
        public ProductDto create(ProductDto productDto) {
        return this.productService.create(productDto);
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.productService.delete(id);
    }
    ...
}

最后,最后一个 REST API 是ReviewResource:

@Path("/reviews")
@Tag(name = "review", description = "All the review methods")
public class ReviewResource {
    ...

    @Authenticated
    @POST
    @Path("/product/{id}")
        public ReviewDto create(ReviewDto reviewDto, @PathParam("id") Long id) {
        return this.reviewService.create(reviewDto, id);
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.reviewService.delete(id);
    }
}

太好了。您已经应用了一级授权层!img/509649_1_En_6_Figfo_HTML.gif

不过,不要太自信。img/509649_1_En_6_Figfp_HTML.gif这个实现泄露了更多级别的授权。例如,用户被授权删除OrdersOrderItems,但是我们无法验证被认证的主体只是删除了自己的OrdersOrderItems,而没有删除其他用户的OrdersOrderItems。我们知道如何定义基于角色的访问规则,但是我们如何收集关于被认证主体的更多信息呢?

为此,我们将创建一个名为UserResource的新 REST API。当被调用时,UserResource将返回当前已验证的主题信息作为响应。

UserResource会是这样的:

  • ①注入一个JsonWebToken,它是定义使用 JWT 作为承载令牌的规范的实现。这个注入将在传入的请求中使用 JWT 令牌的一个实例。

  • JsonWebToken将返回注入的 JWT 令牌。

  • getCurrentUserInfoClaims()方法将返回可用声明的列表以及嵌入在 JWT 令牌中的它们各自的值。声明将从注入到UserResource中的 JWT 令牌实例中提取出来。

@Path("/user")
@Authenticated
@Tag(name = " user", description = "All the user methods")
public class UserResource {

    @Inject
    JsonWebToken jwt;                                           ①

    @GET
    @Path("/current/info")
    public JsonWebToken getCurrentUserInfo() {                  ②
        return jwt;
    }

    @GET
    @Path("/current/info/claims")
    public Map<String, Object> getCurrentUserInfoClaims() {     ③
        return jwt.getClaimNames()
                .stream()
                .map(name -> Map.entry(name, jwt.getClaim(name)))
                .collect(Collectors.toMap(
                        entry -> entry.getKey(),
                        entry -> entry.getValue())
                );
    }
}

让我们测试这个新的 REST API。从获得新的access_token开始:

export access_token=$(curl -X POST http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token \
      -H 'content-type: application/x-www-form-urlencoded' \
      -d 'client_id=quarkushop' \
      -d 'username=nebrass' \
      -d 'password=password' \
      -d 'grant_type=password' | jq --raw-output '.access_token')

然后,调用/user/current/info REST API:

curl -X GET -H "Authorization: Bearer $access_token" http://localhost:8080/api/user/current/info | jq '.'

作为回应,您将获得 JWT 令牌:

{
  "audience": [
    "account"
  ],
  "claimNames": [
    "sub", "resource_access", "email_verified", "allowed-origins", "raw_token", "iss",
    "groups", "typ", "preferred_username", "given_name", "aud", "acr", "realm_access",
    "azp", "scope", "name", "exp", "session_state", "iat", "family_name", "jti", "email"
  ],
  "expirationTime": 1597530481,
  "groups": ["offline_access", "admin", "uma_authorization", "user"],
  "issuedAtTime": 1597530181,
  "issuer": "http://localhost:9080/auth/realms/quarkushop-realm",
  "name": "nebrass",
  "rawToken": "eyJhbGci...L5A",
  "subject": "355877ad-2677-482b-a95f-a28f7fdb5999",
  "tokenID": "7416ee6e-e74c-45ae-bf85-8889744eaacf"
}

很好,我们有可用的索赔。让我们得到它们各自的值:

{
  "sub": "355877ad-2677-482b-a95f-a28f7fdb5999",
  "resource_access": {
    "account": {
      "roles": ["manage-account", "manage-account-links", "view-profile"]
    }
  },
  "email_verified": true,
  "allowed-origins": ["http://localhost:8080"],
  "raw_token": "eyJhbGci...L5A",
  "iss": "http://localhost:9080/auth/realms/quarkushop-realm",
  "groups": ["offline_access", "admin", "uma_authorization", "user"],
  "typ": "Bearer",
  "preferred_username": "nebrass",
  "given_name": "Nebrass",
  "aud": ["account"],
  "acr": "1",
  "realm_access": {
    "roles": ["offline_access", "admin", "uma_authorization", "user"]
  },
  "azp": "quarkushop",
  "scope": "email profile",
  "name": "Nebrass Lamouchi",
  "exp": 1597530481,
  "session_state": "68069099-f534-434f-8d08-d8d75b8ff1c6",
  "iat": 1597530181,
  "family_name": "Lamouchi",
  "jti": "7416ee6e-e74c-45ae-bf85-8889744eaacf",
  "email": "nebrass@quarkushop.store"
}

太棒了!现在,您知道了如何访问安全细节,您可以使用这些细节来准确地识别经过身份验证的主题。顺便说一句,你可以得到关于 JWT 币的同样信息。你可以通过另一种方式获得同样的 JWT 代币:

@GET()
@Path("/current/info-alternative")
public Principal getCurrentUserInfoAlternative(@Context SecurityContext ctx) {
    return ctx.getUserPrincipal();
}

作为方法参数传递的@Context SecurityContext ctx用于在当前上下文中注入SecurityContext

img/509649_1_En_6_Figfr_HTML.gif SecurityContext是一个可注入的接口,像Principal一样提供对安全相关信息的访问。

img/509649_1_En_6_Figfs_HTML.gifPrincipal接口表示委托人的抽象概念,可以用来表示任何实体,比如个人、公司或登录 ID。在这种情况下,主体是 JWT 令牌。img/509649_1_En_6_Figft_HTML.gif

我不会深入研究授权层的完整实现。我就讲到这里;否则,我就需要花两章的时间来讨论这个内容。

安全部分的最后一步是添加一个 REST API,为给定的用户名和密码返回一个access_token。这可能很有用,尤其是对于 Swagger UI。说到 Swagger UI,我们需要让它知道我们的安全层。

我们将从创建TokenService开始,它将是从 Keycloak 发出access_token请求的 REST 客户端。

当我们使用 Java 11 时,我们可以享受它的一个伟大的新特性:全新的 HTTP 客户端。

正如您在使用cURL命令之前所做的那样,您需要使用 Java 11 HTTP 客户端来请求一个access_token:

  • ①注释为每个 HTTP 请求创建一个TokenService实例。

  • ②获取mp.jwt.verify.issuer属性值以构建 Keycloak 令牌的端点 URL。

  • ③获取请求access_token所需的keycloak.credentials.client-id属性值。

@RequestScoped                              ①
public class TokenService {

    @ConfigProperty(name = "mp.jwt.verify.issuer", defaultValue = "undefined")
    Provider<String> jwtIssuerUrlProvider;  ②

    @ConfigProperty(name = "keycloak.credentials.client-id", defaultValue = "undefined")
    Provider<String> clientIdProvider;      ③

    public String getAccessToken(String userName, String password)
                        throws IOException, InterruptedException {

        String keycloakTokenEndpoint =
                    jwtIssuerUrlProvider.get() + "/protocol/openid-connect/token";

        String requestBody = "username=" + userName + "&password=" + password +
                    "&grant_type=password&client_id=" + clientIdProvider.get();

        if (clientSecret != null) {
            requestBody += "&client_secret=" + clientSecret;
        }

        HttpClient client = HttpClient.newBuilder().build();

        HttpRequest request = HttpRequest.newBuilder()
                .POST(BodyPublishers.ofString(requestBody))
                .uri(URI.create(keycloakTokenEndpoint))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        String accessToken = "";

        if (response.statusCode() == 200) {
            ObjectMapper mapper = new ObjectMapper();
            try {
                accessToken = mapper.readTree(response.body()).get("access_token").textValue();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            throw new UnauthorizedException();
        }

        return accessToken;
    }
}

img/509649_1_En_6_Figfv_HTML.gif我们将属性值设为Provider<String>而不是String,以避免本地映像构建失败。

接下来,添加getAccessToken()方法:

@Path("/user")
@Authenticated
@Tag(name = " user", description = "All the user methods")
public class UserResource {

    @Inject JsonWebToken jwt;

    @POST
    @PermitAll
    @Path("/access-token")
    @Produces(MediaType.TEXT_PLAIN)
    public String getAccessToken(@QueryParam("username") String username,
                                @QueryParam("password") String password)
                        throws IOException, InterruptedException {
        return tokenService.getAccessToken(username, password);
    }
    ...
}

太棒了。您可以从 Swagger UI 测试新方法:

img/509649_1_En_6_Figfx_HTML.jpg

耶!img/509649_1_En_6_Figfy_HTML.gif您现在需要找到一种方法让 Swagger UI 应用access_token来请求所有的 REST APIs。

我原以为这将是一次长途旅行,但这是一项非常容易的任务。仅仅 10 行代码就足以让聚会:img/509649_1_En_6_Figfz_HTML.gif

  • @SecurityScheme定义了 OpenAPI 操作可以使用的安全方案。

  • ②添加 OpenAPI 定义的描述。

  • ③将创建的安全模式jwt链接到 OpenAPI 定义。

@SecurityScheme(
        securitySchemeName = "jwt",             ①
        description = "JWT authentication with bearer token",
        type = SecuritySchemeType.HTTP,         ①
        in = SecuritySchemeIn.HEADER,           ①
        scheme = "bearer",                      ①
        bearerFormat = "Bearer [token]")        ①
@OpenAPIDefinition(
        info = @Info(                           ②
                title = "QuarkuShop API",
                description = "Sample application for the book 'Playing with Java Microservices with Quarkus and Kubernetes'",
                contact = @Contact(name = "Nebrass Lamouchi", email = "lnibrass@gmail.com", url = "https://blog.nebrass.fr"),
                version = "1.0.0-SNAPSHOT"
        ),
        security = @SecurityRequirement(name = "JWT") ③
)
public class OpenApiConfig extends Application {
}

再次检查 Swagger UI:

img/509649_1_En_6_Figga_HTML.jpg

注意有锁图标img/509649_1_En_6_Figgb_HTML.gif

使用getAccessToken()操作创建一个access_token,然后点击【授权 将生成的access_token传递给SecurityScheme。最后,单击授权:

img/509649_1_En_6_Figgd_HTML.jpg

现在,当您单击任何操作时,Swagger UI 将包含access_token作为每个请求的载体。

太棒了!现在您已经有了 Keycloak 和安全性 Java 组件。接下来,您需要重构测试以了解安全层。

更新集成测试以支持授权 2

对于测试,您需要动态地提供 Keycloak 实例,就像您使用Testcontainers库为数据库所做的那样。

我们将使用同一个库来提供 Keycloak。我们不会像使用 PostgreSQL 那样使用普通的 Docker 容器,因为在那里,Flyway 为我们填充了数据库。对于 Keycloak,我们没有一个内置的机制来为我们创建 Keycloak 领域。唯一可能的解决方案是使用 Docker Compose 文件,该文件将导入一个示例 Keycloak 领域文件。幸运的是,Testcontainers提供了对 Docker 编写文件的强大支持。

要使用TestContainers从 Docker 合成文件中提供容器,请使用:

public static DockerComposeContainer KEYCLOAK = new DockerComposeContainer(
    new File("src/main/docker/keycloak-test.yml"))
       .withExposedService("keycloak_1", 9080,
           Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

我们将使用一个包含以下内容的示例 Keycloak 领域:

  • 两个用户:

    • 管理员(用户名admin,密码test,角色admin

    • 测试(用户名user,密码test,角色user

  • Keycloak 客户端有一个client-id=quarkus-client:和``secret=mysecret`。

QuarkusTestResourceLifecycleManager将使用我们创建的TokenService与提供的 Keycloak 实例通信,并且它将使用示例领域凭证来获取我们将在集成测试中使用的access_tokens

我们将得到两个access_tokens:一个用于admin角色,一个用于user角色。我们将把它们与mp.jwt.verify.publickey.locationmp.jwt.verify.issuer一起存储,作为当前测试范围中的系统属性。

自定义QuarkusTestResourceLifecycleManager将是这样的:

public class KeycloakRealmResource implements QuarkusTestResourceLifecycleManager {
     @ClassRule
     public static DockerComposeContainer KEYCLOAK = new DockerComposeContainer(
           new File("src/main/docker/keycloak-test.yml"))
          .withExposedService("keycloak_1",
                    9080,
                    Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

       @Override
       public Map<String, String> start() {
              KEYCLOAK.start();

             String jwtIssuerUrl = String.format(
                    "http://%s:%s/auth/realms/quarkus-realm",
                    KEYCLOAK.getServiceHost("keycloak_1", 9080),
                    KEYCLOAK.getServicePort("keycloak_1", 9080)
             );

            TokenService tokenService = new TokenService();
            Map<String, String> config = new HashMap<>();
            try {
                 String adminAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                      "admin", "test", "quarkus-client", "mysecret"
                );

                String testAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                      "test", "test", "quarkus-client", "mysecret"
                );

               config.put("quarkus-admin-access-token", adminAccessToken);
               config.put("quarkus-test-access-token", testAccessToken);

           } catch (IOException | InterruptedException e) {
                  e.printStackTrace();
          }

          config.put("mp.jwt.verify.publickey.location", jwtIssuerUrl + "/protocol/openidconnect/certs");
          config.put("mp.jwt.verify.issuer", jwtIssuerUrl);

          return config;
      }

     @Override
      public void stop() {
              KEYCLOAK.stop();
     }
}

我们将把它和@QuarkusTestResource(KeycloakRealmResource.class)注释一起使用。

@BeforeAll方法中,我们从系统属性中获取access_tokens,使它们准备好在测试中使用。一个典型的测试头骨看起来像这样:

@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
@QuarkusTestResource(KeycloakRealmResource.class)
class CategoryResourceTest {

     static String ADMIN_BEARER_TOKEN;
     static String USER_BEARER_TOKEN;

     @BeforeAll
     static void init() {
       ADMIN_BEARER_TOKEN = System.getProperty("quarkus-admin-access-token");
       USER_BEARER_TOKEN = System.getProperty("quarkus-test-access-token");
    }

    ...
}

要在测试中使用这些令牌:

@Test
void testFindAllWithAdminRole() {
   given().when()
          .header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_BEARER_TOKEN)
          .get("/carts")
          .then()
          .statusCode(OK.getStatusCode())
          .body("size()", greaterThan(0));
}

您需要在测试中测试和验证授权规则,例如,验证给定的概要文件不是Unauthorized:

@Test
void testFindAll() {
    get("/carts").then()
          .statusCode(UNAUTHORIZED.getStatusCode());
}

要验证对于给定的请求,REST API 上不允许主题,请使用以下命令:

@Test
void testDeleteWithUserRole() {
   given().when()
          .header(HttpHeaders.AUTHORIZATION, "Bearer " + USER_BEARER_TOKEN)
          .delete("/products/1")
          .then()
          .statusCode(FORBIDDEN.getStatusCode());
}

很好!我实现了所有的测试;你可以在我的 GitHub 库中找到它们。img/509649_1_En_6_Figgf_HTML.gif

将 Keycloak 添加到生产环境

最后一步是将生产 Keycloak 条目添加到生产虚拟机的 Docker Compose 中。我们还需要添加生产领域。

您可以通过非常少的步骤从本地 Keycloak 实例导出领域。

要从本地 Keycloak 容器(名为docker-keycloak)中导出领域,请使用以下命令:

$docker exec -it docker-keycloak bash

bash-4.4$ cd /opt/jboss/keycloak/bin/

bash-4.4$ mkdir backup

bash-4.4$ ./standalone.sh -Djboss.socket.binding.port-offset=1000 \
      -Dkeycloak.migration.realmName=quarkushop-realm \
      -Dkeycloak.migration.action=export \
      -Dkeycloak.migration.provider=dir \
      -Dkeycloak.migration.dir=./backup/

要将存储的领域从docker-keycloak容器复制到本地目录,请使用以下命令:

$ mkdir ~/keycloak-realms

$ docker cp docker-keycloak:/opt/jboss/keycloak/bin/backup ~/keycloak-realms

你将在~/keycloak-realms中获得两个钥匙锁王国文件——quarkushop-realm-realm.jsonquarkushop-realm-users-0.json

您需要编辑quarkushop-realm-realm.json文件并将sslRequiredexternal更改为none:

{
    ...
    "sslRequired": "none",
    ...
}

img/509649_1_En_6_Figgg_HTML.gif"sslRequired": "none"属性禁用任何请求所需的 SSL 证书。

然后,将两个文件——quarkushop-realm-realm.jsonquarkushop-realm-users-0.json——复制到 Azure VM 实例中的/opt/realms目录,这是生产环境。

这是 Azure 虚拟机实例。img/509649_1_En_6_Figgh_HTML.gif我们将完成最后一步:将 Keycloak 服务添加到/opt/docker-compose.yml文件中:

  • ①覆盖MP_JWT_VERIFY_PUBLICKEY_LOCATIONMP_JWT_VERIFY_ISSUER属性以确保应用指向 Azure VM 实例而不是localhost

  • ②添加 Keycloak Docker 服务。

  • ③定义 Keycloak 集群的用户名和密码。

  • ④将 Keycloak 数据库供应商定义为 PostgreSQL,因为我们的服务中有一个 PostgreSQL DB 实例。

  • ⑤将 DB 主机定义为postgresql-db,由 Docker 用服务 IP 动态解析。

  • ⑥将 Keycloak 数据库凭据定义为与 PostgreSQL 凭据相同。

  • ⑦表示 Keycloak 和 PostgreSQL 服务之间的依赖关系。

 1 version: '3'
 2 services:
 3   quarkushop:
 4     image: nebrass/quarkushop-monolithic-application:latest
 5     environment:
 6       - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/demo
 7       -
  MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://51.103.50.23:9080/auth/realms/quarkushoprealm/protocol/openid-connect/certs   ①
 8       - MP_JWT_VERIFY_ISSUER=http://51.103.50.23:9080/auth/realms/quarkushop-realm ①
 9    ports:
10      - 8080:8080
11   postgresql-db:
12     image: postgres:13
13     volumes:
14       - /opt/postgres-volume:/var/lib/postgresql/data
15     environment:
16       - POSTGRES_USER=developer
17       - POSTGRES_PASSWORD=p4SSW0rd
18       - POSTGRES_DB=demo
19       - POSTGRES_HOST_AUTH_METHOD=trust
20    ports:
21      - 5432:5432
22   keycloak:                                     ②
23     image: jboss/keycloak:latest
24     command:
25       [
26         '-b','0.0.0.0',
27
28         '-Dkeycloak.migration.action=import',
29         '-Dkeycloak.migration.provider=dir',
30         '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
31         '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
32         '-Djboss.socket.binding.port-offset=1000',
33         '-Dkeycloak.profile.feature.upload_scripts=enabled',
34       ]
35
36
37   volumes:
38     - ./realms:/opt/jboss/keycloak/realms
39   environment:
40     - KEYCLOAK_USER=admin     ③
41     - KEYCLOAK_PASSWORD=admin ③
42     - DB_VENDOR=POSTGRES      ④
43     - DB_ADDR=postgresql-db   ⑤
44     - DB_DATABASE=demo        ⑥
45     - DB_USER=developer       ⑥
46     - DB_SCHEMA=public        ⑥
47     - DB_PASSWORD=p4SSW0rd    ⑥
48   ports:
49     - 9080:9080
50   depends_on:                 ⑦
51     - postgresql-db           ⑦

img/509649_1_En_6_Figgi_HTML.gif如您所见,所有凭证都以纯文本形式列在docker-compose.yml文件中。

您可以使用 Docker 机密来保护这些凭证。每个密码都将存储在 Docker secret 中。

docker service create --name POSTGRES_USER --secret developer
docker service create --name POSTGRES_PASSWORD --secret p4SSW0rd
docker service create --name POSTGRES_DB --secret demo
docker service create --name KEYCLOAK_USER --secret admin
docker service create --name KEYCLOAK_PASSWORD --secret admin

img/509649_1_En_6_Figgj_HTML.gif遗憾的是,该功能仅适用于 Docker Swarm 集群。

为了保护凭证,将它们存储在 Azure VM 实例上的一个~/.env文件中:

POSTGRES_USER=developer
POSTGRES_PASSWORD=p4SSW0rd
POSTGRES_DB=demo
KEYCLOAK_USER=admin
KEYCLOAK_PASSWORD=admin

然后,更改docker-compose.yml以使用~/.env元素:

version: '3'
services:
   quarkushop:
     image: nebrass/quarkushop-monolithic-application:latest
     environment:
       - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/${POSTGRES_DB}
       - MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://51.103.50.23:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
       - MP_JWT_VERIFY_ISSUER=http://51.103.50.23:9080/auth/realms/quarkushop-realm
     ports:
       - 8080:8080
   postgresql-db:
     image: postgres:13
     volumes:
       - /opt/postgres-volume:/var/lib/postgresql/data
     environment:
       - POSTGRES_USER=${POSTGRES_USER}
       - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
       - POSTGRES_DB=${POSTGRES_DB}
       - POSTGRES_HOST_AUTH_METHOD=trust
     ports:
       - 5432:5432
   keycloak:
     image: jboss/keycloak:latest
     command:
      [
       '-b',
       '0.0.0.0',
       '-Dkeycloak.migration.action=import',
       '-Dkeycloak.migration.provider=dir',
       '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
       '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
       '-Djboss.socket.binding.port-offset=1000',
       '-Dkeycloak.profile.feature.upload_scripts=enabled',
      ]
     volumes:
       - ./realms:/opt/jboss/keycloak/realms
     environment:
       - KEYCLOAK_USER=${KEYCLOAK_USER}
       - KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD}
       - DB_VENDOR=POSTGRES
       - DB_ADDR=postgresql-db
       - DB_DATABASE=${POSTGRES_DB}
       - DB_USER=${POSTGRES_USER}
       - DB_SCHEMA=public
       - DB_PASSWORD=${POSTGRES_PASSWORD}
     ports:
       - 9080:9080
     depends_on:
       - postgresql-db

很好!Docker 编写服务已经准备就绪!img/509649_1_En_6_Figgk_HTML.gif

我们只需要为 Keycloak 端口的 Azure VM 实例网络规则添加一个例外。在 Azure VM 实例中,转到网络部分,为端口 9080 添加一个例外:

img/509649_1_En_6_Figgl_HTML.jpg

太棒了!img/509649_1_En_6_Figgm_HTML.gif生产环境拥有部署新版本 QuarkuShop 容器所需的所有元素,而不会给 PostgreSQL 数据库或 Keycloak 集群带来风险。

转到生产 Swagger UI,享受 QuarkuShop:最伟大的在线商店!img/509649_1_En_6_Figgn_HTML.gif

img/509649_1_En_6_Figgo_HTML.jpg

很好!img/509649_1_En_6_Figgp_HTML.gif该进入下一个抗灾层了:监控 img/509649_1_En_6_Figgq_HTML.gif

实现监控层

安全性并不是应用中唯一重要的附加层。指标监控也是一个非常重要的层,可以防止灾难。想象一下这样一种情况,您在云中的服务器上部署了一个超级安全、功能强大的应用。如果您不定期检查应用指标,应用可能会在您不知道的情况下耗尽资源。

应用监控不仅仅是一种获取指标的机制;它包括分析不同组件的性能和行为。在本章中,我不会涵盖所有不同的监控工具和实践。

相反,您将看到如何实现两个组件:

  • 应用状态指示器,也称为运行状况检查指示器。

  • 应用指标服务,用于提供有关应用的各种指标和统计信息。

实施健康检查

在 Quarkus 中实现健康检查是一项非常简单的任务:只需添加 SmallRye 健康扩展。没错。你只要给pom.xml加一个库就行了!魔法会自动发生!

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

SmallRye Health 会自动将健康检查端点添加到您的 Quarkus 应用中。

对于那些习惯了 Spring Boot 的人来说,SmallRye Health 就相当于执行器。

新的健康检查终点是:

  • /health/live:应用启动并运行。

  • 应用已经准备好服务请求。

  • /health:累积应用中的所有健康检查程序。

运行应用并在http://localhost:8080/api/health上执行cURL GET:

curl -X GET http://localhost:8080/api/health | jq '.'

您将得到一个 JSON 响应:

{
  "status": "UP",
  "checks": [
    {
      "name": "Database connections health check",
      "status": "UP"
    }
  ]
}

这个 JSON 响应确认应用正在正确运行,并且进行了一次检查,确认数据库是UP

所有的 health REST 端点都返回一个简单的 JSON 对象,它有两个字段:

  • status:所有健康检查程序的总体结果

  • checks:单个支票的数组

Quarkus 开箱即用,包括一个数据库检查。让我们为 Keycloak 实例创建一个健康检查:

  • ①您获得了mp.jwt.verify.publickey.location属性,它将被用作 Keycloak URL。

  • ②你用 3000 毫秒的超时实例化 Java 11 HTTPClient

  • ③您将健康检查的名称定义为Keycloak connection health check

  • ④您验证了 Keycloak URL 是可访问的,并且响应状态代码是 HTTP 200。

  • ⑤如果keycloakConnectionVerification()抛出异常,健康检查状态将为down

  • ⑥您构建健康检查响应并将其发送回调用者。

@Liveness
@ApplicationScoped
public class KeycloakConnectionHealthCheck implements HealthCheck {

    @ConfigProperty(name = "mp.jwt.verify.publickey.location", defaultValue = "false")
    private Provider<String> keycloakUrl;                                 ①

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder =
                HealthCheckResponse.named("Keycloak connection health check");                               ③

        try {
            keycloakConnectionVerification();                             ④
            responseBuilder.up();                                         ⑤
        } catch (IllegalStateException e) {
            // cannot access keycloak
            responseBuilder.down().withData("error", e.getMessage());                                              ⑤
        }

        return responseBuilder.build();                                   ⑥
    }

    private void keycloakConnectionVerification() {
        HttpClient httpClient = HttpClient.newBuilder()                   ②
                .connectTimeout(Duration.ofMillis(3000))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(keycloakUrl.get()))
                .build();

        HttpResponse<String> response = null;

        try {
            response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }

        if (response == null || response.statusCode() != 200) {
            throw new IllegalStateException("Cannot contact Keycloak");
        }
    }
}

让我们再来看一下cURL/health终点:

curl -X GET http://localhost:8080/api/health | jq '.'

新的 JSON 响应将是:img/509649_1_En_6_Figgs_HTML.gif

{
  "status": "UP",
  "checks": [
    {
      "name": "Keycloak connection health check",
      "status": "UP"
    },
    {
      "name": "Database connections health check",
      "status": "UP"
    }
  ]
}

很好!img/509649_1_En_6_Figgt_HTML.gif就这么简单!

还有呢!SmallRye Health 在http://localhost:8080/api/health-ui/提供了一个非常有用的健康 UI:

img/509649_1_En_6_Figgu_HTML.jpg

甚至还有一个池服务,可以配置为按照您设置的时间间隔刷新页面:

img/509649_1_En_6_Figgv_HTML.jpg

这个健康用户界面非常有用,但不幸的是,在prod配置文件中它没有被默认激活。要为每个配置文件/环境启用它,请使用此属性:

### Health Check
quarkus.smallrye-health.ui.always-include=true

太好了。为了实现指标服务,是时候进入下一步了。

实现度量服务

指标是重要且关键的监控数据。Quarkus 有许多用于度量公开的专用库,以及非常丰富的工具集来构建定制的应用度量。毫不奇怪,第一个库也来自 SmallRye 家族。它被称为quarkus-smallrye-metrics,用于公开基于微文件规范的度量。

从 Quarkus v1.9 开始,不推荐使用 SmallRye 度量。Quarkus 正式采用微米作为其度量的新标准。这种采用是基于云市场的趋势和需求。

img/509649_1_En_6_Figgx_HTML.gif要了解更多关于从微轮廓度量到微米度量的转换,参见 https://quarkus.io/blog/micrometer-metrics/

实现度量服务的第一步是添加quarkus-micrometer Maven 依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-micrometer</artifactId>
</dependency>

这种依赖性将提供对千分尺核心度量类型的访问:

  • 计数器:计数器用于测量只增加的数值。例如,测量调用 REST API 的次数。

  • 标尺:显示当前值的指示器,如创建的对象或线程的数量。

  • 定时器:用于测量短时潜伏期及其频率。

  • 分布汇总:用于跟踪事件的分布。它在结构上类似于计时器,但记录的值不代表时间单位。例如,分布摘要可用于测量到达服务器的请求的有效负载大小。

所有这些对象都需要存储在某个地方,这就是仪表注册表的目的。As Micrometer 支持很多监控系统(Prometheus、Azure Monitor、Stackdriver、Datadog、Cloudwatch 等。),我们会为MeterRegistry找一个专用的实现。在这个例子中,我们使用 Prometheus 作为监控系统。在这种情况下,您需要添加它的 Maven 依赖项:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

现在,重启应用并检查 Quarkus Micrometer 生成的http://localhost:8080/metrics URL:

# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
# TYPE jvm_threads_live_threads gauge
jvm_threads_live_threads 64.0
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 1.63971072E8
jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.589934592E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 7598080.0
jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 1.63975168E8
...

这些指标由 Micrometer 生成,与 Prometheus 兼容。您可以尝试在独立的 Prometheus 实例中导入它们。

先从 https://prometheus.io/download/ 下载普罗米修斯:

img/509649_1_En_6_Figgy_HTML.png

为您的机器/操作系统选择合适的版本并下载。对我来说是darwin-amd64

解压缩档案后,编辑prometheus-2.27.0.darwin-amd64/prometheus.yml文件,如下所示:

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
...
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['localhost:9090']

将定义目标/metrics API 主机位置的最后一行从localhost:9090改为localhost:8080。这是应用 URL

运行普罗米修斯:

$ ./prometheus

...msg="Start listening for connections" address=0.0.0.0:9090

您可以从http://localhost:9090/访问它:

img/509649_1_En_6_Figgz_HTML.png

在表达式输入中,输入system_cpu_usage并点击执行。然后单击图表选项卡:

img/509649_1_En_6_Figha_HTML.png

现在,您可以为TokenService创建自己的定制应用指标提供者:

  • ①为执行getAccessToken()方法的持续时间创建一个名为tokensRequestsTimer的计时器。

  • ②创建一个名为tokensRequestsCounter的计数器,它将在每次调用getAccessToken()方法时递增。

@Slf4j
@RequestScoped
public class TokenService {
    private static final String TOKENS_REQUESTS_TIMER = "tokensRequestsTimer";     ①
    private static final String TOKENS_REQUESTS_COUNTER = "tokensRequestsCounter";   ②

    @Inject MeterRegistry registry;
    ...
    @PostConstruct
    public void init() {
        registry.timer(TOKENS_REQUESTS_TIMER, Tags.empty());             ①
        registry.counter(TOKENS_REQUESTS_COUNTER, Tags.empty());         ②
    }

    public String getAccessToken(String userName, String password) {
        var timer = registry.timer(TOKENS_REQUESTS_TIMER);               ①
        return timer.record(() -> { var accessToken = "";
            try {
                accessToken = getAccessToken(jwtIssuerUrlProvider.get(),
                                userName, password, clientIdProvider.get(), null);
                registry.counter(TOKENS_REQUESTS_COUNTER).increment();   ②
            } catch (IOException e) { log.error(e.getMessage());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("Cannot get the access_token");
            }
            return accessToken;
        });
    }
    ...
}

转到 Swagger UI 并多次请求一个access_token来生成一些指标。然后返回到 Prometheus UI 检查新的指标:

img/509649_1_En_6_Fighb_HTML.png

根据前面的截图,看看tokensRequestsCounter达到的值。它的普罗米修斯表达式是:

img/509649_1_En_6_Fighc_HTML.png

您可以检查tokensRequests计时器的最大值,它与 Prometheus 中的tokensRequestsTimer_seconds_max表达式相匹配:

img/509649_1_En_6_Fighd_HTML.png

太好了。现在,您已经拥有了基本的运行状况检查和监控组件,可以确保您的应用运行良好,并拥有所有必需的资源。

结论

我认为安全和监控是抗灾难层,但即使应用了这些组件,应用仍然面临许多风险。其中之一就是高可用性。*****

七、微服务架构模式

介绍

到目前为止,我们已经将 QuarkuShop 开发为一个单块应用。所有组件都封装在一个封装中。这个单块被称为整块

根据《牛津英语词典》,monolith 是指一个单一的、非常大的组织,变化非常缓慢”。在软件架构世界中,monolithic 或 monolithic 应用是一个单块应用,其中所有组件都被组合到一个包中。

例如,在 QuarkuShop 项目中,如果开发人员想要更新一个Product实体的定义,他们将访问与另一个开发PaymentService的开发人员相同的代码库。在这些更新之后,他们必须重新构建并重新部署应用的更新版本。

在运行时,QuarkuShop 被部署为一个包,即我们使用 CI/CD 管道打包在 Docker 容器中的原生 JAR。所有的 Java 代码和 HTML/CSS 资源(我们在src/main/resources/META-INF/resources中有一个网页)将在同一个块中运行,即使我们在 JAR 中嵌入了一些 React 或 Angular 代码。

QuarkuShop 处理 HTTP 请求,执行一些特定于域的逻辑,从数据库中检索和更新数据,并处理要发送给 REST 客户机的有效负载。

由于所有这些原因,QuarkuShop 是一个庞然大物。img/509649_1_En_7_Figa_HTML.gif

这是好事还是坏事? img/509649_1_En_7_Figb_HTML.gif

在培训和讨论期间,当我说一个应用是一个整体时,许多人认为这是一件坏事。本章描述了这种架构的优缺点,我将让您决定它是好还是坏,或者好+坏(两者都有一点)。img/509649_1_En_7_Figc_HTML.gif

单体应用的主要好处是它的简单性。只开发、部署和扩展一个模块很容易。

因为整个应用的代码库都在一个地方,所以只需要配置一个插槽来构建和部署应用。

Monoliths 非常容易工作,尤其是在开始的时候,这是开始一个新项目的默认选择。虽然复杂性会随着时间的推移而增长,但代码库的敏捷管理将有助于在单体应用的整个生命周期中保持生产力。密切关注代码是如何编写的,以及架构是如何发展的,将会保护项目不会变成一大盘意大利面条。

虽然我告诉你敏捷将保证项目保持干净和容易进行,但是没有互惠。单体应用组件是非常紧密耦合的,随着产品的发展会变得非常复杂。因此,随着时间的推移,开发人员很难管理它们。

“构建一个有效的复杂系统的方法是从非常简单的有效系统中构建它。”

—凯文·凯利

在现实世界的项目中,无论是中型还是大型项目,开发人员通常只处理应用的特定部分。每个开发人员通常只理解一个整体的一部分,这意味着很少有开发人员能够解释整个应用。由于 monoliths 必须作为一个单元来开发和部署,因此很难将开发工作分成独立的团队。每次代码更改都必须仔细协调,这会减慢开发速度。

这种情况对新开发人员来说可能很困难,他们不想处理多年来发展的大量代码库。结果,更多的时间花在寻找正确的代码行,并确保它没有副作用。同样,花在编写新特性上的时间会更少,而这些新特性将会改善应用。

在一个整体中采用新技术可能意味着重写整个应用,这是一项既费钱又费时的苦差事。

独石很受欢迎,因为它们比它们的替代品微服务更容易开始建造。

微服务架构

虽然 monolith 是一个单一的大型单元,但微服务架构使用小型的模块化代码单元,可以独立于产品的其余组件进行部署。

“简单的可比复杂的难。你必须努力让你的思维变得干净,让它变得简单。”

—乔布斯

微服务架构的优势

微服务架构允许将一个大的应用分解成小的、松散耦合的服务。这种方法带来了很多好处:

  • 我们的 QuarkuShop 应用是使用单体架构开发的,随着时间的推移,它已经增加了许多新特性。代码库是巨大的,新人正在加入我们的团队。他们很难开始使用给定的应用,因为应用的各个组件之间没有明确的界限。微服务方法允许我们将我们的应用视为小型、松散耦合的组件,因此任何人都可以在短时间内开始使用现有的应用。给代码库添加一个新的补丁不会太难。

  • 想象一下,今天是黑色星期五,这是一年中最繁忙的购物日,所以我们的网站流量很大。我们的产品目录被高度要求,这导致整个应用下降。如果这发生在微服务架构中,我们不会面临这样的故障,因为它独立运行多个服务。如果一个服务关闭,不会影响任何其他服务。

  • 我们是开发人员,花了很多时间来理解我们的应用代码库。我们对增加新功能感到兴奋,如产品搜索引擎。我们必须重新部署整个应用,以便向最终用户展示这些功能。我们将致力于许多这样的功能,想想每次部署所花费的时间。因此,如果应用很大,它需要大量的精力和时间,这肯定会导致生产力的损失。没有人喜欢等着看代码修改的结果。在微服务架构中,我们尽量使每个服务的代码库尽可能小,这样就不会在构建、部署等方面花费太多时间。

  • 今天我们的流量很大。什么都没坏,服务器也启动了。假设我们需要扩展应用的某些组件,例如产品目录。我们不能在一个整体架构中扩展单个组件。但是,在微服务架构中,我们可以通过动态添加更多节点来单独扩展任何组件。

  • 明天,我们希望迁移到新的框架或技术堆栈。很难升级一个整体,因为随着时间的推移,它已经变得非常大和复杂。更新微服务架构并不困难,因为组件小且灵活。

这就是微服务架构如何让我们的生活变得轻松。有各种各样的策略可以帮助我们将应用划分成小的服务,比如通过领域驱动的设计,通过业务能力,等等。

什么是真正的微服务?

一个微服务通常实现一组不同的特性或功能,比如订单管理、客户管理等。每个微服务都是一个迷你应用,有自己的业务逻辑和边界,比如 REST web 服务。一些微服务公开一个 API,由其他微服务或应用的客户端使用。其他微服务可能会实现 web UI。我们还可以使用消息队列进行通信。

应用的每个功能区域现在都由自己的微服务来实现。此外,web 应用被分成一组更小的 web 应用。

每个后端服务通常公开一个 REST API,大多数服务使用其他服务提供的 API。UI 服务调用其他服务来呈现网页。服务也可能使用异步的基于消息的通信。

一些 REST APIs 也暴露给不能直接访问后端服务的客户端应用。相反,通信是通过一个称为 API 网关的中介进行的。API 网关负责负载平衡、缓存、访问控制、API 计量和监控等任务。

微服务架构模式极大地影响了应用和数据库之间的关系。每个服务都有自己的数据库模式,而不是与其他服务共享一个数据库模式。一方面,这种方法与企业范围的数据模型的想法不一致。此外,它通常会导致一些数据的重复。然而,如果您想从微服务中获益,每个服务拥有一个数据库模式是必不可少的,因为它确保了松耦合。

每个服务都有一个数据库。此外,服务可以使用最适合其需求的数据库类型,即所谓的多语言持久性架构

从表面上看,微服务架构模式类似于 SOA。使用这两种方法,架构都由一组服务组成。然而,考虑微服务架构模式的一种方式是,它是 SOA,没有商业化和 Web 服务规范(WS)和企业服务总线(ESB)的包袱。基于微服务的应用更喜欢 REST 等更简单、轻量级的协议,而不是繁重的协议。他们也非常避免使用 ESB,而是在微服务本身中实现类似 ESB 的功能。

结论:进行转换

那么,有没有可能不从零开始,从单体切换到微服务架构呢?值得吗?像网飞、亚马逊和推特这样的大公司都已经从整体架构转向微服务架构,并且没有回头的意思。

改变架构可以分阶段进行。您可以从单体应用中一个接一个地提取微服务,直到完成。一个好的架构选择可以让你的应用和组织变得更好。如果你正在考虑转换,你会在接下来的章节中得到一个很好的迁移指南。

八、分裂单体:轰击领域

介绍

我们已经定义了什么是微服务架构,并讨论了它所解决的问题。我们还了解了采用微服务架构的诸多优势。但是,如何将您的单体应用迁移到微服务架构呢?如何应用这种模式?如何在不重写整个应用的情况下拆分你的 monolith?

本章回答了这些问题。我们将使用领域驱动设计作为分割 QuarkuShop 整体的方法。领域驱动设计(DDD)是一种简化软件建模和设计的软件开发方法。

什么是领域驱动设计?

领域驱动的设计有很多优点:

  • 关注核心业务领域和业务逻辑。

  • 确保设计基于领域模型的最佳方式。

  • 技术和业务团队之间的紧密合作。

要理解领域驱动设计,你需要理解它的许多概念。

语境

语境是一个特定的环境,在这个环境中,一个动作或一个术语有特定的含义。在不同的环境中,这个意思会发生变化。

领域

领域是软件开发所针对的一组知识和规范。

模型

模型是领域中参与者和组件的抽象表示。

普遍存在的语言

公司中不同角色的人对他们面临的业务问题有不同的认识。例如,如果您正在开发一个事务平台,您通常会有一个由项目经理、开发人员、开发人员、测试人员和业务分析师组成的团队。通常,业务分析师是具有业务知识的人,在这种情况下是事务,他们的角色是为不一定精通经济和金融的其他团队成员翻译业务规范和需求。

这种行话将保证所有团队成员对业务环境有相同的理解。

演讲、会议、用户故事和门票中使用的语言/行话被称为通用语言

战略设计

战略设计是领域驱动设计世界中最重要的范例之一。这有助于将总是复杂的领域分割成更小的部分。这种分裂可能是危险的,可能会改变业务逻辑中的关键概念。战略设计的力量来了:它带来了许多方法和原则,保证了主要领域的完全完整性。

您将在接下来的章节中发现战略设计的主要组成部分。

限界上下文

一个有界上下文是属于同一个业务子域的组件的逻辑集合。每个子域由一个专门的团队处理,这将优化新的开发和错误修复。加工小零件比加工大块零件容易。

一个有界的上下文有一个定义的范围,它将覆盖所有相关的模型。每个元素只需要归属于一个特定的有界上下文。从语义上来说,一个元素可以属于两个或更多的有界上下文,但是需要做出一个决定来将其归属于主有界上下文。

让我们检查一下我们一直在使用的表预订示例。当您开始设计系统时,您会看到客人会访问应用,并请求在选定的餐馆、日期和时间预订桌子。后端系统通知餐馆预订信息。类似地,假设餐馆也可以预订桌子,那么餐馆会用桌子预订来更新他们的系统。因此,当您查看系统的细微之处时,您会看到三个领域模型:

  • Product领域模型

  • Order领域模型

  • Customer领域模型

img/509649_1_En_8_Figa_HTML.jpg

它们有自己的有界上下文,您需要确保它们之间的接口工作正常。

轰炸采石场

在这里,您将学习如何将 QuarkuShop 应用分成许多步骤。

代码库

第一步是为每个有界上下文创建一个包。然后,您将把每个类移动到它相关的有界上下文包中。使用 NetBeans(和其他 ide),只需点击几下鼠标,就可以轻松地移动和重构代码。在移动代码时,您可能会发现一些您错过的新的有界上下文。

此时,保证重构不会破坏应用的唯一方法就是通过测试!

此任务是前面步骤中创建的设计的直接应用。在这个例子中,这个任务看起来很简单。但是当处理大型应用时,这将是困难的,并且需要几周甚至几个月的时间。为了优化重组操作,可以使用 Stan4j 和 Structure101 等结构分析工具。

依赖性和共有性

当您将部分代码移动到相应的包中时,您会看到一些公共类(如实用程序类)。这些常用的类必须移动到一个专用的COMMONS包中。

在这一点上,结构分析工具非常方便。

下一节将介绍域分割的最重要的元素:实体和关系。

实体

在这一层,您正在拆分源代码。因为 Java 实体被映射到 SQL 表,所以您也需要拆分数据库。

对于数据库访问代码,每个实体都有一个存储库。实体之间的关系揭示了表之间的外键关系,这代表了数据库级的约束。

在 QuarkuShop 中,一个给定的实体将属于一个特定的有界上下文。但是将实体转移到单独的包中并不是最后一步。您需要通过打破属于不同有界上下文的表之间的关系来打破 JPA 映射关系。您将通过示例了解如何在尊重应用业务逻辑完整性的同时做到这一点。

示例:断开外键关系

QuarkuShop 应用将产品信息存储在一个专用的表中,该表由 ORM 进行映射和管理。OrderItem对象将订购产品的参考和订购数量存储在一个专用表中。在OrderItem表中对Product记录的引用由一个外键约束组成。

那么怎么才能打破这种关系呢?

答案很容易。img/509649_1_En_8_Figb_HTML.gif

首先,您需要将OrderItem中的Product引用从Product改为Long,这是Product实体的主键类型。

原始代码如下:

@ManyToOne(fetch = FetchType.LAZY)
private Product product;

重构之后,看起来是这样的:

private Long product;

@ManyToOne注释是无用的,因为没有目标 JPA 实体。现在只是一个Long属性。

这一修改将限制OrderItem对产品的“知识”量。因此,当您读取OrderItem记录时,您需要在Order服务中有一个组件,它从Product服务中读取给定产品 ID 的数据。服务之间的这种通信将使用 REST 来完成,因为这是一种同步通信。

此交换在此架构中表示:

img/509649_1_En_8_Figc_HTML.png

您有两个数据库调用,而不是一个,因为这两个微服务是分开的,而您在 monolith 中只有一个调用。也许你想知道性能问题?额外的调用将花费一些额外的时间,但这并不重要。如果您在分割之前测试性能,那么您应该在进行更改时具有可见性。

因为外键关系已从代码中删除,所以 SQL 数据库中没有约束。我知道许多数据库管理员对数据完整性不满意。别难过,伙计们!您总是可以开发一个验证批次,确保数据是构建良好的,并且不受约束地不被破坏。删除一个Product也需要考虑这一点。您需要想出一个关于存储在OrderItem记录中的Productid 的策略。

每个微服务都可以自由地存储它的表。它可以使用所有微服务共享的相同数据库/模式,或者您甚至可以为每个微服务创建一个专用的数据库服务器实例。

结论

在这一章中,你学习了如何根据领域驱动的设计原则来拆分 QuarkuShop 应用。每个微服务代表一个有界的上下文。这项任务是巨大的,但是循序渐进地做将会减少达到目标所需的努力。

微服务在那里。您只需要在生产环境中解决它们,在那里它们将被消费。在下一章中,您将发现部署的世界。

九、将 DDD 应用于代码

在前一章中,你学习了如何使用领域驱动设计(DDD)将你的领域分割成有界的上下文。

在这一章中,你将学习如何使用上一章定义的切割线来分割你的代码。

将有界上下文应用于 Java 包

我们的类已经按照组件类型进行了分类,比如存储库服务等。

我们将把每个组件移动到一个新的包中,这个包包含了绑定的上下文名称。每个组件的名称具有以下格式:

img/509649_1_En_9_Figa_HTML.png

重构后,命名格式将为:

img/509649_1_En_9_Figb_HTML.png

重命名所有组件后,您将得到如下所示的项目树:

img/509649_1_En_9_Figc_HTML.jpg

等等!您可能想知道commonsconfiguration包包含什么。让我们现在讨论一下。

公地一揽子计划的诞生

回想一下,公共组件必须被收集到一个专用的commons包中,该包将由所有有界上下文共享。以下是我的commons包里的东西:

img/509649_1_En_9_Figd_HTML.jpg

请注意,这些类由配置和实用程序类组成。可以把它们想象成 Apache Commons 库,你可以在几乎每个 Java 项目中直接找到,或者通过某个依赖项间接获得。这个commons库由四个包组成:

  • config:包含用于配置 OpenAPI 实现和 Swagger UI 的OpenApiConfig

  • health:包含自定义键盘锁健康检查。

  • security:包含用于抓取 JWT 令牌的TokenService

  • utils:包含为测试提供 Keycloak 和 PostgreSQL 的实用程序类。

img/509649_1_En_9_Fige_HTML.gif请注意,所有组件都需要这些实用程序类,对于给定的微服务没有特定的风格。但是你可以随时去掉commons库。

我有一个在commons项目中分享 dto 的坏习惯。但是,我们伟大的技术评论家圣乔治·安德里亚纳基斯建议我停止这样做!我考虑了一下,我同意这是一个坏主意,即使对于概念验证项目也是如此。

img/509649_1_En_9_Figf_HTML.gif我们在讲微服务的时候说过,拆分之后,微服务之间可以互相对话。所以,想象一下这种情况:Order想要使用给定的Product IDProduct微服务获得一个ProductProduct中的 REST API 将返回一个由Product记录的数据填充的ProductDTO对象,该对象将被序列化为 JSON 并返回给请求服务Order。在这个层次上,每个微服务都有一个Product的定义。基于许多元素,如项目规范、文档,甚至通过 OpenAPI 资源,不同的微服务可以轻松地进行通信。如果 dto 在commons库中共享,我们可能会失去微服务的一个好处:摆脱紧耦合。

定位有界上下文关系

这一步旨在打破有界上下文(BCs)之间的依赖关系。为了能够打破它们,您需要首先找到这些依赖关系。有很多方法可以找到他们。例如,通过查看类图、源代码等。在前一章中,我们谈到了可以帮助你突出你的 monolith 块之间的依赖关系的工具。

我主要用的是 STAN (STAN4J),一个强大的 Java 结构分析工具。STAN 支持一套精心选择的度量标准,适合于覆盖结构质量的最重要的方面。特别关注视觉依赖分析,这是结构分析的关键。

STAN 有两种型号:

  • 作为 Windows 和 macOS 的独立应用,面向通常不使用 IDE 的架构师和项目经理。

  • 作为 Eclipse 集成开发环境(IDE)的扩展,它允许开发人员快速探索任何代码束的结构。

我们将使用第二种选择。我获得了 Eclipse IDE 的全新安装,并按照 http://stan4j.com/download/ide/ 中的描述安装了 IDE 扩展。

我们的 monolith 是一个基于 Maven 的项目,所以它可以很容易地导入到 Eclipse IDE 中。导入后,只需遵循以下步骤:

  1. 右键单击该项目。

  2. 选择作为➤ Maven Build 运行。

  3. 选择 Maven 配置,如果您已经有一个的话。

  4. 在目标中,只需输入clean install -DskipTests并选择运行。

  5. 你完蛋了!

接下来,创建项目的结构分析:

  1. 右键单击该项目。

  2. 选择“作为➤结构分析运行”。

  3. 耶!img/509649_1_En_9_Figg_HTML.gif结构图在这里!

img/509649_1_En_9_Figh_HTML.jpg

打破 BC 关系

现在,您将打破有界上下文之间的关系。您可以从实体之间的关系开始。通常,数据库中的表之间的关系更有效。这里,实体类被视为关系表(JPA 概念),因此实体类之间的关系如下:

  • @ManyToOne关系

  • @OneToMany关系

  • @OneToOne关系

  • @ManyToMany关系

更准确地说,您不会破坏属于同一个 BC 的实体之间的关系,但是会破坏 BC 间的关系。例如,考虑属于Order ContextOrderItem和属于Product ContextProduct之间的关系。

在 Java 中,这种关系由以下内容表示:

public class OrderItem extends AbstractEntity {
    @NotNull
    @Column(name = "quantity", nullable = false)
    private Long quantity;

    @ManyToOne
    private Product product;

    @ManyToOne
    private Order order;
}

注释product字段的@ ManyToOne是这里的目标。

你怎么打破这种关系?很简单,这个街区:

@ManyToOne
private Product product;

将被更改如下:

private Long productId;

所以,你可能想知道为什么我们用Long类型替换Product类型?很简单。LongProduct's ID 的类型。

太好了。如果OrderItemProduct之间的关系是双向关系,你必须对Product类做同样的事情。

当您尝试使用mvn clean install构建项目时,会遇到编译问题。这是显而易见的,因为您编辑了OrderItem,所以许多使用这个类的组件必须知道这些修改。在这里,OrderItemService才是问题所在。

让我们看看OrderItemService类中有错误的块。这是第一个:

public OrderItemDto create(OrderItemDto orderItemDto) {
    log.debug("Request to create OrderItem : {}", orderItemDto);

    var order = this.orderRepository
          .findById(orderItemDto.getOrderId())
          .orElseThrow(() -> new IllegalStateException("The Order does not exist!"));

    var product = this.productRepository
          .findById(orderItemDto.getProductId())
          .orElseThrow(() -> new IllegalStateException("The Product does not exist!"));

    return mapToDto(
        this.orderItemRepository.save(
            new OrderItem(
                    orderItemDto.getQuantity(),
                    product,
                    order
            )));
}

我们从ProductRepository中获取一个Product实例,并将它传递给OrderItem构造函数。在OrderItem类中不再有Product字段(您将其改为Product ID ) ,因此您需要将 ID 传递给OrderItem构造函数,而不是Product

因此,Product对象不再存在于Order有界上下文中。现在怎么处理ProductProductRepository这两个职业?

你忘了吗?在Order有界上下文的范围内,我们有commons模块,它包含了ProductDTO,我们可以免费使用!

对于ProductRepository,显而易见。它将被一个 REST 客户端取代,该客户端将从Product微服务收集Product数据,并在需要时填充已知的ProductDTO对象。但是在这里,我们只需要用Product ID来创建OrderItem实例。所以,在这种情况下,我们不需要获取Product记录。

第二个块是将OrderItem映射到OrderItemDto的方法:

public static OrderItemDto mapToDto(OrderItem orderItem) {
        return new OrderItemDto(
                orderItem.getId(),
                orderItem.getQuantity(),
                orderItem.getProductId(),
                orderItem.getOrder().getId()
        );
    }

必须从这个方法中删除对Product的引用,因为它已经从OrderItem类中删除了。我们没有产品作为成员,但是我们有OrderItem类中的Product ID。所以orderItem.getProduct().getId()指令必须改为orderItem.getProductId()。为了从订单总价中增加/减去产品价格,我们在OrderItemService中注入ProductRepository

产生的OrderItemService将如下所示:

@Slf4j
@ApplicationScoped
@Transactional
public class OrderItemService {

    @Inject OrderItemRepository orderItemRepository;
    @Inject OrderRepository orderRepository;
    @Inject ProductRepository productRepository;

    public static OrderItemDto mapToDto(OrderItem orderItem) {
        return new OrderItemDto(orderItem.getId(),
                orderItem.getQuantity(), orderItem.getProductId(),
                orderItem.getOrder().getId());
    }

    public OrderItemDto findById(Long id) {
        log.debug("Request to get OrderItem : {}", id);
        return this.orderItemRepository.findById(id)
                    .map(OrderItemService::mapToDto).orElse(null);
    }

    public OrderItemDto create(OrderItemDto orderItemDto) {
        log.debug("Request to create OrderItem : {}", orderItemDto);
        var order = this.orderRepository
                .findById(orderItemDto.getOrderId()).orElseThrow(() ->
                    new IllegalStateException("The Order does not exist!"));

        var orderItem = this.orderItemRepository.save(
                new OrderItem(orderItemDto.getQuantity(),
                        orderItemDto.getProductId(), order
                ));

        var product = this.productRepository.getOne(orderItem.getProductId());

        order.setPrice(order.getPrice().add(product.getPrice()));
        this.orderRepository.save(order);

        return mapToDto(orderItem);
    }

    public void delete(Long id) {
        log.debug("Request to delete OrderItem : {}", id);

        var orderItem = this.orderItemRepository.findById(id)
                .orElseThrow(() ->
                    new IllegalStateException("The OrderItem does not exist!"));

        var order = orderItem.getOrder();
        var product = this.productRepository.getOne(orderItem.getProductId());

        order.setPrice(order.getPrice().subtract(product.getPrice()));

        this.orderItemRepository.deleteById(id);

        order.getOrderItems().remove(orderItem);
        this.orderRepository.save(order);
    }

    public List<OrderItemDto> findByOrderId(Long id) {
        log.debug("Request to get all OrderItems of OrderId {}", id);
        return this.orderItemRepository.findAllByOrderId(id)
                .stream()
                .map(OrderItemService::mapToDto)
                .collect(Collectors.toList());
    }
}

在这个重构之后,您可以使用 STAN 这个伟大的工具来看看这些修改是如何改变项目的结构的。

经过一些检查后,剩下要做的修改是:

  • order包装中:

    1. Order:private Cart cart;将变为private Long cartId;

    2. OrderService:Cart参考将变为CartDto

  • customer包装中:

    1. Cart:第private Order order;会改成private Long orderId

    2. CartService:Order参考将变为OrderDto

在这些修改之后,我们在CartService中仍然有一个OrderService引用。如前所述,这个OrderService将被一个 REST 客户端取代,该客户端将调用由Order微服务公开的 API。在分析结构之前,您可以引用OrderService来注释这些行。

在这次重构之后,以下是这些修改如何改变了项目的结构:

img/509649_1_En_9_Figi_HTML.jpg

img/509649_1_En_9_Figj_HTML.gif你没有完全切断orderproduct包之间的联系。这是因为,要计算订单总额,您需要有产品价格。您将在以后的步骤中看到如何处理这个链接。

结论

这很好!您刚刚完成了迁移过程中最繁重的任务之一!

接下来,您可以开始构建独立的微服务。在此之前,您需要了解更多关于最佳实践和微服务模式的信息。

十、满足微服务的需求和模式

当我们处理架构和设计时,我们立即开始考虑配方和模式。这些设计模式对于在云中构建可靠、可伸缩、安全的应用非常有用。

一个模式是一个在特定环境中出现的问题的可重用解决方案。这是一个源于现实世界架构的想法,已经被证明在软件架构和设计中是有用的。

实际上,微服务架构是最强大的架构模式之一。我们在第八章中详细讨论了这种模式。

当呈现一个模式时,我们从定义上下文和问题开始,然后提供模式给出的解决方案。

云图案

本章讨论这些模式:

  • 外部化配置

  • 服务发现和注册

  • 断路器

  • 每个服务的数据库

  • API 网关

  • CQRS(消歧义)

  • 活动采购

  • 日志聚合

  • 分布式跟踪

  • 审核日志记录

  • 应用指标

  • 运行状况检查 API

服务发现和注册

背景和问题

在微服务领域,服务注册和发现扮演着重要的角色,因为您很可能会运行多个服务实例,并且需要一种机制来调用其他服务,而无需硬编码它们的主机名或端口号。除此之外,在云环境中,服务实例可能随时增加和减少。所以你需要一个自动的服务注册和发现机制。

在一个单一的应用中,组件之间的调用是通过语言级别的调用进行的。但是,在微服务架构中,服务通常需要使用 REST(或其他)调用来相互调用。为了发出请求,服务需要知道给定服务实例的网络位置(IP 地址和端口)。如前几章所述,微服务会动态分配网络位置。这是由许多因素造成的,例如不同的部署频率。此外,一个服务可以有多个实例,由于自动伸缩、失败等原因,这些实例可以动态地保持变化。

因此,您必须实现一种机制,使给定服务的客户端能够向一组动态变化的服务实例发出请求。

服务的客户端如何发现服务实例的位置?服务的客户端如何知道服务的可用实例?

解决办法

您可以创建服务注册中心,它是可用服务实例的数据库。它是这样工作的:

  • 微服务实例的网络位置在实例启动时向服务注册中心注册。

  • 当实例终止时,微服务实例的网络位置将从服务注册表中删除。

  • 微服务实例的可用性通常使用心跳机制定期刷新。

这种模式有两种类型:

  • 客户端发现模式:请求服务(客户端)负责寻找可用服务实例的网络位置。客户端查询服务注册中心,然后使用一些负载平衡算法选择一个可用的服务实例并发出请求。这种机制遵循客户端发现模式。

  • 服务器端发现模式:服务器端发现模式建议客户端通过负载均衡器向服务发出请求。负载平衡器负责查询服务注册中心,并将每个请求转发给可用的服务实例。因此,如果您想要遵循这种模式,您需要拥有(或实现)一个负载平衡器。

外部化配置

背景和问题

应用通常使用一个或多个基础设施(消息代理和数据库服务器等。)和第三方服务(支付网关、电子邮件和消息等)。).这些服务需要配置信息(例如凭证)。此配置信息存储在与应用一起部署的文件中。

在某些情况下,可以在部署后编辑这些文件来改变应用的行为。但是,更改配置需要重新部署应用,这通常会导致不可接受的停机时间和其他管理开销。

本地配置文件还将配置限制到单个应用,有时在多个应用之间共享配置设置会很有用。示例包括数据库连接字符串以及相关应用集使用的队列和存储的 URL。

我们的整体被分成许多微服务。所有这些微服务都需要提供给 monolith 的配置信息。假设我们需要更新数据库的 URL。所有微服务都需要完成此任务。如果我们忘记在某个地方更新数据,可能会导致在部署更新时实例使用不同的配置设置。

解决办法

将所有应用配置具体化,包括数据库凭证和网络位置。例如,您可以在外部存储配置信息,并提供一个可用于快速有效地读取和更新配置设置的界面。您可以将这个配置存储称为配置服务器。

当微服务启动时,它从给定的配置服务器中读取配置。

断路器

背景和问题

微服务主要使用 HTTP REST 请求进行通信。当一个微服务与另一个同步时,总是存在另一个服务由于高延迟而不可用或不可达的风险,这意味着它本质上是不可用的。这些不成功的调用可能会导致资源耗尽,这将使调用服务无法处理其他请求。一个服务的失败可能会影响整个应用中的其他服务。

解决办法

发出请求的微服务应该通过代理调用远程服务,代理的工作机制类似于电路断路器。当连续失败的次数超过阈值时,断路器跳闸,并且在超时期间,所有调用远程服务的尝试都将立即失败。超时后,断路器允许有限数量的测试请求通过。如果这些请求成功,断路器恢复正常运行。如果出现故障,超时时间将重新开始。

断路器模式,由迈克尔·尼加德在他的书中推广开来!,可以防止应用反复尝试执行可能失败的操作。这允许应用继续运行,而无需等待故障被修复或浪费 CPU 周期,同时确定故障是长期持续的。断路器模式还使应用能够检测故障是否已经解决。如果问题似乎已经解决,应用可以尝试调用操作。

每个服务的数据库

背景和问题

在微服务架构世界中,服务必须是松散耦合的,这样它们才能独立开发、部署和扩展。

大多数服务需要将数据保存在某种数据库中。在我们的应用中,Order Service存储订单信息,Customer Service存储客户信息。

微服务应用中的数据库架构是什么?

解决办法

将每个微服务的持久数据保持为该服务私有,并且只能通过其 API 访问。

该服务的数据库实际上是该服务实现的一部分。其他服务不能直接访问它。

有几种不同的方法可以使服务的持久数据保持私有。您不需要为每个服务提供一个数据库服务器。例如,如果您使用关系数据库,选项如下:

  • 每个服务拥有一组只能由该服务访问的表。

  • 每个服务都有一个专用于该服务的数据库模式。

  • 每个服务都有自己的数据库服务器。

Private-tables-per-serviceschema-per-service开销最低。为每个服务使用一个模式很有吸引力,因为它使所有权更加清晰。一些高吞吐量服务可能需要自己的数据库服务器。

创建壁垒来加强这种模块化是一个好主意。例如,您可以为每个服务分配不同的数据库用户 ID,并使用数据库访问控制机制,比如 grants。如果没有某种强制封装的障碍,开发人员总是会试图绕过服务的 API,直接访问其数据。

应用编程接口网关

背景和问题

对于 QuarkuShop 精品店,假设您正在实现产品详细信息页面。假设您需要开发多个版本的产品详细信息用户界面:

  • 基于 HTML5/JavaScript 的桌面和移动浏览器 UI:HTML 由服务器端 web 应用生成。

  • 本地 Android 和 iPhone 客户端:这些客户端通过 REST APIs 与服务器交互。

此外,QuarkuShop 必须通过 REST API 公开产品细节,供第三方应用使用。

产品详细信息 UI 可以显示关于产品的大量信息。例如:

  • 产品的基本信息,如名称、描述、价格等。

  • 您的产品购买历史。

  • 可用性。

  • 购买期权。

  • 经常与本产品一起购买的其他物品。

  • 购买该产品的顾客购买的其他物品。

  • 顾客评论。

因为 QuarkuShop 遵循微服务架构模式,所以产品细节数据分布在多个服务上:

  • 产品服务:产品的基本信息,如名称、描述、价格、客户评论和产品可用性。

  • 订单服务:产品的购买历史。

  • QuarkuShop: 顾客、推车等。

因此,显示产品详细信息的代码需要从所有这些服务中获取信息。

基于微服务的应用的客户端如何访问单个服务?

解决办法

实现一个 API 网关,它是所有客户端的单一入口点。API 网关以两种方式之一处理请求。一些请求被简单地代理/路由到适当的服务。它通过分散到多个服务来处理其他请求。

API 网关可以为每个客户端提供不同的 API,而不是提供一种通用的 API。API 网关还可以实现安全性,例如验证客户端是否被授权执行请求。

CQRS(消歧义)

背景和问题

在传统的数据管理系统中,命令(数据更新)和查询(数据请求)都是针对单个数据存储库中的同一组实体执行的。这些实体可以是关系数据库(如 SQL Server)中一个或多个表中行的子集。

通常在这些系统中,所有创建、读取、更新和删除(CRUD)操作都应用于实体的相同表示。例如,代表客户的数据传输对象(DTO)由数据访问层(DAL)从数据存储中检索并显示在屏幕上。用户更新 DTO 的一些字段(可能通过数据绑定),然后 DAL 将 DTO 保存回数据存储中。相同的 DTO 用于读取和写入操作。

当只有有限的业务逻辑应用于数据操作时,传统的 CRUD 设计工作得很好。开发工具提供的支架机制可以非常快速地创建数据访问代码,然后可以根据需要定制这些代码。

然而,传统的 CRUD 方法有一些缺点:

  • 这通常意味着数据的读写表示之间存在不匹配,例如必须正确更新的附加列或属性,即使它们不是操作的一部分。

  • 当记录被锁定在协作域中的数据存储中时,存在数据争用的风险,在协作域中,多个参与者对同一组数据进行并行操作。当使用乐观锁定时,由并发更新引起的更新冲突也是一个问题。随着系统复杂性和吞吐量的增加,这些风险也会增加。此外,由于数据存储和数据访问层的负载,以及检索信息所需的复杂查询,传统方法可能会对性能产生负面影响。

  • 这可能会使管理安全性和权限变得更加复杂,因为每个实体都受到读和写操作的影响,这可能会在错误的上下文中公开数据。

解决办法

命令和查询责任分离(CQRS)是一种模式,它通过使用不同的接口将读取数据(查询)的操作与更新数据(命令)的操作分离开来。这意味着用于查询和更新的数据模型是不同的。然后可以隔离这些模型。

与基于 CRUD 的系统中使用的单一数据模型相比,在基于 CQRS 的系统中为数据使用单独的查询和更新模型简化了设计和实现。然而,一个缺点是,与 CRUD 设计不同,CQRS 代码不能使用支架机制自动生成。

用于读取数据的查询模型和用于写入数据的更新模型可以访问同一个物理存储,这可能是通过使用 SQL 视图或动态生成投影来实现的。

读取存储可以是写入存储的只读副本,或者读取和写入存储可以具有完全不同的结构。使用读取存储的多个只读副本可以大大提高查询性能和应用 UI 响应能力,尤其是在只读副本位于应用实例附近的分布式方案中。

活动采购

背景和问题

大多数应用处理数据,典型的方法是应用通过在用户处理数据时更新数据来维护数据的当前状态。例如,在传统的创建、读取、更新和删除(CRUD)模型中,典型的数据流程是从存储中读取数据,对其进行一些修改,并用新值更新数据的当前状态—通常是通过使用锁定数据的事务。

CRUD 方法有一些限制:

  • CRUD 系统直接对数据存储执行更新操作,这会降低性能和响应速度,并限制可伸缩性,因为这需要处理开销。

  • 在有许多并发用户的协作域中,数据更新冲突更有可能发生,因为更新操作发生在单个数据项上。

  • 除非有额外的审计机制在单独的日志中记录每个操作的细节,否则历史就会丢失。

解决办法

Event Sourcing 模式定义了一种处理由一系列事件驱动的数据操作的方法,每个事件都记录在一个只加存储中。应用代码将一系列事件发送到事件存储区,这些事件强制性地描述了数据上发生的每个操作,事件存储区保存了这些事件。每个事件代表一组数据的变化(比如AddedItemToOrder)。

事件保存在事件存储中,事件存储充当有关数据当前状态的记录系统(权威数据源)。事件存储通常会发布这些事件,以便用户可以得到通知,并在需要时处理它们。例如,消费者可以启动将事件中的操作应用到其他系统的任务,或者执行完成操作所需的任何其他相关操作。请注意,生成事件的应用代码与订阅事件的系统是分离的。

事件存储发布的事件的典型用途是在应用中的操作改变实体的物化视图时维护它们,以及与外部系统集成。例如,系统可以维护所有客户订单的物化视图,该视图用于填充部分 UI。当应用添加新订单、添加或删除订单上的项目以及添加运输信息时,描述这些更改的事件可以被处理并用于更新物化视图。

此外,在任何时候,应用都可以读取事件的历史,并通过回放和使用与该实体相关的所有事件来使用它来具体化该实体的当前状态。这可以根据需要在处理请求时具体化一个域对象,也可以通过一个调度任务来实现,以便实体的状态可以存储为一个物化视图来支持表示层。

日志聚合

背景和问题

在微服务架构中,我们的应用由运行在不同服务器和位置上的多个服务和服务实例组成。请求经常跨越多个服务实例。

当我们使用 monolith 时,应用生成一个日志流,它通常存储在一个日志文件/目录中。现在,每个服务实例都生成自己的日志文件。

当日志以这种方式分割时,如何识别应用的行为并解决问题?

解决办法

使用集中的日志记录服务,该服务聚合来自每个服务实例的日志。当日志被聚集时,用户可以搜索和分析日志。他们可以配置当日志中出现某些消息时触发的警报。

分布式跟踪

背景和问题

在微服务架构中,请求通常跨越多个服务。每个服务通过执行一个或多个操作来处理请求,例如数据库查询、消息发布等。

当请求失败时,您如何识别行为并解决问题?

解决办法

具有以下代码的仪表服务:

  • 为每个外部请求分配一个唯一的外部请求 ID

  • 将外部请求 ID 传递给处理请求所涉及的所有服务

  • 在所有日志消息中包含外部请求 ID

  • 记录关于在集中式服务中处理外部请求时所执行的请求和操作的信息(例如,开始时间和结束时间)

审核日志记录

背景和问题

在微服务架构中,除了日志系统之外,我们还需要更多关于服务的可见性,以监控事情的进展。

您如何监控用户和应用的行为并解决任何问题?

解决办法

您可以在数据库或一些特殊的专用日志系统中记录用户活动。

应用指标

背景和问题

在微服务架构中,除了我们已经拥有的指标之外,我们还需要更多关于服务的可见性,以了解正在发生的事情。

你如何识别和阐明一个应用的行为?

解决办法

推荐的解决方案是拥有一个集中的度量服务,该服务收集和存储每个服务操作的决策支持统计信息。微服务可以将它们的度量信息推送到度量服务。另一方面,指标服务可以从微服务中提取指标。

运行状况检查 API

背景和问题

监控 web 应用和后端服务是一个很好的实践,通常也是一个业务需求,目的是确保它们可用并正常运行。然而,监控云中运行的服务比监控内部服务更困难。有许多因素会影响应用,例如网络延迟、底层计算和存储系统的性能和可用性以及网络带宽。这些因素中的任何一个都可能导致服务完全或部分失败。因此,您必须定期验证服务是否正常运行,以确保所需的可用性级别。

解决办法

通过向应用上的端点发送请求来实现健康监控。应用应该执行必要的检查,并返回其状态指示。

健康监控检查通常结合了两个因素:

  • 应用或服务为响应对健康验证端点的请求而执行的检查(如果有)。

  • 通过执行运行状况验证检查的工具或框架对结果进行分析。

响应代码指示应用的状态,并且可选地指示它使用的任何组件或服务的状态。延迟或响应时间检查由监控工具或框架执行。

服务之间的安全性:访问令牌

背景和问题

在微服务架构中,通过使用 API 网关模式,应用由许多服务组成。API 网关是客户端请求的单一入口点。它对请求进行身份验证,并将它们转发给其他服务,这些服务可能会调用其他服务。

如何将请求者的身份传达给处理请求的服务?

解决办法

API 网关对请求进行认证,并将访问令牌(例如,JSON web 令牌)传递给服务,该令牌安全地标识每个请求中的请求者。服务可以在向其他服务发出的请求中包含访问令牌。

结论

既然您已经拆分了 QuarkuShop monolith 并了解了一些有用的模式,那么您可以开始构建独立的微服务了。

十一、Kubernetes 入门

介绍

为了部署我们的(如此庞大的)应用,img/509649_1_En_11_Figa_HTML.gif我们将使用img/509649_1_En_11_Figb_HTML.gif Docker。我们将在一个容器中部署我们的代码,因此我们可以享受由img/509649_1_En_11_Figc_HTML.gif Docker 提供的强大功能。

Docker 已经成为开发和运行容器化应用的标准。

使用img/509649_1_En_11_Fige_HTML.gif Docker 非常简单,尤其是在开发阶段。在同一个服务器中部署容器(docker-machine)很简单,但是当您需要将许多容器部署到许多服务器时,事情就变得复杂了(管理服务器、管理容器状态等等。).

这就是编排系统发挥作用的时候,它提供了许多出色的功能:

  • 在运行的任务之间协调资源

  • 基于许多因素,如资源需求、相似性要求等,调度容器并使其与机器匹配。

  • 处理复制

  • 处理故障

对于本教程,我们使用img/509649_1_En_11_Figf_HTML.gif Kubernetes,容器编排之星。

什么是 Kubernetes?

Kubernetes(又名 K8s)是一个从 Google 分离出来的项目,是一个开源的下一代容器调度器。它是利用从开发和管理博格和欧米茄中学到的经验设计的。

img/509649_1_En_11_Figi_HTML.jpg

Kubernetes 被设计成具有松散耦合的组件,以部署、维护和扩展应用为中心。K8s 抽象了节点的底层基础设施,并为部署的应用提供了统一的层。

永恒的建筑

Kubernetes 集群由两项组成:

  • 主节点:Kubernetes 的主控制平面。它包含一个 API 服务器、一个调度器、一个控制器管理器(K8s 集群管理器)和一个用于保存集群状态的数据存储库Etcd

  • Worker node :运行 pod 的单个主机,物理或虚拟机。它由主节点管理。

让我们看看主节点的内部:

  • Kube API-Server :允许通过 REST APIs,在主节点和它的客户端之间进行通信,例如工作节点、kube-cli等。

  • Kube 调度器(Kube Scheduler):调度器就像一个餐厅服务员,根据特定的逻辑(也就是一个策略)给你分配“第一个可用”的桌子。Kube 调度器根据特定的策略将新成员分配到最合适的节点。

  • Kube 控制器管理器:一个永久运行的进程,负责确保 Kubernetes 集群状态与管理员请求的状态相同。如果管理员发出了一些配置命令,Kube 控制器管理器负责验证这些命令是否应用于整个集群。

  • Etcd:Kubernetes 用来保存集群配置的键值数据库。

让我们看看 worker 节点的内部:

  • Kubelet :在集群中每个节点上运行的代理。它确保节点在主节点中正确注册,并验证 POD 运行正常。

  • Kube-Proxy :集群中每个节点上运行的代理。它确保网络规则在节点中正确应用。

img/509649_1_En_11_Figj_HTML.jpg

img/509649_1_En_11_Figk_HTML.gif我们使用的容器运行时是 Docker。Kubernetes 兼容很多其他的,比如cri-orkt等。

不可思议的核心概念

K8s 生态系统涵盖了很多概念和组件。下一节将简要讨论它们。

库布特雷

kubectl是一个 CLI,用于在已配置的 Kubernetes 集群上执行命令。

一个集群是集合了它们的资源(CPU、RAM、磁盘等)的主机的集合。)放入一个公共的可用池中,然后由集群资源共享(和控制)。

命名空间

名称空间想象成一个用来保存不同种类资源的文件夹。这些资源可以被一个或多个用户使用。

要列出所有的名称空间,可以使用kubectl get namespacekubectl get ns命令。

标签

标识和选择相关对象集的键值对。标签有严格的语法和定义的字符集。

豆荚

一个 pod 是 Kubernetes 的基本工作单元。pod 代表共享资源(如 IP 地址和存储)的容器集合。见清单 11-1 。

apiVersion: v1
kind: Pod
metadata:
  name: example-pod
  labels:
    app: example
spec:

  containers:
  - name: example-container
    image: busybox
    command: ['sh', '-c', 'echo Hello World :) !']

Listing 11-1Pod Example

要列出所有窗格,运行kubectl get podkubectl get po命令。

replication set-复制集

该组件负责在任何给定时间运行所需数量的副本容器。参见清单 11-2 。

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: mongodb
  labels:
    app: mongodb
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
      - name: mongodb
        image: mongo:4.4
        imagePullPolicy: Always

Listing 11-2ReplicaSet Example

要列出所有副本集,请运行kubectl get replicasetkubectl get rs命令。

部署

这包括 pod 模板和副本字段。Kubernetes 将确保实际状态(副本的数量和 pod 模板)总是与期望的状态相匹配。当您更新部署时,它将执行“滚动更新”参见清单 11-3 。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80

Listing 11-3Deployment Example

要列出所有部署,使用kubectl get deployment命令。

有状态任务

该组件负责管理必须保持或维护状态的 pod。包括主机名、网络和存储在内的 pod 标识将被保留。参见清单 11-4 。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "my-storage-class"
      resources:
        requests:
          storage: 1Gi

Listing 11-4StatefulSet Example

要列出所有状态集,运行kubectl get statefulset命令。

达蒙塞特

该组件在集群中的所有(或部分)节点上创建每个 pod 的实例。参见清单 11-5 。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule
      containers:
        - name: fluentd-elasticsearch
          image: gcr.io/google-containers/fluentd-elasticsearch:1.20
      terminationGracePeriodSeconds: 30

Listing 11-5DaemonSet Example

要列出所有 DaemonSets,请运行kubectl get daemonsetkubectl get ds命令。

服务

这定义了一个 IP/端口组合,提供对一组 pod 的访问。它使用标签选择器将多组 pod 和端口映射到一个集群独有的虚拟 IP。参见清单 11-6 。

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    run: my-nginx

Listing 11-6Service Example

要列出所有服务,运行kubectl get servicekubectl get svc命令。

进入

入口控制器是向外界公开集群服务(通常是http)的主要方法。这些是负载平衡器或路由器,通常提供 SSL 终端、基于名称的虚拟主机等。见清单 11-7 。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - http:
        paths:
          - path: /testpath
            backend:
              service:
                name: test
                port:
                  number: 80

Listing 11-7Ingress Example

要列出所有入口,请使用kubectl get ingress

这表示与 pod 生命周期相关的存储,可由 pod 中的一个或多个容器使用。

持久卷

持久卷(PV)代表一种存储资源。PVs 通常链接到后备存储资源,如 NFS、GCEPersistentDisk、RBD 等。、和是提前配置的。它们的生命周期独立于 pod 进行处理。

要列出所有持久卷,请运行kubectl get persistentvolumekubectl get pv命令。

PersistentVolumeClaim

PersistentVolumeClaim (PVC)是满足一组要求的存储请求。它通常用于动态调配的存储。

要列出所有 PersistentVolumeClaims,请使用kubectl get persistentvolumeclaimkubectl get pvc命令。

存储类

存储类是对外部存储资源的抽象。它们包括置备程序、置备程序配置参数和 PV reclaimPolicy

要列出所有存储类,请运行kubectl get storageclasskubectl get sc

职位

作业控制器确保一个或多个 pod 被执行并成功终止。它将这样做,直到它满足完成和/或并行条件。

要列出所有作业,运行kubectl get job

克朗乔布

作为作业控制器的扩展,CronJob 提供了一种按照类似 cron 的时间表执行作业的方法。

要列出所有 CronJobs,运行kubectl get cronjob

ConfigMap(配置地图)

ConfigMap 是存储在 Kubernetes 中的外部化数据,可以作为命令行参数或环境变量引用,或者作为文件注入到卷挂载中。它们是实现外部配置存储模式的理想选择。

要列出所有配置图,运行kubectl get configmapkubectl get cm

秘密

功能上与 ConfigMaps 相同,但存储编码为 base64,静态加密(如果已配置)。

要列出所有秘密,运行kubectl get secret

在本地运行 Kubernetes

对于本教程,我们不会构建一个真正的 Kubernetes 集群。我们使用 Minikube。

Minikube 用于在本地运行单节点 Kubernetes 集群,没有任何痛苦。这个工具非常有用,尤其是对于开发。

如需安装 Minikube,请访问 https://github.com/kubernetes/minikube

安装完成后,运行以下命令启动 Minikube:

minikube start

当运行 Minikube 时,一个新的 Kubernetes 上下文被创建并可用。minikube start命令创建一个名为minikubekubectl context

Kubernetes 上下文包含与 Kubernetes 集群通信所需的配置。

要访问 Kubernetes 仪表板,请运行:

minikube dashboard

仪表板将在您的默认浏览器中打开,如下所示:

img/509649_1_En_11_Figm_HTML.jpg

这些minikube命令特别有用:

  • minikube stop:停止 K8s 集群并关闭 Minikube 虚拟机,而不会丢失集群内容。

  • minikube delete:删除 K8s 集群和 Minikube 虚拟机。

您可以使用以下命令为minikube分配资源:

minikube config set memory 8192                 ①
minikube config set cpus 4                      ②
minikube config set kubernetes-version 1.16.2   ③
minikube config set vm-driver kvm2              ④
minikube config set container-runtime crio      ⑤

每行定义:

  • ①分配的内存为 8192MB (8GB)

  • ②分配给 4 个 CPU

  • ③kubler 版本至 1.16.2

  • ④虚拟机驱动程序到kvm2。要了解更多关于司机的信息,请访问 https://minikube.sigs.k8s.io/docs/drivers/

  • ⑤容器运行时使用crio而不是 Docker(默认选择)

img/509649_1_En_11_Fign_HTML.gif在设置新配置之前,您需要删除当前的minikube集群实例。

要检查配置是否正确保存,只需运行minikube config view:

$ minikube config view

- cpus: 4
- kubernetes-version: 1.16.2
- memory: 8192
- vm-driver: kvm2
- container-runtime: crio

实践总结和结论

Kubernetes 是市场上使用最广泛的容器 orchestrator。许多解决方案现在都基于它,比如 OpenShift 容器平台和 Rancher Kubernetes 引擎。Kubernetes 成为了云原生架构和基础设施的标准。几乎所有的云提供商都管理 Kubernetes 托管以下内容:

  • 蓝色忽必烈服务

  • 亚马逊弹性库柏服务

  • 谷歌库比厄引擎

  • IBM 云库服务

  • 用于 Kubernetes 的 Oracle 容器引擎

  • 阿里巴巴容器服务公司

甚至 OVH 和数字海洋也加入了提供托管 Kubernetes 托管解决方案的竞赛。img/509649_1_En_11_Figp_HTML.gif

如本章第一部分所列,有许多 Kubernetes 对象。每一种都可以用来满足特定的需求。在下一节中,我们将看看这些对象如何满足我们的需求。

img/509649_1_En_11_Figq_HTML.jpg

QuarkuShop 被打包成一个 Docker 容器。在 Kubernetes 中,我们的应用将在 pod 对象中运行,这是最基本的 Kubernetes 对象!对于给定的微服务,我们可能希望有多个 pod。为了避免手动管理它们,您可以使用 Deployment 对象,该对象将处理每个 pod 集。它还将确保有所需数量的实例,以防您希望某个特定 pod 有许多实例。

要存储这些属性,可以使用 ConfigMap。要存储凭证,可以使用SECRET对象。要访问它们,您需要与 Kubernetes API 服务器通信,以获取/读取所需的数据。

当我们将单体应用拆分为微服务时,我们说它们之间的通信是基于 HTTP 协议的(直接或间接)。每个微服务都在一个 pod 内运行,它将拥有一个专用的动态 IP 地址。因此,例如,如果Order微服务与Product微服务通信,它就无法猜测其目标的 IP 地址。我们需要使用类似 DNS 的解决方案,使用域名而不是 IP 地址,让系统动态解析域名。这正是 K8s 服务所做的。属于同一个服务的所有单元在同一个 DNS 名称下共享它们的 IP 地址,在那里你可以对它们进行负载平衡。

除了动态 IP 地址解析,该服务还包括负载平衡功能。

要在集群外部公开 QuarkuShop,请使用INGRESS对象。它对外公开了一个 Kubernetes 服务,并具有许多优秀的特性,包括负载平衡、SSL 等。

附加阅读

我无法在一章中完全涵盖 Kubernetes。这一章只是对 Kubernetes 世界的一个小介绍。我建议你读读马尔科·卢卡写的、曼宁出版社出版的《Kubernetes 在行动中的 ??》。我个人认为这是 Kubernetes 写的最好的书。

img/509649_1_En_11_Figs_HTML.jpg

如果你更喜欢视频,我推荐这个伟大的 Kubernetes 学习 YouTube 播放列表,它是由我的朋友 Houssem Dellai 制作的,他是微软的云工程师。

img/509649_1_En_11_Figt_HTML.jpg

十二、实现云模式

介绍

你已经知道你将在本书中使用 Kubernetes 作为云平台。在前一章中,您学习了将与 QuarkuShop 一起使用的 Kubernetes 对象。在这一章中,您将开始实现一些云模式,并将 monolithic universe(monolithic application、PostgreSQL 和 Keycloak)引入 Kubernetes。

将庞大的宇宙带到 Kubernetes

在开始处理应用代码之前,您需要将img/509649_1_En_12_Figa_HTML.gif PostgreSQL 数据库和 Keycloak 放到 Kubernetes 集群中。

将 PostgreSQL 部署到 Kubernetes

要将一个img/509649_1_En_12_Figb_HTML.gif PostgreSQL 数据库实例部署到 Kubernetes,您将使用:

  • 存储 PostgreSQL 用户名和数据库名的ConfigMap

  • 存储 PostgreSQL 密码的Secret

  • 一个为 PostgreSQL pods 请求存储空间的PersistentVolumeClaim

  • 一个Deployment提供了想要的 PostgreSQL pods 的描述

  • 用作指向 PostgreSQL pods 的 DNS 名称的Service

PostgreSQL 的ConfigMap如清单 12-1 所示。

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres
data:
  POSTGRES_DB: demo
  POSTGRES_USER: developer

Listing 12-1postgres-cm.yaml

PostgreSQL 密码存储在一个Secret中。该值需要编码为 Base64。您可以在本地使用openssl库对p4SSW0rd字符串进行编码:

echo -n 'p4SSW0rd' | openssl base64

结果是cDRTU1cwcmQ=。你在Secret对象内部使用它,如清单 12-2 所示。

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  labels:
    app: postgres
type: Opaque
data:
  POSTGRES_PASSWORD: cDRTU1cwcmQ=

Listing 12-2postgres-secret.yaml

清单 12-3 显示了用于请求 PostgreSQL 存储访问的PersistentVolumeClaim

  • PVC 名称。

  • ②访问类型。许多 pod 可以在这个 PVC 中同时读写。

  • ③所需的 PVC 存储。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc        ①
  labels:
    app: postgres
spec:
  accessModes:
    - ReadWriteMany         ②
  resources:
    requests:
      storage: 2Gi          ③

Listing 12-3postgres-pvc.yaml

清单 12-4 展示了 PostgreSQL Deployment文件。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1                         ①
  selector:
    matchLabels:
      app: postgres                   ②
  template:
    metadata:
      labels:
        app: postgres
    spec:
      volumes:                        ③
        - name: data                  ③
          persistentVolumeClaim:      ③
            claimName: postgres-pvc   ③
      containers:
        - name: postgres
          image: postgres:12.3
          envFrom:
            - configMapRef:           ④
                name: postgres-config ④
            - secretRef:              ④
                name: postgres-secret ④
          ports:
            - containerPort: 5432
          volumeMounts:               ③
            - name: data              ③
              mountPath: /var/lib/postgresql/data
              subPath: postgres
          resources:
            requests:
              memory: '512Mi'           ⑤
              cpu: '500m'               ⑤
            limits:
              memory: '1Gi'             ⑥
              cpu: '1'                  ⑥

Listing 12-4postgres-deployment.yaml

deployment资源将使用以下内容部署 PostgreSQL:

  • ①一个 pod 实例。

  • ②瞄准带有app=postgres标签的单元。

  • ③将postgres-pvc定义为持久性卷。

  • ④从postgres-configpostgres-secret加载环境变量。

  • ⑤每个 pod 所需的最低资源是 512MB 和 0.5 个 CPU 单元。

  • ⑥每个 pod 允许的最大资源是 1GB 和 1 个 CPU 单元。

PostgreSQL Service文件如清单 12-5 所示。

apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  selector:
    app: postgres
  ports:
   - port: 5432
  type: LoadBalancer

Listing 12-5postgres-svc.yaml

我们正在创建一个类型为LoadBalancerService。在支持负载平衡器的云提供商上,将提供一个外部 IP 地址来访问Service。当我们在minikube时,LoadBalancer类型使Service可以通过minikube service命令访问:

minikube service SERVICE_NAME

接下来,我们使用 IntelliJ 数据库浏览器测试已部署的 PostgreSQL 实例。

首先获取 PostgreSQL Kubernetes 服务 URL。您可以使用这个命令从 Minikube 集群获取 Kubernetes 服务 URL:

$ minikube service postgres --url

http://192.168.39.79:31450

该命令将返回 Minikube 集群中服务的 Kubernetes URL。如果有很多 URL,将一次打印一个。

我们将使用以下内容:

  • 192.168.39.79:31450作为数据库的 URL

  • developer作为用户

  • p4SSW0rd作为密码

  • demo作为数据库名称

只需点击测试连接,以验证一切正常。如果没问题,您将得到一个确认连接成功的img/509649_1_En_12_Fige_HTML.gif

img/509649_1_En_12_Figf_HTML.jpg

将 Keycloak 部署到 Kubernetes

为了将 Keycloak 部署到 Kubernetes 集群,我们将使用 Helm。

What is Kubernetes Helm?

Helm 是 Kubernetes 的一个包管理器,允许开发人员和操作人员更容易地将应用和服务打包、配置和部署到 Kubernetes 集群上。

Helm 现在是 Kubernetes 的一个官方项目,也是 Cloud Native Computing Foundation 的一部分,这是一个支持 Kubernetes 生态系统内外开源项目的非营利组织。

舵柄可以:

  • 安装软件。

  • 自动安装软件依赖项。

  • 升级软件。

  • 配置软件部署。

  • 从存储库中获取软件包。

Helm 通过以下组件提供此功能:

  • 一个名为helm的命令行工具,提供了所有舵功能的用户界面。

  • 一个名为tiller的配套服务器组件运行在您的 Kubernetes 集群上,监听来自helm的命令,并处理集群上软件版本的配置和部署。

  • 舵的包装格式,称为图表

What is a Helm Chart ?

Helm 包被称为 charts ,它们由几个 YAML 配置文件和一些呈现在 Kubernetes 清单文件中的模板组成。

helm命令可以从本地目录安装图表,或者从这个目录结构的.tar.gz打包版本安装图表。这些打包的图表也可以从图表存储库或 repos 自动下载和安装。

要安装 Helm CLI,只需转到本指南: helm.sh/docs/intro/install

安装 Helm CLI 后,您可以将 Keycloak Helm chart 安装到 Kubernetes 集群中。

您需要安装的 Keycloak helm 图表可以在codecentric存储库中找到,因此您需要将其添加到 helm 存储库中:

helm repo add codecentric https://codecentric.github.io/helm-charts

接下来,创建一个 Kubernetes 名称空间来安装 Keycloak:

$ kubectl create namespace keycloak

namespace/keycloak created

接下来,使用这个 Helm 命令安装 Keycloak:

helm install keycloak --namespace keycloak codecentric/keycloak

要列出在keycloak名称空间中创建的对象,使用kubectl get all -n keycloak命令:

NAME                        READY   STATUS    RESTARTS
pod/keycloak-0              1/1     Running   0
pod/keycloak-postgresql-0   1/1     Running   0

NAME                                   TYPE        CLUSTER-IP      PORT(S)
service/keycloak-headless              ClusterIP   None            80/TCP
service/keycloak-http                  ClusterIP   10.96.104.85    80/TCP 8443/TCP 9990/TCP
service/keycloak-postgresql            ClusterIP   10.107.175.90   5432/TCP
service/keycloak-postgresql-headless   ClusterIP   None            5432/TCP

NAME                                   READY
statefulset.apps/keycloak              1/1
statefulset.apps/keycloak-postgresql   1/1

请注意,我们有两个 pod:

  • keycloak-0:包含 Keycloak 应用

  • keycloak-postgresql-0:包含一个专用于 Keycloak 应用的 PostgreSQL 数据库实例

现在你需要像在第五章中一样配置 Keycloak 实例。要访问键盘锁盒,使用端口转发从盒到localhost:

kubectl -n keycloak port-forward service/keycloak-http 8080:80

现在,访问localhost:8080/auth上的钥匙锁控制台:

img/509649_1_En_12_Figg_HTML.jpg

在此屏幕中,您可以创建管理员用户:

img/509649_1_En_12_Figh_HTML.jpg

接下来,单击管理控制台以访问登录屏幕:

img/509649_1_En_12_Figi_HTML.jpg

登录后,您将进入 Keycloak 管理控制台:

img/509649_1_En_12_Figj_HTML.jpg

你现在可以使用与第五章相同的步骤来创建钥匙锁领域。

将单块夸库商店部署到库贝内特斯

在将img/509649_1_En_12_Figk_HTML.gif PostgreSQL 数据库和 Keycloak 引入 Kubernetes 之后,您可以继续将 monolithic QuarkuShop 应用部署到 Kubernetes。这个练习对于学习如何只将一个应用部署到 Kubernetes 非常有用。

我们首先向 quark shoppom.xml添加两个依赖项,如下所示:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-kubernetes</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-container-image-jib</artifactId>
</dependency>

当使用 Maven 构建项目时,这两个库将:

  • 为应用创建 Docker 图像

  • 生成部署应用所需的 Kubernetes 对象描述符

为了验证我说的是事实,img/509649_1_En_12_Figl_HTML.gif让我们构建应用并检查它:

$ mvn clean install -Dquarkus.container-image.build=true

target目录中,有两个新文件夹——jibkubernetes。要检查它们的内容,请运行以下命令:

  • ①生成 Docker 映像时 JIB 使用的目录。

  • ②用于存储生成的 Kubernetes 描述符的目录。相同的内容以两种格式生成:YAML 和 JSON。

$ ls -l target/jib target/kubernetes

target/jib:         ①
total 32
-rw-rw-r-- 1 nebrass nebrass 1160 sept.  8 21:01 application.properties
-rw-rw-r-- 1 nebrass nebrass  427 sept.  8 21:01 banner.txt
drwxrwxr-x 3 nebrass nebrass 4096 sept.  8 21:01 com
drwxrwxr-x 3 nebrass nebrass 4096 sept.  8 21:01 db
drwxrwxr-x 7 nebrass nebrass 4096 sept.  8 21:01 io
drwxrwxr-x 7 nebrass nebrass 4096 sept.  8 21:01 javax
drwxrwxr-x 7 nebrass nebrass 4096 sept.  8 21:01 META-INF
drwxrwxr-x 4 nebrass nebrass 4096 sept.  8 21:01 org

target/kubernetes:  ②
total 12
-rw-rw-r-- 1 nebrass nebrass 5113 sept.  8 21:01 kubernetes.json
-rw-rw-r-- 1 nebrass nebrass 3478 sept.  8 21:01 kubernetes.yml

如果希望基于本机二进制文件构建 Docker 映像,请运行以下命令:

mvn clean install -Pnative -Dquarkus.native.container-build=true -Dquarkus.container-image.build=true

您还可以验证本地是否有新创建的 Docker 映像:

$ docker images

REPOSITORY           TAG              IMAGE ID       CREATED          SIZE
nebrass/quarkushop   1.0.0-SNAPSHOT   eb2c67d7fa27   21 minutes ago   244MB

接下来,将该图像推送到 Docker Hub:

$ docker push nebrass/quarkushop:1.0.0-SNAPSHOT

在导入 Kubernetes 描述符之前,您需要做一个小的修改。该应用当前指向本地 PostgreSQL 和 Keycloak 实例。你有两个选择:

  • 更改硬编码的属性。

  • 使用环境变量覆盖这些属性。这是最好的解决方案,因为它不需要新的版本。

为了覆盖这些属性,我们将使用环境变量。例如,要将quarkus.http.port属性的值覆盖为9999,您可以创建一个名为QUARKUS_HTTP_PORT=9999的环境变量。正如您所注意到的,这是一个带下划线而不是分隔点.的大写字符串,在本例中,我们想要覆盖这些属性:

  • quarkus.datasource.jdbc.url:指向 PostgreSQL URL。其对应的环境变量将是QUARKUS_DATASOURCE_JDBC_URL

  • mp.jwt.verify.publickey.location:指向 Keycloak 网址。其对应的环境变量将是MP_JWT_VERIFY_PUBLICKEY_LOCATION

  • mp.jwt.verify.issuer:指向 Keycloak 网址。其对应的环境变量将是MP_JWT_VERIFY_ISSUER

这三个属性指向localhost作为 PostgreSQL 和 Keycloak 实例的主机。我们希望它们现在指向 PostgreSQL 和 Keycloak 各自的 pod。

如前一章所述,K8s 服务对象可以用作每组 pod 的 DNS 名称。

让我们列出集群中可用的Service对象:

  • ①PostgreSQLServicedefault名称空间中可用。

  • ②我们在keycloak名称空间中有几个 Keycloak Service对象:

    • 指向 Keycloak pods 的无头 Kubernetes 服务。无头服务用于实现与匹配 pod 的直接通信,而无需中间层。通信中不涉及代理或路由层。无头服务列出所有选定的后备 pod,而其他服务类型将呼叫转发到随机选择的 pod。

    • Kubernetes 服务,为 Keycloak pods 提供负载平衡路由器。

    • Kubernetes 服务,为 PostgreSQL pods 提供负载平衡路由器。

    • keycloak-postgresql-headless:指向 PostgreSQL pods 的无头 Kubernetes 服务。

$ kubectl get svc --all-namespaces

NAMESPACE             NAME                          PORT(S)
default               kubernetes                    443/TCP
default               postgres                      5432:31450/TCP              ①
keycloak              keycloak-headless             80/TCP                      ②
keycloak              keycloak-http                 80/TCP,8443/TCP,9990/TCP    ②
keycloak              keycloak-postgresql           5432/TCP                    ②
keycloak              keycloak-postgresql-headless  5432/TCP                    ②
kube-system           kube-dns                      53/UDP,53/TCP,9153/TCP
kubernetes-dashboard  dashboard-metrics-scraper     8000/TCP
kubernetes-dashboard  kubernetes-dashboard          80/TCP

在这种情况下,我们将不会使用 headless 服务,因为我们希望实现请求负载平衡,并且我们不需要直接与 pod 通信。

quarkus.datasource.jdbc.url属性包含jdbc:postgresql://localhost:5432/demo。我们将使用来自default名称空间的端口 5432 的postgres服务,而不是localhost。因此,新的属性定义将是:

quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/demo

环境变量将是:

QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgres:5432/demo

很好!我们将对mp.jwt.verify.publickey.locationmp.jwt.verify.issuer属性应用相同的逻辑。

1 mp.jwt.verify.publickey.location=http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
2 mp.jwt.verify.issuer=http://localhost:9080/auth/realms/quarkushop-realm

我们将使用来自keycloak名称空间的端口 80 的keycloak-http来代替localhost。新的properties值将为:

1 mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
2 mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

img/509649_1_En_12_Figm_HTML.gif注意,为了可见性,我们在 Keycloak 服务的service name后面附加了namespace name,但是我们没有为 PostgreSQL 服务这样做。这是因为在我们部署应用的default名称空间中存在 PostgreSQL 服务。Keycloak 服务在keycloak名称空间中可用。

MP JWT环境变量将是:

1 MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
2 MP_JWT_VERIFY_ISSUER=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

现在我们需要将这些新的环境变量添加到 Kubernetes 描述符中,该描述符是由 Quarkus Kubernetes 扩展在target/kubernetes/目录中生成的。

在 Kubernetes 世界中,如果我们想要将环境变量传递给 pod,我们使用 Kubernetes Deployment 对象在spec.template.spec.containers.env部分传递它们。

Kubernetes.json描述符文件中更新后的Deployment对象如清单 12-6 所示。

{
  "apiVersion": "apps/v1",
  "kind": "Deployment",
  "metadata": { ...,
    "name": "quarkushop"
  },
  "spec": {
    "replicas": 1,
    "selector": ...,
    "template": {
      "metadata": {
        "annotations": ...,
        "labels": {
          "app.kubernetes.io/name": "quarkushop",
          "app.kubernetes.io/version": "1.0.0-SNAPSHOT"
        }
      },
      "spec": {
        "containers": [
          {
            "env": [
              { "name": "KUBERNETES_NAMESPACE",
                "valueFrom": {
                  "fieldRef": { "fieldPath": "metadata.namespace" }
                }
              },
              { "name": "QUARKUS_DATASOURCE_JDBC_URL",
                "value": "jdbc:postgresql://postgres:5432/demo"
              },
              { "name": "MP_JWT_VERIFY_PUBLICKEY_LOCATION",
                "value": "http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs"
              },
              { "name": "MP_JWT_VERIFY_ISSUER",
                "value": "http://keycloak-http.keycloak/auth/realms/quarkushop-realm"
              }
            ],
            "image": "nebrass/quarkushop:1.0.0-SNAPSHOT",
            "imagePullPolicy": "IfNotPresent",
  ...
}

Listing 12-6target/kubernetes/kubernetes.json

每次都必须添加这些值是很烦人的,尤其是在每次构建代码时都删除目标文件夹的情况下。这就是为什么 Quarkus 团队为 Kubernetes 描述符添加了一个新的环境变量定义机制。您可以在 https://quarkus.io/guides/deploying-to-kubernetes#env-vars 了解更多信息。

您可以使用带有前缀quarkus.kubernetes.env.vars.application.properties文件为每个环境变量添加相同的环境变量:

1 quarkus.kubernetes.env.vars.quarkus-datasource-jdbc-url=jdbc:postgresql://postgres:5432/demo
2 quarkus.kubernetes.env.vars.mp-jwt-verify-publickey-location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
3 quarkus.kubernetes.env.vars.mp-jwt-verify-issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

要导入生成的 Kubernetes 描述符,请使用:

$ kubectl apply -f target/kubernetes/kubernetes.json

serviceaccount/quarkushop created
service/quarkushop created
deployment.apps/quarkushop created

要验证 QuarkuShop 是否正确部署到 Kubernetes,只需列出当前(default)名称空间中的所有对象,如下所示:

$ kubectl get all

NAME                              READY   STATUS    RESTARTS
pod/postgres-69c47c748-pnbbf      1/1     Running   3
pod/quarkushop-78c67844ff-7fzbv   1/1     Running   0

NAME                 TYPE           CLUSTER-IP    PORT(S)
service/kubernetes   ClusterIP      10.96.0.1     443/TCP
service/postgres     LoadBalancer   10.106.7.15   5432:31450/TCP
service/quarkushop   ClusterIP      10.97.230.13  8080/TCP

NAME                         READY   UP-TO-DATE   AVAILABLE
deployment.apps/postgres     1/1     1            1
deployment.apps/quarkushop   1/1     1            1

NAME                                    DESIRED   CURRENT   READY
replicaset.apps/postgres-69c47c748      1         1         1
replicaset.apps/quarkushop-78c67844ff   1         1         1

在资源列表中,有一个名为quarkushop-78c67844ff-7fzbv的 pod。为了检查一切是否正常,您需要访问打包在其中的应用。为此,您从quarkushop-78c67844ff-7fzbv pod 的端口 8080 到localhost的端口 8080 执行一次port-forward

$ kubectl port-forward quarkushop-78c67844ff-7fzbv 8080:8080

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

现在打开http://localhost:8080/api/health-ui,如截图所示:

img/509649_1_En_12_Fign_HTML.jpg

您可以看到,运行状况检查验证了 PostgreSQL 数据库和 Keycloak 是可访问的。

使用application.properties定义环境变量并不总是一个好主意。例如,如果您想要更改/添加一个属性,您需要构建并重新部署应用。有一种替代方法,不需要所有这些努力,就可以使用ConfigMaps为应用提供额外的properties。应用将ConfigMap内容解析为环境变量,这是将属性传递给应用的一种很好的方式。

为此,您需要另一个 Maven 依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-kubernetes-config</artifactId>
</dependency>

您需要删除之前添加的ENV VARs属性:

quarkus.kubernetes.env.vars.quarkus-datasource-jdbc-url=jdbc:postgresql://postgres: ...
quarkus.kubernetes.env.vars.mp-jwt-verify-publickey-location=http://keycloak-http.k...
quarkus.kubernetes.env.vars.mp-jwt-verify-issuer=http://keycloak-http.keycloak/a...

接下来,您启用 Kubernetes ConfigMap访问,并告诉应用哪个ConfigMap具有您需要的属性:

quarkus.kubernetes-config.enabled=true
quarkus.kubernetes-config.config-maps=quarkushop-monolith-config

清单 12-7 展示了如何定义新的ConfigMap称为quarkushop-monolith-config

1 apiVersion: v1
2 kind: ConfigMap
3 metadata:
4   name: quarkushop-monolith-config
5 data:
6   application.properties: |-
7     quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/demo
8     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
9     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

Listing 12-7quarkushop-monolith-config.yml

您只需将quarkushop-monolith-config.yml导入到 Kubernetes 集群中:

kubectl apply -f quarkushop-monolith-config.yml

现在,如果您再次构建 QuarkuShop 应用,您会注意到在生成的 Kubernetes 描述符中,有一个新的RoleBinding对象。这个对象由quarkus-kubernetes-config Quarkus 扩展生成。

  • ①当前对象是RoleBinding

  • RoleBinding对象名称为quarkushop:view

  • ③这将把ClusterRoleview角色绑定,以读取ConfigMapsSecrets

  • ④这个RoleBinding应用于quarkus-kubernetes扩展产生的ServiceAccount称为quarkushop

{
  "apiVersion" : "rbac.authorization.k8s.io/v1",
  "kind" : "RoleBinding",                             ①
  "metadata" : {
    "annotations" : {
      "prometheus.io/path" : "/metrics",
      "prometheus.io/port" : "8080",
      "prometheus.io/scrape" : "true"
    },
    "labels" : {
      "app.kubernetes.io/name" : "quarkushop",
      "app.kubernetes.io/version" : "1.0.0-SNAPSHOT"
    },
    "name" : "quarkushop:view"                        ②
  },
  "roleRef" : {
    "kind" : "ClusterRole",                           ③
    "apiGroup" : "rbac.authorization.k8s.io",
    "name" : "view"                                   ③
  },
  "subjects" : [ {
    "kind" : "ServiceAccount",                        ④
    "name" : "quarkushop"                             ④
  } ]
}

让我们构建并打包应用映像,并将其再次部署到 K8s 集群:

mvn clean install -DskipTests -DskipITs -Pnative \
      -Dquarkus.native.container-build=true \
      -Dquarkus.container-image.build=true

接下来,推送图片:

docker push nebrass/quarkushop:1.0.0-SNAPSHOT

接下来,再次将应用部署到 Kubernetes:

kubectl apply -f target/kubernetes/kubernetes.json

现在要测试应用,只需在 QuarkuShop pod 上执行port-forward:

$ kubectl get pods

NAME                          READY   STATUS    RESTARTS   AGE
postgres-69c47c748-pnbbf      1/1     Running   5          19d
quarkushop-77dcfc7c45-tzmbs   1/1     Running   0          73m

$ kubectl port-forward quarkushop-77dcfc7c45-tzmbs 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

然后打开http://localhost:8080/api/health-ui/,如截图所示:

img/509649_1_En_12_Figo_HTML.jpg

太棒了!您了解了如何将单体应用引入 Kubernetes。在下一章中,您将使用相同的步骤来创建微服务并将其引入 Kubernetes 集群。

结论

本章试图介绍一些基于 Kubernetes 生态系统的云模式。将单体应用及其依赖项引入 Kubernetes 的练习是创建和部署微服务的第一步。

十三、构建 Kubernetized 微服务

介绍

第八章讨论了如何将数据驱动设计(DDD)应用到单体应用中。我们打破了在分析项目时揭示的边界上下文之间的关系。在 Stan4J 中,最终的代码结构如下所示:

img/509649_1_En_13_Figa_HTML.jpg

在本章中,您将实现三个微服务— ProductOrderCustomer。这些包依赖于commons包,因此,您需要在实现三个微服务之前实现它。

创建公共图书馆

commons JAR 库将包装commons包的内容。

我们将生成一个简单的 Maven 项目,其中我们将复制commons包的内容。

mvn archetype:generate -DgroupId=com.targa.labs.commons -DartifactId=quarkushop-commons \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.4 -DinteractiveMode=false

该命令生成一个包含以下内容的简单项目:

project
|-- pom.xml
`-- src
    |-- main/java
    |       `-- com.targa.labs.commons
    |           `-- App.java
    `-- test/java
            `-- com.targa.labs.commons
                `-- AppTest.java

img/509649_1_En_13_Figb_HTML.gif我们将删除App.javaAppTest.java,因为我们不再需要它们。

然后,我们将commons包的内容从单体应用复制/粘贴到我们的quarkushop-commons项目中。

不要害怕!当您粘贴复制的类时,您会看到许多错误和警告,但是下一步是添加缺少的依赖项以使 IDE 满意。img/509649_1_En_13_Figc_HTML.gif

让我们打开pom.xml文件并开始进行更改:

  1. 首先将maven.compiler.sourcemaven.compiler.target1.7改为11

  2. 如下定义依赖关系:

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.openapi</groupId>
            <artifactId>microprofile-openapi-api</artifactId>
            <version>1.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.spec.javax.ws.rs</groupId>
            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>jakarta.persistence</groupId>
            <artifactId>jakarta.persistence-api</artifactId>
            <version>2.2.3</version>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.health</groupId>
            <artifactId>microprofile-health-api</artifactId>
            <version>2.2</version>
        </dependency>
    
        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.metrics</groupId>
            <artifactId>microprofile-metrics-api</artifactId>
            <version>2.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus.security</groupId>
            <artifactId>quarkus-security</artifactId>
            <version>1.1.2.Final</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <version>1.15.3</version>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-test-common</artifactId>
            <version>1.13.3.Final</version>
        </dependency>
    </dependencies>
    
    

哇哦!这些依赖来自哪里?我敢肯定你和我一样,不喜欢写自己不懂的东西。img/509649_1_En_13_Fige_HTML.gif不过不用担心,这些依赖项都是来自 Quarkus 框架。我使用 IDE 添加了缺少的依赖项:

img/509649_1_En_13_Figf_HTML.jpg

将出现 Maven 工件搜索窗口,如下所示:

img/509649_1_En_13_Figg_HTML.jpg

那么,您应该选择给定依赖项的哪个版本呢?您可以使用 IntelliJ 来确定在单体应用中使用哪些外部库:

img/509649_1_En_13_Figh_HTML.jpg

展开该部分并滚动以找到所需的库。版本将显示在groupIdartifactId之后,如下所示:

img/509649_1_En_13_Figi_HTML.jpg

在这里,您可以看到单体应用正在使用Lombok v1.18.12,因此在Commons项目中,您需要选择相同的版本。

img/509649_1_En_13_Figj_HTML.gif为了避免冲突,请确保quarkus-test-common依赖项与微服务具有相同的 Quarkus 版本。img/509649_1_En_13_Figk_HTML.gif

最后,您需要使用mvn clean install构建 Maven 项目。这个命令将构建 JAR,并使它在本地.m2目录中可用。这使您能够在以后的步骤中将它用作依赖项。

等等!你还没说完呢!你需要考虑一下测试!您需要将utils包从测试类复制到quarkushop-commons项目的主类中。

为了能够在quarkushop-commons库之外重用这些类,您需要将它们放在主目录中,就像任何其他普通类一样。属于测试目录的类只是用于测试目的,它们不打算被重用。

实施产品微服务

在这一部分,您开始做严肃的工作:创建Product微服务。让我们从代码中生成一个名为quarkushop-product的新 Quarkus 应用。夸库斯。io :

img/509649_1_En_13_Figl_HTML.jpg

以下是选定的扩展:

  • RESTEasy JAX-RS

  • 塞西·JSON-b

  • SmallRye OpenAPI

  • 冬眠的奥姆

  • Hibernate 验证程序

  • JDBC 驱动程序- PostgreSQL

  • 候鸟迁徙所经的路径

  • spring 数据 jpa api 的 quartus 扩展

  • 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj

  • SmallRye 健康

  • 忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈

  • 库比涅斯配置

  • 容器图像悬臂

通过单击 Generate Your Application 下载生成的应用 skull。

然后将代码导入您的 IDE。打开pom.xml文件,向其中添加 Lombok 和 TestContainers 依赖项:

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
    </dependency>

    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.15.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <version>1.15.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

接下来,添加最重要的依赖项——quarkushop-commons:

<dependency>
    <groupId>com.targa.labs</groupId>
    <artifactId>quarkushop-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

将代码从product包复制到quarkushop-product微服务。

img/509649_1_En_13_Figm_HTML.gif不要忘记将 banner.txt 文件从单体应用复制到Product微服务的src/main/resources目录中。

下一步是填充application.properties:

 1 # Datasource config properties
 2 quarkus.datasource.db-kind=postgresql
 3 quarkus.datasource.username=developer
 4 quarkus.datasource.password=p4SSW0rd
 5 quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/product
 6 # Flyway minimal config properties
 7 quarkus.flyway.migrate-at-start=true
 8 # HTTP config properties
 9 quarkus.http.root-path=/api
10 quarkus.http.access-log.enabled=true
11 %prod.quarkus.http.access-log.enabled=false
12 # Swagger UI
13 quarkus.swagger-ui.always-include=true
14 # Datasource config properties
15 %test.quarkus.datasource.db-kind=postgresql
16 # Flyway minimal config properties
17 %test.quarkus.flyway.migrate-at-start=true
18 # Define the custom banner
19 quarkus.banner.path=banner.txt
20 ### Security
21 quarkus.http.cors=true
22 quarkus.smallrye-jwt.enabled=true
23 # Keycloak Configuration
24 keycloak.credentials.client-id=quarkushop
25 # MP-JWT Config
26 mp.jwt.verify.publickey.location=http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
27 mp.jwt.verify.issuer=http://localhost:9080/auth/realms/quarkushop-realm
28 ### Health Check
29 quarkus.smallrye-health.ui.always-include=true
30 # Kubernetes ConfigMaps
31 quarkus.kubernetes-config.enabled=true
32 quarkus.kubernetes-config.config-maps=quarkushop-product-config

这些属性与单体应用几乎相同,这是合乎逻辑的,因为微服务是从单体应用上切下的一片。

img/509649_1_En_13_Fign_HTML.gif我更改了数据库的名称(第 5 行)和ConfigMap(第 34 行)。img/509649_1_En_13_Figo_HTML.gif

回想一下,我们更改了ConfigMap,所以现在我们需要创建它。见清单 13-1 。

1 apiVersion: v1
2 kind: ConfigMap
3 metadata:
4   name: quarkushop-product-config
5 data:
6   application.properties: |-
7     quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/product
8     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
9     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

Listing 13-1quarkushop-product-config.yml

img/509649_1_En_13_Figp_HTML.gif我还在这个ConfigMap中更改了数据库的名称(第 7 行)。

我更改了数据库名称,因为正如您在第九章中了解到的,每个微服务拥有一个数据库是明智的。img/509649_1_En_13_Figq_HTML.gif这就是我为每个微服务创建专用模式的原因:

img/509649_1_En_13_Figr_HTML.jpg

因为我们在数据库上下文中,我们需要将 Flyway 脚本V1.0__Init_app.sqlV1.1__Insert_samples.sql从单体应用复制到Product微服务的src/main/resources/db/migration目录中。我们还需要清理 SQL 脚本,只保留产品绑定的上下文相关对象和样本数据。

确保正确清理脚本,否则部署将在应用引导过程中失败。

接下来,有一个非常重要的任务要做:将quarkushop-commons项目标识到quarkushop-product的 Quarkus 索引。

What is the Quarkus Index?

Quarkus 自动索引当前模块。但是,当您拥有包含 CDI beans、实体和序列化为 JSON 的对象的外部模块时,您需要显式地对它们进行索引。

可以通过多种方式建立索引:

  • 使用 Jandex Maven 插件

  • 添加一个空的META-INF/beans.xml文件

  • 使用 Quarkus 索引依赖属性,这是我最喜欢的选择

可以使用application.properties值完成该索引:

quarkus.index-dependency.commons.group-id=com.targa.labs
quarkus.index-dependency.commons.artifact-id=quarkushop-commons

img/509649_1_En_13_Figt_HTML.gif没有这个index-dependency配置,您就无法构建应用的本机二进制文件。

在构建项目之前,我们需要将相关测试从 monolith 复制到quarkushop-product微服务:

  • CategoryResourceIT

  • CategoryResourceTest

  • ProductResourceIT

  • ProductResourceTest

  • ReviewResourceIT

  • ReviewResourceTest

我们还需要将 Keycloak Docker 文件从src/main/docker复制到quarkushop-product微服务:

  • keycloak-test.yml文件

  • realms目录

为了能够执行测试,我们需要在test环境/概要文件中禁用 Kubernetes 支持:

%test.quarkus.kubernetes-config.enabled=false
quarkus.test.native-image-profile=test

img/509649_1_En_13_Figu_HTML.gif我们将原生映像测试配置文件定义为test,以便禁用 Kubernetes 对原生映像测试的支持。

接下来,我们需要执行测试,构建并推送quarkushop-product映像:

mvn clean install -Pnative \
    -Dquarkus.native.container-build=true \
    -Dquarkus.container-image.build=true

然后,我们将quarkushop-product图像推送到容器注册表,如下所示:

docker push nebrass/quarkushop-product:1.0.0-SNAPSHOT

我们现在创建quarkushop-product-config ConfigMap:

kubectl apply -f quarkushop-product/quarkushop-product-config.yml

并将Product微服务部署到 Kubernetes 集群:

kubectl apply -f quarkushop-product/target/kubernetes/kubernetes.json

太棒了!现在我们可以列出 pod 了:

$ kubectl get pods

NAME                                  READY   STATUS
postgres-69c47c748-pnbbf              1/1     Running
quarkushop-product-7748f9f74c-dqnqk   1/1     Running

我们可以在quarkushop-product上使用port-forward来测试应用:

$ kubectl port-forward quarkushop-product-7748f9f74c-dqnqk 8080:8080

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080

然后一个curl命令可以计算存储在数据库中的产品数量:

$ curl -X GET "http://localhost:8080/api/products/count"

4

很好!对数据库的访问工作正常。img/509649_1_En_13_Figv_HTML.gif我们还可以通过使用curl -X GET "http://localhost:8080/api/health"命令来运行健康检查,以确保正确到达键盘锁:

{
    "status": "UP",
    "checks": [
        {
            "name": "Keycloak connection health check",
            "status": "UP"
        },
        {
            "name": "Database connections health check",
            "status": "UP"
        }
    ]
}

太棒了!一切都在按预期工作!img/509649_1_En_13_Figw_HTML.gif img/509649_1_En_13_Figx_HTML.gif我们现在可以转移到Order微服务。

实施订单微服务

在本节中,我们将生成与Product微服务具有相同扩展的Order微服务,并增加一个扩展:REST 客户端扩展。

我们知道Order微服务对Product微服务有通信依赖。这种通信可以作为从Order微服务到Product微服务的 REST API 调用来实现。这就是为什么我们在选择的依赖项中包含 REST 客户端扩展。

生成项目后,我们将运行与使用Product微服务时相同的任务:

  • 将代码从单体应用中的order包复制到新的Order微服务中

  • 添加 Lombok、AssertJ 和 TestContainers 依赖项

  • 添加quarkushop-commons依赖项

  • 从 monolith 复制banner.txt文件

  • 添加application.properties并更改数据库和ConfigMap名称

  • 创建quarkushop-product-config ConfigMap文件

  • 复制 Flyway 脚本并清理不相关的对象和数据

  • application.properties中添加quarkushop-commons的 Quarkus 索引依赖关系

此时,我们需要修复代码,因为我们在OrderItemService类中仍然有一个ProductRepository引用。

OrderItemService使用ProductRepository查找使用给定 ID 的产品。这个编程调用将被对Product微服务的 REST API 调用所取代。为此,我们需要创建一个ProductRestClient类,它将使用给定的 ID 获取产品数据:

  • ProductRestClient将指向/products URI。

  • @RegisterRestClient允许 Quarkus 知道这个接口可以作为 REST 客户端用于 CDI 注入。

  • findById()方法将在/products URI 上进行 HTTP GET。

@Path("/products")                              ①
@RegisterRestClient                             ②
public interface ProductRestClient {

    @GET
    @Path("/{id}")
    ProductDto findById(@PathParam Long id);    ③
}

但是Product微服务 API 的基 URL 是什么?为了正常工作,需要对ProductRestClient进行配置。可以使用这些属性完成配置:

  • ProductRestClient的基本 URL 配置。

  • ②将ProductRestClient bean 的范围定义为Singleton

1 product-service.url=http://quarkushop-product:8080/api
2 com.targa.labs.quarkushop.order.client.ProductRestClient/mp-rest/url=${product-service.url}     ①
3 com.targa.labs.quarkushop.order.client.ProductRestClient/mp-rest/scope=javax.inject.Singleton   ②

我们将重构OrderItemService类来改变:

@Inject
ProductRepository productRepository;

敬新的:

@RestClient
ProductRestClient productRestClient;

img/509649_1_En_13_Figy_HTML.gif @RestClient用于注入一个 REST 客户端。

然后,我们将productRepository.getOne()调用更改为productRestClient.findById()img/509649_1_En_13_Figz_HTML.gifJPA 存储库获取已经被 REST API 调用所取代。

因为我们对Product微服务有一个外部依赖,所以我们需要创建一个健康检查来验证Product微服务是否可达,就像我们对 PostgreSQL 和 Keycloak 所做的一样。ProductServiceHealthCheck看起来是这样的:

@Slf4j
@Liveness
@ApplicationScoped
public class ProductServiceHealthCheck implements HealthCheck {

    @ConfigProperty(name = "product-service.url", defaultValue = "false")
    Provider<String> productServiceUrl;

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder =
                HealthCheckResponse.named("Product Service connection health check");

        try {

            productServiceConnectionVerification();
            responseBuilder.up();

        } catch (IllegalStateException e) {
            responseBuilder.down().withData("error", e.getMessage());
        }

        return responseBuilder.build();
    }

    private void productServiceConnectionVerification() {
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofMillis(3000))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(productServiceUrl.get() + "/health"))
                .build();

        HttpResponse<String> response = null;

        try {
            response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException e) {
            log.error("IOException", e);
        } catch (InterruptedException e) {
            log.error("InterruptedException", e);
            Thread.currentThread().interrupt();
        }

        if (response == null || response.statusCode() != 200) {
            throw new IllegalStateException("Cannot contact Product Service");
        }
    }
}

我们需要将这些相关测试从 monolith 复制到quarkushop-order微服务:

  • AddressServiceUnitTest

  • CartResourceIT

  • CartResourceTest

  • OrderItemResourceIT

  • OrderItemResourceTest

  • OrderResourceIT

  • OrderResourceTest

我们还需要将 Keycloak Docker 文件从src/main/docker复制到quarkushop-order微服务:

  • keycloak-test.yml文件

  • realms目录

然后我们添加执行测试所需的属性:

%test.quarkus.kubernetes-config.enabled=false
quarkus.test.native-image-profile=test

quarkushop-product不依赖任何其他微服务,所以我们为quarkushop-order所做的修改对quarkushop-product来说已经足够了。然而,quarkushop-product微服务上的quarkushop-order回复意味着测试也依赖于quarkushop-product微服务。

我们有许多解决方案,以下是我的两个选择:

  • 嘲笑RestClient

  • 添加一个quarkushop-product的测试实例

我将为quarkushop-order使用第二种选择,并为quarkushop-customer模拟RestClient,因为它在另一个微服务上回复。

回想一下,我们使用 TestContainers 框架为集成测试提供了一个 Keycloak 实例。该供应是使用一个docker-compose文件进行的,其中创建了一个keycloak服务。我们可以使用相同的方法来提供一个quarkushop-product实例,使用相同的docker-compose文件。但是我们必须首先创建一个新的QuarkusTestResourceLifecycleManager类,而不是使用KeycloakRealmResource,因为它只提供 Keycloak。

让我们将src/main/docker/keycloak-test.yml文件重命名为src/main/docker/context-test.yml。然后我们可以添加两个服务:quarkushop-productpostgresql-db

img/509649_1_En_13_Figaa_HTML.gif我们为什么要增加postgresql-db服务?答案很简单;img/509649_1_En_13_Figab_HTML.gif这是quarkushop-product所需要的(我们的微服务需要数据库存储和一个 Keycloak 租户来工作)。img/509649_1_En_13_Figac_HTML.gif

src/main/docker/ context-test.yml的内容如下:

version: '3'
services:
  keycloak:
    image: jboss/keycloak:latest
    command:
      [
        '-b','0.0.0.0',
        '-Dkeycloak.migration.action=import',
        '-Dkeycloak.migration.provider=dir',
        '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
        '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
        '-Djboss.socket.binding.port-offset=1000',
        '-Dkeycloak.profile.feature.upload_scripts=enabled',
      ]
    volumes:
      - ./realms-test:/opt/jboss/keycloak/realms
    environment:
      - KEYCLOAK_USER=admin
      - KEYCLOAK_PASSWORD=admin
      - DB_VENDOR=h2
    ports:
      - 9080:9080
      - 9443:9443
      - 10990:10990
  quarkushop-product:
    image: nebrass/quarkushop-product:1.0.0-SNAPSHOT
    environment:
      - QUARKUS_PROFILE=test
      - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/product
      - MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://keycloak:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
      - MP_JWT_VERIFY_ISSUER=http://keycloak:9080/auth/realms/quarkushop-realm
    depends_on:
      - postgresql-db
      - keycloak
    ports:
      - 8080:8080
  postgresql-db:
    image: postgres:13
    volumes:
      - /opt/postgres-volume:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=developer
      - POSTGRES_PASSWORD=p4SSW0rd
      - POSTGRES_DB=product
      - POSTGRES_HOST_AUTH_METHOD=trust
    ports:
      - 5432:5432

img/509649_1_En_13_Figad_HTML.gif注意,通过quarkushop-product的环境变量使用了可用的 Keycloak 实例。img/509649_1_En_13_Figae_HTML.gif

太棒了!现在,TestContainers将提供 Keycloak/PostgreSQL/ quarkushop-product实例,这是quarkushop-order在这些集成测试中所需要的。img/509649_1_En_13_Figaf_HTML.gif img/509649_1_En_13_Figag_HTML.gif

接下来,我们需要创建一个新的QuarkusTestResourceLifecycleManager类,名为ContextTestResource。这个类将提供 Keycloak 和quarkushop-product,并将它们的属性传递给应用。见清单 13-2 。

  • ①定义提供的服务。

  • ②定义 Keycloak 和quarkushop-product需要的属性。

public class ContextTestResource implements QuarkusTestResourceLifecycleManager {

    @ClassRule
    public static DockerComposeContainer ECOSYSTEM = new DockerComposeContainer(
            new File("src/main/docker/context-test.yml"))
            .withExposedService("quarkushop-product_1", 8080,   ①
                    Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)))
            .withExposedService("keycloak_1", 9080,             ①
                    Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

    @Override
    public Map<String, String> start() {
        ECOSYSTEM.start();

        String jwtIssuerUrl = String.format("http://%s:%s/auth/realms/quarkus-realm",
                ECOSYSTEM.getServiceHost("keycloak_1", 9080),
                ECOSYSTEM.getServicePort("keycloak_1", 9080)
        );

        TokenService tokenService = new TokenService();
        Map<String, String> config = new HashMap<>();

        try {

            String adminAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                    "admin", "test", "quarkus-client", "mysecret");
            String testAccessToken = tokenService.getAccessToken(jwtIssuerUrl, "test", "test", "quarkus-client", "mysecret");

            config.put("quarkus-admin-access-token", adminAccessToken);
            config.put("quarkus-test-access-token", testAccessToken);

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        config.put("mp.jwt.verify.publickey.location", jwtIssuerUrl
                                  + "/protocol/openid-connect/certs");    ②

        config.put("mp.jwt.verify.issuer", jwtIssuerUrl);                  ②

        String productServiceUrl = String.format("http://%s:%s/api",
                ECOSYSTEM.getServiceHost("quarkushop-product_1", 8080),
                ECOSYSTEM.getServicePort("quarkushop-product_1", 8080)
        );
        config.put("product-service.url", productServiceUrl);              ②

        return config;
    }

    @Override
    public void stop() {
        ECOSYSTEM.stop();
    }

}

Listing 13-2src/test/java/com/targa/labs/quarkushop/order/util/ContextTestResource.java

然后我们重构测试以包含ContextTestResource:

@DisabledOnNativeImage
@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
@QuarkusTestResource(ContextTestResource.class)
class CartResourceTest {
...
}

img/509649_1_En_13_Figah_HTML.gif当运行重构步骤时,我删除了客户创建。我用的是随机生成的客户 ID。

让我们仔细看看微服务的创建过程:img/509649_1_En_13_Figai_HTML.gif

  • 构建quarkushop-order映像并将其推送到容器注册表中:

  • 创建quarkushop-order-config ConfigMap文件:

$ mvn clean install -Pnative \
    -Dquarkus.native.container-build=true \
    -Dquarkus.container-image.build=true

$ docker push nebrass/quarkushop-order:1.0.0-SNAPSHOT

  • 在 Kubernetes 中创建quarkushop-order-config ConfigMap:
1 apiVersion: v1
2 kind: ConfigMap
3 metadata:
4   name: quarkushop-order-config
5 data:
6   application.properties: |-
7     quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/order
8     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
9     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm

  • quarkushop-order应用部署到 Kubernetes:
kubectl apply -f quarkushop-order/quarkushop-order-config.yml

  • 使用健康检查、一个port-forward和一个简单的curl GET 命令对/health API 检查quarkushop-order pod:
kubectl apply -f quarkushop-order/target/kubernetes/kubernetes.json

{
    "status": "UP",
    "checks": [
        {
            "name": "Keycloak connection health check",
            "status": "UP"
        },
        {
            "name": "Product Service connection health check",
            "status": "UP"
        },
        {
            "name": "Database connections health check",
            "status": "UP"
        }
    ]
}

很好!Order微服务工作正常。接下来,我们对Customer微服务做同样的事情。img/509649_1_En_13_Figaj_HTML.gif

实施客户微服务

为了实现Customer微服务,我们将应用与Order微服务相同的步骤。同样,我们将:

  • 将代码从单体应用中的customer包复制到新的Customer微服务中

  • 添加 Lombok、AssertJ 和 TestContainers 依赖项

  • 添加quarkushop-commons依赖项

  • 从 monolith 复制banner.txt文件

  • 添加application.properties并更改数据库和ConfigMap名称

  • 复制 Flyway 脚本并清理不相关的对象和数据

  • application.properties中添加quarkushop-commons的 Quarkus 索引依赖关系

然后,我们需要将这些相关测试从 monolith 复制到quarkushop-customer微服务:

  • CustomerResourceIT

  • CustomerResourceTest

  • PaymentResourceIT

  • PaymentResourceTest

我们还需要将 Keycloak Docker 文件从src/main/docker复制到quarkushop-order微服务:

  • keycloak-test.yml文件

  • realms目录

然后我们添加执行测试所需的属性:

%test.quarkus.kubernetes-config.enabled=false
quarkus.test.native-image-profile=test

接下来,我们嘲弄一下OrderRestClient。我们创建一个名为MockOrderRestClient的新类,如清单 13-3 所示。

  • ①用于模拟测试中注入的 beans 的注释。

  • Mock类实现了RestClient接口。

  • ③实现了Mock方法,以返回适合测试的结果。

@Mock                                                           ①
@ApplicationScoped
@RestClient
public class MockOrderRestClient implements OrderRestClient {   ②

    @Override
    public Optional<OrderDto> findById(Long id) {               ③
        OrderDto order = new OrderDto();
        order.setId(id);
        order.setTotalPrice(BigDecimal.valueOf(1000));
        return Optional.of(order);
    }

    @Override
    public Optional<OrderDto> findByPaymentId(Long id) {
        OrderDto order = new OrderDto();
        order.setId(5L);
        return Optional.of(order);
    }

    @Override
    public OrderDto save(OrderDto order) {
        return order;
    }
}

Listing 13-3src/test/java/com/targa/labs/quarkushop/customer/utils/MockOrderRestClient.java

仅此而已!模仿是非常容易的,被模仿的组件会被自动注入到测试中。img/509649_1_En_13_Figak_HTML.gif微服务创建流程如下:

  • 构建quarkushop-customer映像并将其推送到容器注册表中

  • 创建quarkushop-customer-config ConfigMap文件

  • 在 Kubernetes 中创建quarkushop-customer-config ConfigMap

  • 使用健康检查、port-forward和简单的curl GET 命令对/health API 检查quarkushop-customer pod

img/509649_1_En_13_Figal_HTML.gif你会在本书的img/509649_1_En_13_Figam_HTML.gif GitHub 资源库中找到所有的代码和资源。

实施用户微服务

什么,额外的微服务?我知道我在这一章的开始没有提到这一点。img/509649_1_En_13_Figan_HTML.gif

别担心,这不是一个巨大的微服务。它用于身份验证,并将保存 Quarkushop Swagger UI monolith 中列出的user部分 REST APIs:

img/509649_1_En_13_Figao_HTML.jpg

让我们用这些扩展生成一个新的 Quarkus 应用:

  • RESTEasy JAX-RS

  • 塞西·JSON-b

  • SmallRye OpenAPI

  • 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj 小 jj

  • SmallRye 健康

  • 忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈

  • 库比涅斯配置

  • 容器图像悬臂

没有与持久性相关的扩展,因为在这个微服务中你不会与数据库交互。

实施User微服务的步骤如下:

  • 复制用户相关内容。这个微服务非常少,所以只会装UserResource级。

  • 添加 Lombok 依赖项。

  • 添加quarkushop-commons依赖项。

  • 从 monolith 复制banner.txt文件。

  • 添加application.properties,更改ConfigMap名称,并删除所有数据持久性属性(Flyway、JPA 等。).

  • application.properties中添加quarkushop-commons的 Quarkus 索引依赖关系。

  • 构建quarkushop-user映像并将其推送到容器注册中心。

  • 创建quarkushop-user-config ConfigMap文件。

  • 在 Kubernetes 中创建quarkushop-user-config ConfigMap

  • 使用健康检查、port-forward和简单的curl GET 命令对/health API 检查quarkushop-user pod。

img/509649_1_En_13_Figaq_HTML.gif你会在本书的img/509649_1_En_13_Figar_HTML.gif GitHub 资源库中找到所有的代码和资源。

在部署了quarkushop-user微服务之后,您可以使用它来获得一个access_token,并与其他三个微服务的安全 API 进行通信:

img/509649_1_En_13_Figas_HTML.png

很好!微服务已正确部署并正常工作!img/509649_1_En_13_Figat_HTML.gif

结论

在本章中,您学习了如何创建微服务并将它们部署到 Kubernetes。在这个繁重的任务中,您实现了在第九章中介绍的模式。但是您没有实现最需要的模式。这是你在下一章要做的。img/509649_1_En_13_Figau_HTML.gif

十四、与 Quarkus 和 Kubernetes 一起飞遍天空

介绍

从第九章开始,您学习了云原生模式,甚至实现了其中的一些模式:

  • 服务发现和注册:使用 Kubernetes DeploymentService对象完成

  • 外部化配置:使用 Kubernetes ConfigMapSecret对象制作

  • 每个服务的数据库:在使用 DDD 概念分割整体代码库时创建的

  • 应用度量:使用 SmallRye Metrics Quarkus 扩展实现

  • 健康检查 API :使用 SmallRye Health Quarkus 扩展实现

  • 服务间的安全性:使用 SmallRye JWT 夸库扩展和 Keycloak 实现

在本章中,您将学习如何实现更流行的模式:

  • 断路器

  • 日志聚合

  • 分布式跟踪

  • 应用编程接口网关

实现断路器模式

断路器模式对于使用错误通信协议(如 HTTP)的弹性微服务非常有用。该模式的思想是处理微服务之间的任何通信问题。

img/509649_1_En_14_Figa_HTML.gif断路器模式的这种实现只会影响OrderCustomer微服务,其中我们使用 REST 客户端进行外部调用。

在基于 Quarkus 的应用中实现这种模式非常容易。第一步是将这个依赖项添加到pom.xml文件中:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>

现在,我们在 REST 客户端组件上启用断路器特性。假设我们希望 REST 客户机停止调用 API 15 秒钟,如果我们在最近 10 次请求中有 50%的请求失败。这可以使用下面这行代码来完成:

@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 15000)

Order微服务中:

@Path("/products")
@RegisterRestClient
public interface ProductRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 15000)
    ProductDto findById(@PathParam Long id);
}

@CircuitBreaker有许多属性:

  • failOn:应被视为失败的异常类型列表;默认值为{Throwable.class}。由带注释的方法抛出的继承自Throwable的所有异常都被认为是失败的。

  • skipOn:不应被视为失败的异常类型列表;默认值为{}

  • delay:开路转变为半开状态的延迟时间;默认值为5000

  • delayUnit:延时的单位。默认值为ChronoUnit.MILLIS

  • requestVolumeThreshold:滚动窗口中连续请求的数量。默认值为20

  • failureRatio:滚动窗口内使电路跳闸断开的故障比率。默认值为0.50

  • successThreshold:半开电路再次闭合前成功执行的次数。默认值为1

img/509649_1_En_14_Figb_HTML.gif断路器模式有三种状态:

  • 所有的请求都是正常的

  • Half-open:进行验证以检查问题是否仍然出现的过渡状态

  • Open:所有请求都被禁用,直到延迟到期

Customer微服务中:

@Path("/orders")
@RegisterRestClient
public interface OrderRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    Optional<OrderDto> findById(@PathParam Long id);

    @GET
    @Path("/payment/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    Optional<OrderDto> findByPaymentId(Long id);

    @POST
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    OrderDto save(OrderDto order);
}

SmallRye 容错扩展提供了许多其他有用的选项来处理故障,以便成为具有强大弹性的微服务。例如,我们有这些机制:

  • 重试机制:用于调用失败时重试的次数;
@Path("/products")
@RegisterRestClient
public interface ProductRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    @Retry(maxRetries = 4)
    ProductDto findById(@PathParam Long id);
}

如果调用失败,img/509649_1_En_14_Figc_HTML.gif @Retry(maxRetries = 4)将运行最多四次重试。

  • 超时机制:用于定义方法执行超时。这很容易实现:
@Path("/products")
@RegisterRestClient
public interface ProductRestClient {

    @GET
    @Path("/{id}")
    @CircuitBreaker(requestVolumeThreshold = 10, delay = 15000)
    @Retry(maxRetries = 4)
    @Timeout(500)
    ProductDto findById(@PathParam Long id);
}

如果findById()调用超过 500 毫秒,那么img/509649_1_En_14_Figd_HTML.gif @Timeout(500)会让应用抛出一个TimeoutException

  • 回退机制:用于在主方法失败时调用回退(或备份)方法。这里有一个注释来完成这项工作:
public class SomeClass {

    @Inject
    @RestClient
    ProductRestClient productRestClient;

    @Fallback(fallbackMethod = "fallbackFetchProduct")
    List<ProductDto> findProductsByCategory(String category){
        return productRestClient.findProductsByCategory(category);
    }

    public List<ProductDto> fallbackFetchProduct(String category) {
        return Collections.emptyList();
    }
}

img/509649_1_En_14_Fige_HTML.gif如果productRestClient.findProductsByCategory()失败,您将从fallbackFetchProduct()方法而不是findProductsByCategory()获得响应。您可以进一步调整这个强大的机制。例如,您可以将其配置为在定义异常或特定超时后切换到回退方法。

请注意,断路器模式和容错模式在 Quarkus 框架中得到了完美的实现。

实现日志聚合模式

对于日志聚合模式,我们使用著名的 ELK(弹性搜索、日志存储和 Kibana)堆栈。这是三个开源项目:

  • Elasticsearch 是一个搜索和分析引擎。

  • Logstash 是一个服务器端数据处理管道,它同时从多个来源获取数据,对其进行转换,然后将其发送到 Elasticsearch 这样的“stash”。

  • Kibana 允许用户在 Elasticsearch 中用图表和图形可视化数据。

总之,这些工具最常用于集中和分析分布式系统中的日志。ELK 堆栈很受欢迎,因为它满足了日志分析领域的需求。

本章中 ELK 堆栈的使用案例如下:

img/509649_1_En_14_Figf_HTML.jpg

所有的微服务都会将各自的日志推送到 Logstash,Logstash 会使用 Elasticsearch 对它们进行索引。索引日志可以在以后被 Kibana 使用。

Quarkus 有一个很棒的扩展叫做quarkus-logging-gelf,它被描述为“使用 Graylog 扩展日志格式的日志,并将您的日志集中在埃尔克或 EFK。”img/509649_1_En_14_Figg_HTML.gif

What is the Graylog Extended log Format?

基于 Graylog.org 网站:“Graylog 扩展日志格式(GELF)是一种独特方便的日志格式,旨在解决传统普通系统日志的所有缺点。这一企业功能允许您从任何地方收集结构化事件,然后在眨眼之间压缩并分块它们。”

很好!Logstash 本身支持 Graylog 扩展日志格式。你只需要在配置过程中激活它。

步骤 1:将 ELK 栈部署到 Kubernetes

如何在 Kubernetes 集群中安装 ELK 堆栈?img/509649_1_En_14_Figh_HTML.gif这是个大问题!

这是一个极其简单的任务:正如我们对 Keycloak 所做的那样,我们将使用 Helm 来安装 ELK 堆栈。img/509649_1_En_14_Figi_HTML.gif

首先将官方 ELK Helm 图表库添加到我们的 Helm 客户端:

helm repo add elastic https://helm.elastic.co

接下来,我们需要更新引用:

helm repo update

如果你像我一样在 Minikube 上,你需要创建一个elasticsearch-values.yaml文件,你将使用它来定制helm install。见清单 14-1 。

# Permit co-located instances for solitary minikube virtual machines.
antiAffinity: "soft"

# Shrink default JVM heap.
esJavaOpts: "-Xmx128m -Xms128m"

# Allocate smaller chunks of memory per pod.
resources:
  requests:
    cpu: "100m"
    memory: "512M"
  limits:
    cpu: "1000m"
    memory: "512M"

# Request smaller persistent volumes.
volumeClaimTemplate:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: "standard"
  resources:
    requests:
      storage: 100M

Listing 14-1elasticsearch-values.yaml

img/509649_1_En_14_Figj_HTML.gif该配置文件是在 Minikube 上安装 Elasticsearch 时推荐使用的配置: https://github.com/elastic/helm-charts/blob/master/elasticsearch/examples/minikube/values.yaml

现在我们安装 Elasticsearch:

helm install elasticsearch elastic/elasticsearch -f ./elasticsearch-values.yaml

我们可以看到使用该命令创建了什么:

$ kubectl get all -l release=elasticsearch

NAME                         READY   STATUS    RESTARTS
pod/elasticsearch-master-0   1/1     Running   0
pod/elasticsearch-master-1   1/1     Running   0
pod/elasticsearch-master-2   1/1     Running   0

NAME                                    TYPE        CLUSTER-IP    PORT(S)
service/elasticsearch-master            ClusterIP   10.103.91.46  9200/TCP,9300/TCP
service/elasticsearch-master-headless   ClusterIP   None          9200/TCP,9300/TCP

NAME                                    READY
statefulset.apps/elasticsearch-master   3/3

img/509649_1_En_14_Figk_HTML.gif这三个主机是为了实现高可用性。我知道我们现在没有问题,但是想想黑色星期五!img/509649_1_En_14_Figl_HTML.gif

pod 处于运行状态,因此我们可以测试 Elasticsearch 9200 端口。我们可以做一个port-forward和一个curl:

$ kubectl port-forward service/elasticsearch-master 9200
Forwarding from 127.0.0.1:9200 -> 9200
Forwarding from [::1]:9200 -> 9200

$ curl localhost:9200
{
  "name" : "elasticsearch-master-1",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "UkYbL4KsSeK4boVr4rOe2w",
  "version" : {
    "number" : "7.9.2",
    ...
    "lucene_version" : "8.6.2",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

太棒了!我们可以部署基巴纳:

helm install kibana elastic/kibana --set fullnameOverride=quarkushop-kibana

我们可以看到使用该命令创建了什么:

$ kubectl get all -l release=kibana

NAME                                     READY   STATUS
pod/quarkushop-kibana-696f869668-5tcvz   1/1     Running

NAME                        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)
service/quarkushop-kibana   ClusterIP   10.107.223.6   <none>        5601/TCP

NAME                                READY   UP-TO-DATE   AVAILABLE
deployment.apps/quarkushop-kibana   1/1     1            1

NAME                                           DESIRED   CURRENT   READY
replicaset.apps/quarkushop-kibana-696f869668   1         1         1

我们将在 Kibana 服务上做一个port-forward:

kubectl port-forward service/quarkushop-kibana 5601

然后打开 Kibana UI,检查一切是否正常工作:

img/509649_1_En_14_Figm_HTML.jpg

很好!我们需要安装 Logstash,但是我们需要使用 Helm logstash-values.yaml文件定制安装;见清单 14-2 。

logstashConfig:
  logstash.yml: |
    http.host: "0.0.0.0"
    xpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch-master:9200" ]
    xpack.monitoring.enabled: true
  pipelines.yml: |
    - pipeline.id: custom
      path.config: "/usr/share/logstash/pipeline/logstash.conf"
logstashPipeline:
  logstash.conf: |
    input {
      gelf {
        port => 12201
        type => gelf
      }
    }

    output {
      stdout {}
      elasticsearch {
        hosts => ["http://elasticsearch-master:9200"]
        index => "logstash-%{+YYYY-MM-dd}"
      }
    }
service:
  annotations: {}
  type: ClusterIP
  ports:
    - name: filebeat
      port: 5000
      protocol: TCP
      targetPort: 5000
    - name: api
      port: 9600
      protocol: TCP
      targetPort: 9600
    - name: gelf
      port: 12201
      protocol: UDP
      targetPort: 12201

Listing 14-2logstash-values.yaml

values.yaml文件用于配置:

  • Logstash 管道。启用gelf插件,公开默认的 12201 端口,并定义 Logstash 输出模式和到 Elasticsearch 实例的流

  • Logstash 服务定义和公开的端口

现在,让我们安装 Logstash:

helm install -f ./logstash-values.yaml logstash elastic/logstash \
      --set fullnameOverride=quarkushop-logstash

要列出创建的对象,请运行以下命令:

$ k get all -l chart=logstash

NAME                        READY   STATUS    RESTARTS
pod/quarkushop-logstash-0   1/1     Running   0

NAME                                   TYPE        CLUSTER-IP      PORT(S)
service/quarkushop-logstash            ClusterIP   10.107.204.49   5000/TCP,9600/TCP,12201/UDP
service/quarkushop-logstash-headless   ClusterIP   None            5000/TCP,9600/TCP,12201/UDP

NAME                                   READY
statefulset.apps/quarkushop-logstash   1/1

太棒了!现在,ELK 堆栈已正确部署。下一步是配置微服务以登录到 ELK 堆栈。

步骤 2:配置微服务以登录 ELK 堆栈

我们将在此步骤中进行的修改适用于:

  • quarkushop-product

  • quarkushop-order

  • quarkushop-customer

  • quarkushop-user

让我们给pom.xml文件添加扩展名:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-logging-gelf</artifactId>
</dependency>

我们需要为每个微服务ConfigMap文件定义 Logstash 服务器属性,如清单 14-3 所示。

  • ①Logstash 主机是公开 log stash 的 Kubernetes 服务。

  • ②Logstash 端口在 Logstash Kubernetes 服务中定义。

apiVersion: v1
kind: ConfigMap
metadata:
  name: quarkushop-order-config
data:
  application.properties: |-
    quarkus.datasource.jdbc.url=jdbc:postgresql://postgres:5432/order
    mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
    mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm
    quarkus.log.handler.gelf.enabled=true
    quarkus.log.handler.gelf.host=quarkushop-logstash     ①
    quarkus.log.handler.gelf.port=12201                   ②

Listing 14-3quarkushop-order-config.yml

让我们构建、推送容器,并再次部署到我们的 Kubernetes 集群。我们需要再次导入ConfigMaps来更新它们。

步骤 3:收集日志

一旦部署和配置好一切,我们需要访问 Kibana UI 来解析收集到的日志:

kubectl port-forward service/quarkushop-kibana 5601

转到管理➤堆栈管理➤指数管理➤基巴纳➤指数模式:

img/509649_1_En_14_Fign_HTML.jpg

从这里,单击创建索引模式以创建新的索引模式。将出现一个新屏幕:

img/509649_1_En_14_Figo_HTML.jpg

logstash-*填充索引模式名称字段,并点击下一步:

img/509649_1_En_14_Figp_HTML.jpg

在下一个屏幕上,为时间字段选择@timestamp,并点击创建索引模式:

img/509649_1_En_14_Figq_HTML.jpg

将创建一个新的索引模式,并出现一个确认屏幕:

img/509649_1_En_14_Figr_HTML.jpg

现在,如果您单击 Kibana 部分中的 Discover 菜单,您将看到日志列表:img/509649_1_En_14_Figs_HTML.gif

img/509649_1_En_14_Figt_HTML.png

太棒了。现在,我们可以密切关注所有微服务中生成的所有日志。我们可以享受 ELK stack 的强大功能。例如,我们可以创建一个自定义查询来监控日志流中特定种类的错误。

实现分布式跟踪模式

分布式跟踪模式在 Quarkus 中有一个名为quarkus-smallrye-opentracing的专用扩展。img/509649_1_En_14_Figv_HTML.gif就像日志聚合模式一样,我们需要一个分布式跟踪模式的分布式跟踪系统。

分布式跟踪系统用于收集和存储微服务架构中监控通信请求所需的定时数据,以便检测延迟问题。市面上有很多分布式追踪系统,比如 Zipkin 和 Jaeger。在本书中,我们将使用 Jaeger,因为它是由quarkus-smallrye-opentracing扩展支持的默认跟踪器。

我们需要安装 Jaeger 并配置微服务来支持它,以便收集请求跟踪。

在开始安装之前,以下是 Jaeger 生态系统的组成部分:

  • Jaeger Client :包含用于分布式跟踪的 OpenTracing API 的特定语言实现。

  • Jaeger Agent :监听 UDP 上发送的 SPANS 的网络守护程序。

  • Jaeger Collector :接收跨度并将其放入队列进行处理。这允许收集器立即返回到客户端/代理,而不是等待 SPAN 进入存储。

  • 查询:从存储中检索踪迹的服务。

  • Jaeger 控制台:一个用户界面,让你可视化你的分布式追踪数据。

Jaeger 组件架构如下所示:

img/509649_1_En_14_Figw_HTML.jpg

步骤 1:将 Jaeger 一体机部署到 Kubernetes

我们首先用清单 14-4 中显示的内容创建jaeger-deployment.yml文件。

apiVersion: apps/v1
kind: Deployment
metadata:
    labels:
        app: jaeger
        app.kubernetes.io/component: all-in-one
        app.kubernetes.io/name: jaeger
    name: jaeger
spec:
    progressDeadlineSeconds: 2147483647
    replicas: 1
    revisionHistoryLimit: 2147483647
    selector:
        matchLabels:
            app: jaeger
            app.kubernetes.io/component: all-in-one
            app.kubernetes.io/name: jaeger
    strategy:
        type: Recreate
    template:
        metadata:
            annotations:
                prometheus.io/port: "16686"
                prometheus.io/scrape: "true"
            labels:
                app: jaeger
                app.kubernetes.io/component: all-in-one
                app.kubernetes.io/name: jaeger
        spec:
            containers:
                - env:
                      - name: COLLECTOR_ZIPKIN_HTTP_PORT
                        value: "9411"
                  image: jaegertracing/all-in-one
                  imagePullPolicy: Always
                  name: jaeger
                  ports:
                      - containerPort: 5775
                        protocol: UDP
                      - containerPort: 6831
                        protocol: UDP
                      - containerPort: 6832
                        protocol: UDP
                      - containerPort: 5778
                        protocol: TCP
                      - containerPort: 16686
                        protocol: TCP
                      - containerPort: 9411
                        protocol: TCP
                  readinessProbe:
                      failureThreshold: 3
                      httpGet:
                          path: /
                          port: 14269
                          scheme: HTTP
                      initialDelaySeconds: 5
                      periodSeconds: 10
                      successThreshold: 1
                      timeoutSeconds: 1
                  resources: {}
                  terminationMessagePath: /dev/termination-log
                  terminationMessagePolicy: File
            dnsPolicy: ClusterFirst
            restartPolicy: Always
            schedulerName: default-scheduler
            securityContext: {}
            terminationGracePeriodSeconds: 30

Listing 14-4jaeger/jaeger-deployment.yml

接下来,将该文件导入 Kubernetes 集群:

kubectl apply -f jaeger/jaeger-deployment.yml

这个Deployment资源将在一个容器中部署所有 Jaeger 后端组件和 UI。

我们现在需要创建一个名为jaeger-query的负载平衡的 Kubernetes 服务对象,如清单 14-5 所示。

apiVersion: v1
kind: Service
metadata:
    name: jaeger-query
    labels:
        app: jaeger
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: query
spec:
    ports:
        - name: query-http
          port: 80
          protocol: TCP
          targetPort: 16686
    selector:
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: all-in-one
    type: LoadBalancer

Listing 14-5jaeger/jaeger-query-service.yml

我们还需要创建另一个名为jaeger-collector的服务,如清单 14-6 所示。

apiVersion: v1
kind: Service
metadata:
    name: jaeger-collector
    labels:
        app: jaeger
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: collector
spec:
    ports:
        - name: jaeger-collector-tchannel
          port: 14267
          protocol: TCP
          targetPort: 14267
        - name: jaeger-collector-http
          port: 14268
          protocol: TCP
          targetPort: 14268
        - name: jaeger-collector-zipkin
          port: 9411
          protocol: TCP
          targetPort: 9411
    selector:
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: all-in-one
    type: ClusterIP

Listing 14-6jaeger/jaeger-collector-service.yml

清单 14-7 显示了我们需要创建的最后一个,名为jaeger-agent

apiVersion: v1
kind: Service
metadata:
    name: jaeger-agent
    labels:
        app: jaeger
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: agent
spec:
    ports:
        - name: agent-zipkin-thrift
          port: 5775
          protocol: UDP
          targetPort: 5775
        - name: agent-compact
          port: 6831
          protocol: UDP
          targetPort: 6831
        - name: agent-binary
          port: 6832
          protocol: UDP
          targetPort: 6832
        - name: agent-configs
          port: 5778
          protocol: TCP
          targetPort: 5778
    clusterIP: None
    selector:
        app.kubernetes.io/name: jaeger
        app.kubernetes.io/component: all-in-one

Listing 14-7jaeger/jaeger-agent-service.yml

我们通过 TCP 和 UDP 协议为 Jaeger 代理公开了许多端口。UDP 上的 6831 消耗跨度。这是我们用来和杰格特工联系的。

接下来,让我们创建 Kubernetes 对象:

kubectl apply -f jaeger/jaeger-query-service.yml
kubectl apply -f jaeger/jaeger-collector-service.yml
kubectl apply -f jaeger/jaeger-agent-service.yml

现在很好,让我们检查一下 Jaeger 是否安装正确。我们需要对jaeger-query服务执行port-forward:

kubectl port-forward service/jaeger-query 8888:80

然后打开localhost:8888,如截图所示:

img/509649_1_En_14_Figx_HTML.jpg

太棒了。我们现在可以转到微服务配置。img/509649_1_En_14_Figy_HTML.gif

步骤 2:在我们的微服务中启用 Jaeger 支持

第一步是将quarkus-smallrye-opentracing依赖性添加到我们的微服务中:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-opentracing</artifactId>
</dependency>

然后,我们需要定义每个微服务的 Jaeger 配置:

 1 apiVersion: v1
 2 kind: ConfigMap
 3 metadata:
 4   name: quarkushop-user-config
 5 data:
 6   application.properties: |-
 7     mp.jwt.verify.publickey.location=http://keycloak-http.keycloak/auth/realms/quarkushop-realm/protocol/openid-connect/certs
 8     mp.jwt.verify.issuer=http://keycloak-http.keycloak/auth/realms/quarkushop-realm
 9     quarkus.log.handler.gelf.enabled=true
10     quarkus.log.handler.gelf.host=quarkushop-logstash
11     quarkus.log.handler.gelf.port=12201
12     quarkus.jaeger.service-name=quarkushop-user
13     quarkus.jaeger.sampler-type=const
14     quarkus.jaeger.sampler-param=1
15     quarkus.log.console.format=%d{HH:mm:ss} %-5p traceId=%X{traceId}, spanId=%X{spanId}, sampled=%X{sampled} [%c{2.}] (%t) %s%e%n
16     quarkus.jaeger.agent-host-port=jaeger-agent:6831

新的 Jaeger 特性如下:

  • quarkus.jaeger.service-name:服务名,是微服务用来向 Jaeger 服务器呈现自己的名称。

  • quarkus.jaeger.sampler-type:示例中的采样器类型为const。我们将不断发送quarkus.jaeger.sampler-param中定义的配额。

  • quarkus.jaeger.sampler-param:在 0 和 1 之间定义的样本配额,其中 1 表示 100%的请求。

  • quarkus.log.console.format:在日志消息中添加跟踪 id。

  • quarkus.jaeger.agent-host-port:通过 UDP 与 Jaeger 代理通信的主机名和端口。我们将它指向作为主机的jaeger-agent和作为端口的 6831。

很好。让我们构建、推送容器,并再次将它们部署到我们的 Kubernetes 集群。我们还需要再次导入ConfigMaps来更新它们。

步骤 3:收集痕迹

部署 Jaeger 服务器和更新微服务后,我们需要发出一些请求来生成踪迹。然后我们就能看到耶格在抓什么了。

例如,我们可以使用quarkushop-user向 Keycloak 请求一个access_token:

kubectl port-forward service/quarkushop-user 8080

然后运行一个curlquarkushop-user微服务请求access_token:

curl -X POST "http://localhost:8080/api/user/access-token?password=password&username=nebrass"

很好!让我们做一个port-forward并访问 Jaeger,看看那里发生了什么:

kubectl port-forward service/jaeger-query 8888:80

然后打开localhost:8888,如截图所示:

img/509649_1_En_14_Figz_HTML.jpg

正如您在服务部分看到的,有三个元素。只需选择quarkushop-user并点击查找痕迹:

img/509649_1_En_14_Figaa_HTML.jpg

显示了一个跨度:

img/509649_1_En_14_Figab_HTML.jpg

如果您点按它,会出现更多详细信息:

img/509649_1_En_14_Figac_HTML.jpg

您可以看到此处显示了请求的所有详细信息:

  • 网址

  • HTTP 动词

  • 持续时间

  • 诸如此类;一切都在这里img/509649_1_En_14_Figad_HTML.gif

搞定了。我们以一种非常有效和简单的方式在 Quarkus 中实现了分布式跟踪模式!我真的很开心!img/509649_1_En_14_Figae_HTML.gif

实现 API 网关模式

一个 API 网关是一个位于 API 前面的编程门面,充当一组定义的微服务的单一入口点。

为了实现 Kubernetes,一个入口管理对集群中服务的外部访问,通常是facadeHTTP。Ingress 可以提供负载平衡、SSL 终止和基于名称的虚拟主机。

img/509649_1_En_14_Figaf_HTML.jpg

一个入口是允许入站连接到达集群服务的规则集合。它可以被配置为向服务提供外部可达的 URL、负载平衡的流量、终止 SSL、基于名称的虚拟主机等等。

一个入口控制器负责实现入口,通常带有一个负载平衡器,尽管它也可以配置您的边缘路由器或附加前端,以帮助以高可用性的方式处理流量。

让我们将 API 网关模式引入 Kubernetes。img/509649_1_En_14_Figag_HTML.gif

步骤 1:在 Minikube 中启用入口支持

第一步是在 Minikube 中启用入口支持。对于那些使用真正的 Kubernetes 集群的人(幸运的人)来说,这一步是不需要的。img/509649_1_En_14_Figah_HTML.gif

要在 Minikube 中启用入口支持,只需启动您的minikube实例,然后运行以下命令:

minikube addons enable ingress

Ingress 是 Minikube 的附加产品。img/509649_1_En_14_Figaj_HTML.gif

我们需要一个入口域名;让我们使用quarkushop.io域名。我们通过键入以下命令获得 Minikube 的 IP 地址:

$ minikube ip
192.168.39.243

然后,我们需要在这个 IP 地址的/etc/hosts文件中添加一个新条目,以便将其用于我们的自定义域quarkushop.io:

192.168.39.243  quarkushop.io

该自定义内部 DNS 条目将对目标192.168.39.243进行任何调用。img/509649_1_En_14_Figak_HTML.gif

步骤 2:创建 API 网关入口

Ingress 将指向我们的四种微服务:

  • quarkushop-product

  • quarkushop-order

  • quarkushop-customer

  • quarkushop-user

img/509649_1_En_14_Figal_HTML.jpg

入口描述符如清单 14-8 所示。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
  - http:
      paths:
      - path: /user
        pathType: Prefix
        backend:
          service:
            name: quarkushop-user
            port:
              number: 8080
      - path: /product
        pathType: Prefix
        backend:
          service:
            name: quarkushop-product
            port:
              number: 8080
      - path: /order
        pathType: Prefix
        backend:
          service:
            name: quarkushop-order
            port:
              number: 8080
      - path: /customer
        pathType: Prefix
        backend:
          service:
            name: quarkushop-customer
            port:
              number: 8080

Listing 14-8api-gateway-ingress.yml

只需将该内容保存到api-gateway-ingress.yml并使用以下命令创建资源:

kubectl create -f api-gateway-ingress.yml

入口已成功创建!让我们检查一下:

$ kubectl get ingress

NAME          CLASS    HOSTS           ADDRESS          PORTS   AGE
api-gateway   <none>   quarkushop.io   192.168.39.243   80      7m46s

太棒了!如你所见,ADDRESS与 Minikube IP 相同。img/509649_1_En_14_Figam_HTML.gif

步骤 3:测试入口

现在我们可以享受我们的入口了。我们可以用它向quarkushop-user微服务请求一个access_token:

$ curl -X POST "http://quarkushop.io/user/api/user/access-token?password=password&username=nebrass"

eyJhbGciOiJSUzI1NiIsIn...

万岁!我们收到了access_token请求!img/509649_1_En_14_Figan_HTML.gif Ingress 非常好用!img/509649_1_En_14_Figao_HTML.gif

结论

在这一章中,我们使用不同的 Quarkus 扩展和 Kubernetes 对象实现了许多模式。这个任务非常简单,特别是因为我们将许多任务委托给了 Kubernetes。

Hakuna matata!img/509649_1_En_14_Figap_HTML.gif我们成功实施了云原生微服务。我对可用的扩展和提供的优秀文档感到非常高兴。

posted @ 2024-10-01 21:04  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报