注:
- Oracle 提供的免费实验室环境中提供了本教程。
- 它使用 Oracle Cloud Infrastructure 身份证明、租户和区间示例值。完成实验室后,请使用特定于您的云环境的这些值替换这些值。
GraalVM Native Image、Spring 和容器化
简介
此实验室面向开发人员,希望了解有关如何容器化 GraalVM Native Image 应用的更多信息。
GraalVM Native Image 技术可提前将 Java 代码编译为原生可执行文件。可执行文件中仅包含在应用程序运行时所需的代码。
本机映像生成的可执行文件具有以下几个重要优点:
- 使用 JVM 所需的一小部分资源,因此运行成本更低
- 开始时间(以毫秒为单位)
- 立即提供峰值性能,无需热身
- 可以打包到轻量级容器映像,提升部署速度和效率
- 展示减少的攻击面(在将来的实验室中详述)
许多先进的微服务框架都支持使用 GraalVM Native Image 进行提前编译,包括 Micronaut、Spring、Helidon 和 Quarkus。
此外,还有适用于 Native Image 的 Maven 和 Gradle 插件,因此您可以轻松构建、测试并将 Java 应用程序作为可执行文件运行。
注意:Oracle Cloud Infrastructure (OCI) 无需额外付费即可提供 GraalVM 企业版。
估计的实验室时间:90 分钟
实验室目标
在此实验室中,您将:
- 将基本 Spring 引导应用程序添加到 Docker 映像并运行该应用程序
- 使用 GraalVM Native Image 从此应用程序构建原生可执行文件
- 将本机可执行文件添加到 Docker 映像
- 使用 GraalVM Native Image 和 Distroless 容器缩小应用 docker 映像大小
- 了解如何使用 GraalVM 原生构建工具 Maven 插件
注意:如果在实验室中看到笔记本电脑图标,这意味着您需要执行一些操作,例如输入命令。留意

# This is where you will need to do something
STEP 1:连接到虚拟主机并检查开发环境
您的开发环境由远程主机提供:具有 Oracle Linux 8、4 个核心和 32GB 内存的 OCI 计算实例。在远程主机准备就绪之前将显示 Luna Labs 桌面环境,这最多需要两分钟。
您可以通过在 Luna Desktop 环境中运行设置脚本来连接到远程主机。此脚本可通过“resource(资源)”选项卡访问。
-
双击桌面上的 Luna Lab 图标以打开浏览器。

-
将显示资源选项卡。请注意,在云中预配计算实例时,资源标题旁边的齿轮将旋转。

-
预配实例时(这至多需要 2 分钟),您将在资源选项卡上看到以下内容。

-
从“资源”选项卡复制用于设置 VS 代码环境的配置脚本。单击查看详细信息链接可显示配置。复制此项,如下面的屏幕截图中所示:

-
打开终端,如下面的屏幕截图中所示:

-
将配置代码粘贴到终端中,终端将打开 VS 代码。


-
VS Code 窗口将打开并自动连接到为您预配的 VM 实例。单击继续接受计算机指纹。

结束了!祝贺您,您现在已成功连接到 Oracle Cloud 中的远程主机!
以上脚本将打开 VS 代码,该代码通过打开的实验室的源代码连接到远程计算实例。
接下来,您需要打开 VS 代码内的终端。此终端将允许您与远程主机进行交互。可以通过以下菜单在 VS Code 中打开终端:Terminal > New Terminal(新建终端)。

我们将在实验室的其余部分使用此终端。
关于开发环境的说明
我们将 GraalVM Enterprise 22 用作此实验的 Java 环境。GraalVM 是来自 Oracle 的高性能 JDK 分销商。它基于可信的安全 Oracle Java SE 构建。
您的开发环境预配置了 GraalVM 和本机操作所需的原生映像工具。
您可以通过在终端中运行以下命令轻松检查:

java -version

