注:

GraalVM Native Image、Spring 和容器化

简介

此实验室面向开发人员,希望了解有关如何容器化 GraalVM Native Image 应用的更多信息。

GraalVM Native Image 技术可提前将 Java 代码编译为原生可执行文件。可执行文件中仅包含在应用程序运行时所需的代码。

本机映像生成的可执行文件具有以下几个重要优点:

许多先进的微服务框架都支持使用 GraalVM Native Image 进行提前编译,包括 Micronaut、Spring、Helidon 和 Quarkus。

此外,还有适用于 Native Image 的 Maven 和 Gradle 插件,因此您可以轻松构建、测试并将 Java 应用程序作为可执行文件运行。

注意:Oracle Cloud Infrastructure (OCI) 无需额外付费即可提供 GraalVM 企业版。

估计的实验室时间:90 分钟

实验室目标

在此实验室中,您将:

注意:如果在实验室中看到笔记本电脑图标,这意味着您需要执行一些操作,例如输入命令。留意

# This is where you will need to do something

STEP 1:连接到虚拟主机并检查开发环境

您的开发环境由远程主机提供:具有 Oracle Linux 8、4 个核心和 32GB 内存的 OCI 计算实例。在远程主机准备就绪之前将显示 Luna Labs 桌面环境,这最多需要两分钟。

您可以通过在 Luna Desktop 环境中运行设置脚本来连接到远程主机。此脚本可通过“resource(资源)”选项卡访问。

  1. 双击桌面上的 Luna Lab 图标以打开浏览器。

    Luna 桌面图标

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

    Luna 资源选项卡

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

    Luna 资源标签

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

    复制配置脚本

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

    打开终端

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

    粘贴终端 1

    粘贴终端 2

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

    接受 VS 代码

结束了!祝贺您,您现在已成功连接到 Oracle Cloud 中的远程主机!

以上脚本将打开 VS 代码,该代码通过打开的实验室的源代码连接到远程计算实例。

接下来,您需要打开 VS 代码内的终端。此终端将允许您与远程主机进行交互。可以通过以下菜单在 VS Code 中打开终端:Terminal > New Terminal(新建终端)

VS 代码终端

我们将在实验室的其余部分使用此终端。

关于开发环境的说明

我们将 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 中找到:

那么,应用程序的工作原理是什么?如果您调用在应用程序中定义的端点 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,只需关闭对话框即可,如下所示。

VS 代码

运行以下命令测试 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:生成本地可执行文件

回顾到目前为止已有的内容:

  1. 您使用 HTTP 端点 /jibber 构建了 Spring Boot 应用程序
  2. 您已成功将其容器化

现在,您将了解如何使用 GraalVM Native Image 从应用创建本地可执行文件。此本地可执行文件将具有许多有趣的特征,即:

  1. 会很快开始的
  2. 它使用的资源少于相应的 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:生成最静态可执行文件并将其打包在无限图像中

再次总结,到目前为止所做的:

  1. 您使用 HTTP 端点 /jibber 构建了 Spring Boot 应用程序
  2. 您已成功将其容器化
  3. 您使用适用于 Maven 的原生映像构建工具构建了应用的原生可执行文件
  4. 您的原生可执行文件已经过容器化

如果进一步收缩您的容器大小,会很有帮助,因为小型容器可以更快地下载并启动。通过 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 映像的大小。

了解更多

更多学习资源

docs.oracle.com/learn 上浏览其他实验室,或者在 Oracle Learning YouTube 渠道上访问更多免费学习内容。此外,访问 education.oracle.com/learning-explorer 以成为 Oracle Learning Explorer。

有关产品文档,请访问 Oracle 帮助中心