Native Image 捆绑包

Native Image 提供了一项功能,允许用户从自包含的bundle构建原生可执行文件。与常规的native-image构建不同,此操作模式仅将单个*.nib文件作为输入。该文件包含构建原生可执行文件(或原生共享库)所需的一切。当包含许多输入文件(JAR 文件、配置文件、自动生成的文件、下载的文件)的大型应用程序需要在以后某个时间点重新构建,而无需担心所有文件是否仍然可用时,这会非常有用。通常,复杂的构建涉及下载许多库,这些库不保证在以后仍然可访问。使用 Native Image bundles 是一种安全的解决方案,可以将构建所需的所有输入封装到一个文件中。

注意:此功能是实验性的。

目录 #

创建 Bundle #

要创建 Bundle,请传递 --bundle-create 选项以及特定 native-image 命令行调用的其他参数。这将使 native-image 除了实际的镜像之外,还会创建一个 *.nib 文件。

以下是选项说明

--bundle-create[=new-bundle.nib][,dry-run][,container[=<container-tool>][,dockerfile=<Dockerfile>]]
                      in addition to image building, create a Native Image bundle file (*.nib
                      file) that allows rebuilding of that image again at a later point. If a
                      bundle-file gets passed, the bundle will be created with the given
                      name. Otherwise, the bundle-file name is derived from the image name.
                      Note both bundle options can be extended with ",dry-run" and ",container"
                      * 'dry-run': only perform the bundle operations without any actual image building.
                      * 'container': sets up a container image for image building and performs image building
                        from inside that container. Requires podman or rootless docker to be installed.
                        If available, 'podman' is preferred and rootless 'docker' is the fallback. Specifying
                        one or the other as '=<container-tool>' forces the use of a specific tool.
                      * 'dockerfile=<Dockerfile>': Use a user provided 'Dockerfile' instead of the default based on
                        Oracle Linux 8 base images for GraalVM (see https://github.com/graalvm/container)

使用 Maven 创建 Bundle #

假设 Java 应用程序使用 Maven 构建,将 --bundle-create 作为构建参数传递到 用于 Native Image 构建配置的 Maven 插件中。

<plugin>
  <groupId>org.graalvm.buildtools</groupId>
  <artifactId>native-maven-plugin</artifactId>
  <configuration>
      <buildArgs combine.children="append">
          <buildArg>--bundle-create</buildArg>
      </buildArgs>
  </configuration>
</plugin>

然后,运行 Maven package 命令

./mvnw -Pnative native:compile

注意:为 Micronaut 项目使用 Maven 创建原生可执行文件的命令是:./mvnw package -Dpackaging=native-image

您将获得以下构建产物

Finished generating 'application' in 2m 0s.

Native Image Bundles: Bundle build output written to /application/target/application.output
Native Image Bundles: Bundle written to /application/target/application.nib

此输出表明您已创建了一个原生可执行文件 application 和一个 Bundle application.nib。Bundle 文件是在 target/ 目录中创建的。如果以后需要重新构建原生可执行文件,应将其复制到安全的地方以便找到。

使用 Gradle 创建 Bundle #

假设 Java 应用程序使用 Gradle 构建,将 --bundle-create 作为构建参数传递到 用于 Native Image 构建配置的 Gradle 插件中。

graalvmNative {
    binaries {
        main {
            buildArgs.add("--bundle-create")
        }
    }
}

然后,运行 Gradle 构建命令

./gradlew nativeCompile

您将获得以下构建产物

Finished generating 'application' in 2m 0s.

Native Image Bundles: Bundle build output written to /application/build/native/nativeCompile/application.output
Native Image Bundles: Bundle written to /application/build/native/nativeCompile/application.nib

此输出表明您已创建了一个原生可执行文件 application 和一个 Bundle application.nib。Bundle 文件是在 build/native/nativeCompile/ 目录中创建的。

Bundle 文件和输出目录 #

显然,Bundle 文件可能很大,因为它包含所有输入文件以及可执行文件本身(可执行文件在 Bundle 内部被压缩)。将原生镜像包含在 Bundle 中允许将从 Bundle 重新构建的原生可执行文件与原始文件进行比较。

Bundle 只是一个具有特定布局的 JAR 文件。这将在下文详细解释。要查看 Bundle 内部的内容,请运行

jar tf application.nib

在 Bundle 旁边,您还可以找到输出目录:application.output。它包含原生可执行文件以及作为构建一部分创建的所有其他文件。由于您未指定任何会产生额外输出的选项(例如,-g 用于生成调试信息或 --diagnostics-mode),因此那里只能找到可执行文件。

将 --bundle-create 与 dry-run 结合使用 #

正如 --bundle-create 选项描述中所述,也可以让 native-image 构建 Bundle 但不实际创建镜像。如果用户希望将 Bundle 移动到更强大的机器并在那里构建镜像,这可能会很有用。修改上面 Maven / Gradle Native Image 插件配置中的 --bundle-create 参数为 <buildArg>--bundle-create,dry-run</buildArg>。这样构建项目只需几秒钟,并且创建的 Bundle 会小得多。例如,检查 target/application.nib 的内容,并注意可执行文件不包含在内

jar tf application.nib
META-INF/MANIFEST.MF
META-INF/nibundle.properties
...

请注意,这次您在 Maven 输出中看不到以下消息

Native Image Bundles: Bundle build output written to /application/target/application.output

由于没有创建可执行文件,因此没有 Bundle 构建输出可用。

使用 Bundle 进行构建 #

假设原生可执行文件在生产中使用,并且偶尔会在运行时抛出意外异常。由于您仍然拥有用于创建可执行文件的 Bundle,因此使用调试支持构建该可执行文件的变体是微不足道的。像这样使用 --bundle-apply=application.nib

native-image --bundle-apply=application.nib -g

运行此命令后,可执行文件将从 Bundle 重新构建,并启用调试信息。

--bundle-apply 的完整选项帮助显示了一个更高级的用例,这将在稍后详细讨论

--bundle-apply=some-bundle.nib[,dry-run][,container[=<container-tool>][,dockerfile=<Dockerfile>]]
                      an image will be built from the given bundle file with the exact same
                      arguments and files that have been passed to native-image originally
                      to create the bundle. Note that if an extra --bundle-create gets passed
                      after --bundle-apply, a new bundle will be written based on the given
                      bundle arguments plus any additional arguments that have been passed
                      afterwards. For example:
                      > native-image --bundle-apply=app.nib --bundle-create=app_dbg.nib -g
                      creates a new bundle app_dbg.nib based on the given app.nib bundle.
                      Both bundles are the same except the new one also uses the -g option.

在容器中构建 #

--bundle-create--bundle-apply 选项的另一个补充是在容器镜像内部执行镜像构建。这确保了在镜像构建期间,native-image 无法访问未通过类路径或模块路径明确指定的任何资源。

修改上面 Maven / Gradle Native Image 插件配置中的 --bundle-create 参数为 <buildArg>--bundle-create,container<buildArg>。这仍然会像以前一样创建相同的 Bundle。然而,会构建一个容器镜像,然后用于构建原生可执行文件。

如果容器镜像新创建,您还可以看到来自容器工具的构建输出。容器镜像的名称是所使用的 Dockerfile 的哈希值。如果容器镜像已经存在,您将在构建输出中看到以下行

Native Image Bundles: Reusing container image c253ca50f50b380da0e23b168349271976d57e4e.

要在容器中构建,您的系统上需要安装 podmanrootless docker

目前只支持在 Linux 上进行容器化构建。使用任何其他操作系统,原生镜像将不会创建和使用容器镜像。

用于运行镜像构建的容器工具可以使用 <buildArg>--bundle-create,container=podman<buildArg><buildArg>--bundle-create,container=docker<buildArg> 进行指定。如果未指定,native-image 将使用其中一个受支持的工具。如果可用,优先使用 podman,而无根 docker 作为备用。

用于构建容器镜像的 Dockerfile 也可以通过 --bundle-create,container,dockerfile=<path-to-dockerfile> 明确指定。如果没有指定 Dockerfile,则使用默认的 Dockerfile,该文件基于 此处 的 GraalVM 的 Oracle Linux 8 容器镜像。最终用于构建容器镜像的 Dockerfile 将存储在 Bundle 中。即使您不使用 container 选项,native-image 也会创建一个 Dockerfile 并将其存储在 Bundle 中。

除了在主机系统上创建容器镜像外,在容器内构建不会产生任何额外的构建输出。但是,创建的 Bundle 包含一些附加文件

jar tf application.nib
META-INF/MANIFEST.MF
META-INF/nibundle.properties
...
input/stage/path_substitutions.json
input/stage/path_canonicalizations.json
input/stage/build.json
input/stage/run.json
input/stage/environment.json
input/stage/Dockerfile
input/stage/container.json

Bundle 包含用于构建容器镜像的 Dockerfile,并将所使用的容器工具、其版本和容器镜像的名称存储在 container.json 中。例如

{
    "containerTool":"podman",
    "containerToolVersion":"podman version 3.4.4",
    "containerImage":"c253ca50f50b380da0e23b168349271976d57e4e"
}

container 选项也可以与 dry-run 结合使用,在这种情况下,native-image 既不创建可执行文件也不创建容器镜像。它甚至不检查所选的容器工具是否可用。在这种情况下,container.json 会被省略,或者,如果您明确指定了容器工具,则只包含 containerTool 字段而没有任何附加信息。

容器化构建是粘性的,这意味着如果 Bundle 是使用 --bundle-create,container 创建的,则该 Bundle 被标记为容器构建。如果您现在使用此 Bundle 并带有 --bundle-apply,它会自动再次在容器中构建。但是,这不适用于执行 Bundle 应用程序,默认情况下,Bundled 应用程序仍将在容器外部执行。

上面 --bundle-create--bundle-apply 的选项帮助文本中显示了容器化构建的扩展命令行界面。

捕获环境变量 #

在添加 Bundle 支持之前,所有环境变量对于 native-image 构建器都是可见的。这种方法与 Bundle 不太兼容,并且对于没有 Bundle 的镜像构建也存在问题。考虑一个环境变量,其中包含来自构建机器的敏感信息。由于 Native Image 能够在构建时运行代码以创建在运行时可用的数据,因此很容易构建一个意外泄露此类变量内容的镜像。

现在,将环境变量传递给 native-image 需要明确的参数。

假设用户希望在 native-image 工具被调用的环境中,在设置为构建时初始化的类初始化器中使用环境变量(例如,KEY_STORAGE_PATH)。为了允许在类初始化器中访问该变量(使用 java.lang.System.getenv),请将选项 -EKEY_STORAGE_PATH 传递给构建器。

要使环境变量在构建时可访问,请使用

-E<env-var-key>[=<env-var-value>]
                      allow native-image to access the given environment variable during
                      image build. If the optional <env-var-value> is not given, the value
                      of the environment variable will be taken from the environment
                      native-image was invoked from.

使用 -E 与 Bundle 配合时按预期工作。任何通过 -E 指定的环境变量都将被捕获在 Bundle 中。对于未提供可选 <env-var-value> 的变量,Bundle 将捕获该变量在创建 Bundle 时所拥有的值。选择前缀 -E 是为了使该选项看起来类似于相关的 -D<java-system-property-key>=<java-system-property-value> 选项(该选项使 Java 系统属性在构建时可用)。

结合使用 --bundle-create 和 --bundle-apply #

正如在使用 Bundle 构建中已经提到的,可以基于现有 Bundle 创建一个新 Bundle。--bundle-apply 帮助消息有一个简单的示例。如果使用现有 Bundle 创建一个新的 Bundle,该 Bundle 构建原始应用程序的 PGO 优化版本,则会出现一个更有趣的示例。

假设您已经将应用程序构建成一个名为 application.nib 的 Bundle。要生成该 Bundle 的 PGO 优化变体,首先构建一个在运行时生成 PGO 性能分析信息的原生可执行文件变体(您稍后会用到它)

native-image --bundle-apply=application.nib --pgo-instrument

现在运行生成的可执行文件,以便收集性能分析信息

./target/application

完成后,停止应用程序。

查看当前工作目录,您可以找到一个新文件 default.iprof。它包含由于您运行了使用 --pgo-instrument 构建的可执行文件而创建的性能分析信息。现在您可以从现有 Bundle 创建一个新的优化 Bundle

native-image --bundle-apply=application.nib --bundle-create=application-pgo-optimized.nib,dry-run --pgo

现在来看看 application-pgo-optimized.nibapplication.nib 有何不同

$ ls -lh *.nib
-rw-r--r-- 1 testuser testuser  20M Mar 28 11:12 application.nib
-rw-r--r-- 1 testuser testuser  23M Mar 28 15:02 application-pgo-optimized.nib

新 Bundle 应该比原始 Bundle 大。原因,正如可以猜到的,是现在 Bundle 包含 default.iprof 文件。使用工具比较目录,您可以详细检查差异。

正如您所看到的,application-pgo-optimized.nibinput/auxiliary 目录中包含 default.iprof,并且在其他文件中也有更改。META-INF/nibundle.propertiesinput/stage/path_substitutions.jsoninput/stage/path_canonicalizations.json 的内容将在稍后解释。现在,查看 build.json 中的差异

@@ -4,5 +4,6 @@
   "--no-fallback",
   "-H:Name=application",
   "-H:Class=example.com.Application",
-  "--no-fallback"
+  "--no-fallback",
+  "--pgo"

正如预期的那样,新 Bundle 包含了您传递给 native-image 以构建优化 Bundle 的 --pgo 选项。从这个新 Bundle 构建原生可执行文件将开箱即用地生成一个 PGO 优化过的可执行文件(在构建输出中查看 PGO: on

native-image --bundle-apply=application-pgo-optimized.nib

执行 Bundled 应用程序 #

正如Bundle 文件格式中稍后描述的,Bundle 文件是一个 JAR 文件,其中包含用于启动 Bundled 应用程序的启动器。这意味着您可以将 Native Image Bundle 与任何 JDK 一起使用,并使用 <jdk>/bin/java -jar [bundle-file.nib] 将其作为 JAR 文件执行。启动器使用存储在 run.json 中的命令行参数,并将 input/classes/cp/input/classes/p/ 中的所有 JAR 文件和目录分别添加到类路径和模块路径。

启动器还带有一个独立的命令行界面,在其帮助文本中描述

This native image bundle can be used to launch the bundled application.

Usage: java -jar bundle-file [options] [bundle-application-options]

where options include:

    --with-native-image-agent[,update-bundle[=<new-bundle-name>]]
                runs the application with a native-image-agent attached
                'update-bundle' adds the agents output to the bundle-files class path.
                '=<new-bundle-name>' creates a new bundle with the agent output instead.
                Note 'update-bundle' requires native-image to be installed

    --container[=<container-tool>][,dockerfile=<Dockerfile>]
                sets up a container image for execution and executes the bundled application
                from inside that container. Requires podman or rootless docker to be installed.
                If available, 'podman' is preferred and rootless 'docker' is the fallback. Specifying
                one or the other as '=<container-tool>' forces the use of a specific tool.
                'dockerfile=<Dockerfile>': Use a user provided 'Dockerfile' instead of the Dockerfile
                bundled with the application

    --verbose   enable verbose output
    --help      print this help message

使用 --with-native-image-agent 参数运行 Bundled 应用程序需要 native-image-agent 库可用。native-image-agent 的输出写入到 _.output/launcher/META-INF/native-image/-agent_。如果原生镜像代理输出应该通过 `,update-bundle` 插入到 Bundle 中,那么启动器也需要 `native-image`。`update-bundle` 选项在执行附加了 `native-image-agent` 的 Bundled 应用程序后,会执行命令 `native-image --bundle-apply=.nib --bundle-create=.nib -cp.output/launcher`。

container 选项实现了与容器化镜像构建类似的行为。但是,唯一的例外是,在这种情况下,应用程序在容器内执行而不是 native-image。每个 Bundle 都包含一个 Dockerfile,用于在容器中执行 Bundled 应用程序。然而,可以通过在 --container 参数中添加 ,dockerfile=<path-to-dockerfile> 来覆盖此 Dockerfile。

Bundle 启动器只处理它知道的选项,所有其他参数都传递给 Bundled 应用程序。如果 Bundle 启动器解析到 -- 而没有指定选项,则启动器将停止解析参数。所有剩余的参数也将传递给 Bundled 应用程序。

Bundle 文件格式 #

Bundle 文件是一个具有良好定义内部布局的 JAR 文件。在 Bundle 内部可以找到以下内部结构

[bundle-file.nib]
├── META-INF
│   ├── MANIFEST.MF
│   └── nibundle.properties <- Contains build bundle version info:
│                              * Bundle format version (BundleFileVersion{Major,Minor})
│                              * Platform and architecture the bundle was created on 
│                              * GraalVM / Native-image version used for bundle creation
├── com.oracle.svm.driver.launcher <- launcher for executing the bundled application
├── input <- All information required to rebuild the image
│   ├── auxiliary <- Contains auxiliary files passed to native-image via arguments
│   │                (for example, external `config-*.json` files or PGO `*.iprof`-files)
│   ├── classes   <- Contains all class-path and module-path entries passed to the builder
│   │   ├── cp
│   │   └── p
│   └── stage
│       ├── build.json          <- Full native-image command line (minus --bundle options)
│       ├── container.json            <- Containerization tool, tool version and container
│       │                                image name (not available information is omitted)
│       ├── Dockerfile                 <- Dockerfile used for building the container image
│       ├── environment.json              <- Environment variables used in the image build
│       ├── path_canonicalizations.json  <- Record of path-canonicalizations that happened
│       │                                       during bundle creation for the input files
│       ├── path_substitutions.json          <- Record of path-substitutions that happened
│       │                                       during bundle creation for the input files                                        
│       └── run.json            <- Full command line for executing the bundled application
│                                                        (minus class path and module path)
└── output
    ├── default
    │   ├── myimage         <- Created image and other output created by the image builder 
    │   ├── myimage.debug
    |   └── sources
    └── other      <- Other output created by the builder (not relative to image location)

META-INF #

Bundle 文件本身的布局是版本化的。META-INF/nibundle.properties 中有两个属性声明了给定 Bundle 文件所基于的布局版本。Bundle 当前使用以下布局版本

BundleFileVersionMajor=0
BundleFileVersionMinor=9

未来的 GraalVM 版本可能会更改或扩展 Bundle 文件的内部结构。版本控制使我们能够在考虑向后兼容性的前提下发展 Bundle 格式。

输入数据 #

此目录包含传递给 native-image 构建器的所有输入数据。文件 input/stage/build.json 包含在创建 Bundle 时传递给 native-image 的原始命令行。

在 Bundle 构建中重新应用没有意义的参数已被过滤掉。这些参数包括

  • --bundle-{create,apply}
  • --verbose
  • --dry-run

与构建相关的环境变量状态捕获在 input/stage/environment.json 中。对于在创建 Bundle 时看到的每个 -E 参数,其键值对的快照都记录在文件中。其余文件 path_canonicalizations.jsonpath_substitutions.json 包含 native-image 工具根据原始命令行参数指定的输入文件路径执行的文件路径转换记录。

输出数据 #

如果原生可执行文件作为构建 Bundle 的一部分被构建(例如,没有使用 dry-run 选项),那么您在 Bundle 中也会有一个 output 目录。它包含随构建一起生成的任何其他文件以及所构建的可执行文件。大多数输出文件位于 output/default 目录中(可执行文件、其调试信息和调试源)。如果可执行文件不是在 Bundle 模式下构建的,则会写入任意绝对路径的构建器输出文件可以在 output/other 中找到。

联系我们