native-image --version
步骤 2:满足示例 Java 应用程序
在此实验室中,您将使用基于 REST 的极少 API 构建简单的应用。然后,您将使用 Docker 将此应用容器化。首先,快速查看您的简单应用程序。
我们提供了此应用程序的源代码和构建脚本,包含源代码的文件夹将在 VS Code 中打开。
应用程序基于 Spring Boot 框架构建,并利用 Spring Native Project(使用 GraalVM Native Image 生成本机可执行文件的 Spring 收发器)。
应用程序有两个类,可在 src/main/java 中找到:
com.example.demo.DemoApplication:也定义 HTTP 端点的主 Spring Boot 类/jibbercom.example.demo.Jabberwocky:实施应用程序逻辑的实用程序类
那么,应用程序的工作原理是什么?如果您调用在应用程序中定义的端点 REST /jibber,它将返回 Lewis Carroll 以 Jabberwocky poem 样式生成的一些无意义的动词。程序通过使用 Markov Chain 为原始诗(本质上是一个统计模型)建模来实现这一点。此模型生成新文本。
在示例应用程序中,我们向应用程序提供诗词的文本,然后生成文本模型,应用程序随后使用该模型生成类似于原始文本的新文本。我们正在使用 RiTa 库来完成繁重的工作 - 它支持构建和使用 Markov Chains。
下面是生成模型的实用程序类 com.example.demo.Jabberwocky 中的两个片段。text 变量包含原始诗歌的文本。此片段展示了如何创建模型,然后使用 text 填充模型。这从类构造器调用,我们将类定义为 Singleton(因此,仅创建类的一个实例)。
this.r = new RiMarkov(3);
this.r.addText(text);
您可以在此处查看根据原始文本从模型生成新动词行的方法。
public String generate() {
String[] lines = this.r.generate(10);
StringBuffer b = new StringBuffer();
for (int i=0; i< lines.length; i++) {
b.append(lines[i]);
b.append("<br/>\n");
}
return b.toString();
}
请花一些时间查看代码并了解代码。
要构建应用程序,您将使用 Maven。pom.xml 文件是使用 Spring Initializr 生成的,包含使用 Spring Native 工具的支持。如果您计划将 GraalVM Native Image 作为目标,则此依赖关系会添加到 Spring Boot 项目中。如果使用的是 Maven,添加对 Spring Native 的支持会将以下插件插入到默认构建配置中。
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>${spring-native.version}</version>
<executions>
<execution>
<id>test-generate</id>
<goals>
<goal>test-generate</goal>
</goals>
</execution>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
现在构建应用程序。从系统信息库的根目录,在 shell 中运行以下命令:

mvn clean package
这将生成“可执行”JAR 文件,该文件包含应用程序的所有依赖项以及正确配置的 MANIFEST 文件。您可以运行此 JAR 文件,然后“ping”应用程序的端点以查看返回的内容 - 使用 & 将命令放入后台,以便返回提示符。

java -jar ./target/jibber-0.0.1-SNAPSHOT-exec.jar &
使用命令行中的 curl 命令调用端点。
将命令发布到终端中时,VS 代码可能会提示您在浏览器中打开 URL,只需关闭对话框即可,如下所示。

运行以下命令测试 HTTP 端点:

curl http://localhost:8080/jibber
你回到了什么废话吗?因此,您构建了一个可以工作的应用,终止该应用并继续进行容器化。将应用带到前台,以便您可以终止它。

fg
输入 <ctrl-c> 以立即终止应用程序。

<ctrl-c>
步骤 3:使用 Docker 容器化 Java 应用
将您的 Java 应用容器化为 Docker 容器可以非常感谢,而且相对简单。您可以基于包含 JDK 分布的映像构建新的 Docker 映像。因此,对于此实验,您将使用已包含 JDK container-registry.oracle.com/java/openjdk:17-oraclelinux8 的容器 - 这是具有 OpenJDK 的 Oracle Linux 8 映像。
下面是 Dockerfile 的细分,介绍如何构建 Docker 映像。请参见注释以说明内容。
FROM container-registry.oracle.com/java/openjdk:17-oraclelinux8 # Base Image
ARG JAR_FILE # Pass in the JAR file as an argument to the image build
EXPOSE 8080 # This image will need to expose TCP port 8080, as this is the port on which your app will listen
COPY ${JAR_FILE} app.jar # Copy the JAR file from the `target` directory into the root of the image
ENTRYPOINT ["java"] # Run Java when starting the container
CMD ["-jar","app.jar"] # Pass in the parameters to the Java command that make it load and run your executable JAR file
可以在目录 00-containerise 中找到用于容器化 Java 应用程序的 Dockerfile。
要构建包含应用的 Docker 映像,请从终端运行以下命令:

docker build -f ./00-containerise/Dockerfile \
--build-arg JAR_FILE=./target/jibber-0.0.1-SNAPSHOT-exec.jar \
-t localhost/jibber:java.01 .
查询 Docker 以查看新构建的映像:

docker images | head -n2
您应该会看到列出的新图像。按如下方式运行此图像:

docker run --rm -d --name "jibber-java" -p 8080:8080 localhost/jibber:java.01
然后,与使用 curl 命令之前一样调用端点:

