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

145 阅读32分钟

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.38.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 {
    @PrePersistvoid preCreate(AbstractEntity auditable) {
        Instant now = Instant.now();
        auditable.setCreatedDate(now);
        auditable.setLastModifiedDate(now);
    }

    @PreUpdatevoid 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 数据查询方法构建器机制自动实现查询。

@Repositorypublic 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@Transactionalpublic 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 管道构建和部署您的应用。