curl http://localhost:8080/jibber
你看过胡说八道吗?现在检查您的应用程序启动时间有多长。可以从日志中提取此项,因为 Spring Boot 应用程序将时间写入到启动日志:

docker logs jibber-java
例如,应用程序在 3.896s 中启动。以下是从日志中提取的:

2022-03-09 19:48:09.511 INFO 1 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 3.896 seconds (JVM running for 4.583)
好的,终止您的容器并移至以下位置:

docker kill jibber-java
您还可以查询 Docker 来获取映像的大小。我们为您提供了一个脚本。在终端中运行以下命令:

echo $((`docker inspect -f "" localhost/jibber:java.01`/1024/1024))
这会输出映像的大小(以 MB 为单位),即 606 MB。
STEP 4:生成本地可执行文件
回顾到目前为止已有的内容:
- 您使用 HTTP 端点
/jibber构建了 Spring Boot 应用程序 - 您已成功将其容器化
现在,您将了解如何使用 GraalVM Native Image 从应用创建本地可执行文件。此本地可执行文件将具有许多有趣的特征,即:
- 会很快开始的
- 它使用的资源少于相应的 Java 应用程序的资源
可以使用随 GraalVM 一起安装的原生映像工具从命令行构建应用程序的原生可执行文件。但是,随着 Maven 的使用,您将应用 GraalVM Native Build Tools for Maven,这样可以方便地继续使用 Maven 进行构建。
添加对构建本机可执行文件的支持的一种方法是使用 Maven profile,这允许您决定是仅构建 JAR 文件还是本机可执行文件。
在提供的 Maven pom.xml 文件中,已添加用于构建本地可执行文件的配置文件。进一步了解:
首先,您需要声明配置文件并为其指定名称。
<profiles>
<profile>
<id>native</id>
<!-- Rest of profile hidden, to highlight relevant parts -->
</profile>
</profiles>
接下来,在配置文件中加入 GraalVM Native Image 构建工具插件,并将其附加到 Maven 中的 package 阶段。这意味着它将在 package 阶段运行。请注意,可以使用 <buildArgs> 部分将配置参数传递到底层本机映像构建工具。在单个 buildArg 标记中,可以像传递 native-image 工具一样传递参数。因此,您可以使用 native-image 工具的所有参数:
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-buildtools.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<imageName>jibber</imageName>
<buildArgs>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
<!-- Rest of profile hidden, to high-light relevant parts -->
</plugins>
</build>
现在使用配置文件运行 Maven 构建,如下所示(请注意使用 -P 标志指定配置文件名称):

mvn package -Pnative
这将为 target 目录(称为 jibber)中的平台生成本地可执行文件。查看文件的大小:

ls -lh target/jibber
运行此本机可执行文件并对其进行测试。在终端中执行以下命令,运行本机可执行文件并使用 & 将其放入后台:

./target/jibber &
使用 curl 命令调用端点:

curl http://localhost:8080/jibber
现在,您拥有一个真正快速启动的原生应用可执行文件!
在继续操作之前终止应用程序。将应用程序引入前台:

fg
使用 <ctrl-c> 终止:

<ctrl-c>
步骤 5:容器化本机可执行文件
现在,由于您拥有应用的原生可执行文件版本,并且您已经看到了它可以正常运行,因而可以将其容器化。
我们提供了用于打包此本机可执行文件的简单 Dockerfile:该文件位于目录 native-image/containerisation/lab/01-native-image/Dockerfile 中。下面显示了内容以及说明每行的注释。
FROM container-registry.oracle.com/os/oraclelinux:8-slim
ARG APP_FILE # Pass in the native executable
EXPOSE 8080 # This image will need to expose TCP port 8080, as this is port your app will listen on
COPY ${APP_FILE} app # Copy the native executable into the root directory and call it "app"
ENTRYPOINT ["/app"] # Just run the native executable :)
要生成,请从终端运行以下命令:

docker build -f ./01-native-image/Dockerfile \
--build-arg APP_FILE=./target/jibber \
-t localhost/jibber:native.01 .
查看新构建的映像:

docker images | head -n2
现在,您可以运行此命令,然后按以下方式从终端进行测试:

docker run --rm -d --name "jibber-native" -p 8080:8080 localhost/jibber:native.01
使用 curl 从终端调用端点:

curl http://localhost:8080/jibber
再说一遍,诗歌《 Jabberwocky 》风格中也该见到更多的胡说八道。您可以查看应用之前生成的日志,了解应用的启动时间。在终端中,运行以下命令并查找启动时间:

docker logs jibber-native
我们看到以下内容,说明应用程序在 0.074s 中启动。与原来的 3.896s 相比,这有了很大的改进!
2022-03-09 19:44:12.642 INFO 1 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.074 seconds (JVM running for 0.081)
终止容器并移至下一步:

docker kill jibber-native
但在下一步之前,请先了解一下生成的容器的大小:

echo $((`docker inspect -f "" localhost/jibber:native.01`/1024/1024))
我们看到的容器映像大小为 199 MB。比起原始 Java 容器小得多。
STEP 6:生成最静态可执行文件并将其打包在无限图像中
再次总结,到目前为止所做的:
- 您使用 HTTP 端点
/jibber构建了 Spring Boot 应用程序 - 您已成功将其容器化
- 您使用适用于 Maven 的原生映像构建工具构建了应用的原生可执行文件
- 您的原生可执行文件已经过容器化
如果进一步收缩您的容器大小,会很有帮助,因为小型容器可以更快地下载并启动。通过 GraalVM Native Image,可以将系统库静态链接到您生成的本机可执行文件中。如果您构建静态链接的本机可执行文件,可以直接将本机可执行文件打包到空的 Docker 映像(也称为 scratch 容器)中。
另一个选项是生成称为主要链接的本机可执行文件。通过此功能,可以在除标准 C 库 glibc 之外的所有系统库中静态链接。通过这种原生可执行文件,您可以使用小型容器,例如包含 glibc 库的 Google Distroless、一些标准文件和 SSL 安全证书。标准无失控容器大小约为 20MB。
您将构建一个主要链接的可执行文件,然后将其打包到无过失容器中。
我们添加了另一个 Maven 概要信息来构建此主要链接的本机可执行文件。此配置文件名为 distroless。此配置文件与之前使用的配置文件 native 的唯一区别是传递参数 -H:+StaticExecutableWithDynamicLibC。您可能猜测,这指示 native-image 构建大多数链接的本机可执行文件。
您可以按如下方式构建大多数静态链接的本机可执行文件:

mvn package -Pdistroless
非常简单。生成的本机可执行文件位于目标目录 jibber-distroless 中。
现在将其打包成无主容器。要执行此操作的 Dockerfile 可以在目录 native-image/containerisation/lab/02-smaller-containers/Dockerfile 中找到。请看 Dockerfile 的内容,其中包含对每一行进行说明的注释:
FROM gcr.io/distroless/base # Our base image, which is Distroless
ARG APP_FILE # Everything else is the same :)
EXPOSE 8080
COPY ${APP_FILE} app
ENTRYPOINT ["/app"]
要生成,请从终端运行以下命令:

docker build -f ./02-smaller-containers/Dockerfile \
--build-arg APP_FILE=./target/jibber-distroless \
-t localhost/jibber:distroless.01 .
查看新构建的单调图像:

docker images | head -n2
现在,您可以按如下方式运行和测试:

docker run --rm -d --name "jibber-distroless" -p 8080:8080 localhost/jibber:distroless.01

curl http://localhost:8080/jibber
太好了!成功了。但是,您的容器大小(或大小)是多少?使用脚本检查图像大小:

echo $((`docker inspect -f "" localhost/jibber:distroless.01`/1024/1024))
大小约是 107MB!因此容器拆了 92MB。从我们的起始大小(对于 Java 容器)减去大约 600MB。
结论
我们希望您喜欢这个实验室,并学习了几件事情。我们了解了如何将 Java 应用容器化。接下来,我们学习了如何将该 Java 应用转换为原生可执行文件,比 Java 应用快得多。然后,我们容器化了原生可执行文件,并发现 Docker 映像的大小(包含本地可执行文件)比 Java Docker 映像小得多。
最后,我们研究了如何构建大多数静态链接的本机可执行文件以及 Native Image。这些应用可以打包在较小的容器中(例如 Distroless),这样我们还可以进一步缩小 Docker 映像的大小。
了解更多
- 观看本机映像架构师 Christian Wimmer 的演示 GraalVM Native Image:针对 Java 的大规模静态分析
- GraalVM Native Image 参考文档
更多学习资源
在 docs.oracle.com/learn 上浏览其他实验室,或者在 Oracle Learning YouTube 渠道上访问更多免费学习内容。此外,访问 education.oracle.com/learning-explorer 以成为 Oracle Learning Explorer。
有关产品文档,请访问 Oracle 帮助中心。
GraalVM Native Image, Spring and Containerisation
F54858-01
March 2022
Copyright © 2022, Oracle and/or its affiliates.