原生映像檔綁定

原生映像檔提供一項功能,讓使用者可以從一個獨立的綁定組建原生可執行檔。與常規的 native-image 組建相比,此操作模式僅採用單個 *.nib 檔案作為輸入。該檔案包含組建原生可執行檔 (或原生共用程式庫) 所需的一切內容。當包含許多輸入檔案 (JAR 檔案、組態檔案、自動產生的檔案、下載的檔案) 的大型應用程式需要在稍後時間點重新組建,而無需擔心是否仍可存取所有檔案時,這會很有用。複雜的組建通常涉及下載許多無法保證稍後仍可存取的程式庫。使用原生映像檔綁定是將組建所需的所有輸入封裝到單個檔案中的安全解決方案。

注意:此功能為實驗性功能。

目錄 #

建立綁定 #

若要建立綁定,請傳遞 --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 建立綁定 #

假設 Java 應用程式是以 Maven 組建的,請在 用於原生映像檔組建組態的 Maven 外掛程式 中,傳遞 --bundle-create 作為組建引數。

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

然後,執行 Maven 套件命令

./mvnw -Pnative native:compile

注意:使用 Maven 為 Micronaut 專案建立原生可執行檔的命令是:./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 和綁定 application.nib。綁定檔案是在 target/ 目錄中建立。它應複製到某些安全位置,以便在稍後需要重新組建原生可執行檔時找到它。

使用 Gradle 建立綁定 #

假設 Java 應用程式是以 Gradle 組建的,請在 用於原生映像檔組建組態的 Gradle 外掛程式 中,傳遞 --bundle-create 作為組建引數。

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 和綁定 application.nib。綁定檔案是在 build/native/nativeCompile/ 目錄中建立。

綁定檔案和輸出目錄 #

顯然,綁定檔案可能很大,因為它包含所有輸入檔案以及可執行檔本身 (可執行檔已壓縮在綁定內)。將原生映像檔放在綁定內可讓您比較從綁定重建的原生可執行檔與原始的可執行檔。

綁定只是具有特定配置的 JAR 檔案。這在 下方 有詳細說明。若要查看綁定內的內容,請執行

jar tf application.nib

在綁定旁邊,您也可以找到輸出目錄:application.output。它包含原生可執行檔和所有其他在組建過程中建立的檔案。由於您沒有指定任何會產生額外輸出的選項 (例如,產生除錯資訊的 -g--diagnostics-mode),因此只能在那裡找到可執行檔。

結合 --bundle-create 與 dry-run #

--bundle-create 選項描述中所述,也可以讓 native-image 組建綁定,但實際上不建立映像檔。如果使用者想要將綁定移至功能更強大的機器並在那裡組建映像檔,這可能會很有用。將上述 Maven/Gradle 原生映像檔外掛程式組態中的 --bundle-create 引數修改為 <buildArg>--bundle-create,dry-run</buildArg>。然後,組建專案只需要幾秒鐘,而建立的綁定會小得多。例如,檢查 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-apply=application.nib,如下所示

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

執行此命令後,會使用已啟用的除錯資訊從綁定重建可執行檔。

--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 原生映像檔外掛程式組態中的 --bundle-create 引數修改為 <buildArg>--bundle-create,container</buildArg>。這仍然會建立與之前相同的綁定。不過,會組建容器映像檔,然後用於組建原生可執行檔。

如果容器映像檔是新建立的,您也可以看到容器工具的組建輸出。容器映像檔的名稱是所用 Dockerfile 的雜湊。如果容器映像檔已存在,您會在組建輸出中看到以下這行文字

Native Image Bundles: Reusing container image c253ca50f50b380da0e23b168349271976d57e4e.

若要在容器中組建,您必須在系統上提供 podmanrootless docker

容器中的組建目前僅支援 Linux。使用任何其他 OS 原生映像檔將不會建立和使用容器映像檔。

可用 <buildArg>--bundle-create,container=podman<buildArg><buildArg>--bundle-create,container=docker<buildArg> 指定用於執行映像檔組建的容器工具。如果未指定,native-image 會使用其中一個支援的工具。如果可用,則會優先使用 podman,而 rootless docker 則是備用方案。

也可用 --bundle-create,container,dockerfile=<path-to-dockerfile> 明確指定用於組建容器映像檔的 Dockerfile。如果未指定任何 Dockerfile,則會使用預設 Dockerfile,該 Dockerfile 是以來自 此處 的適用於 GraalVM 的 Oracle Linux 8 容器映像檔為基礎。最終用於組建容器映像檔的任何 Dockerfile 都會儲存在綁定中。即使您不使用 container 選項,native-image 也會建立 Dockerfile 並將其儲存在綁定中。

除了在主機系統上建立容器映像檔之外,在容器內組建不會建立任何額外的組建輸出。不過,建立的綁定包含一些額外的檔案

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

綁定包含用於組建容器映像檔的 Dockerfile,並將所用的容器工具、其版本和容器映像檔的名稱儲存在 container.json 中。例如

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

container 選項也可以與 dry-run 結合使用,在這種情況下,native-image 不會建立可執行檔或容器映像檔。它甚至不會檢查所選容器工具是否可用。在這種情況下,會省略 container.json,或者,如果您明確指定了容器工具,則只會包含 containerTool 欄位,而沒有任何額外資訊。

容器化組建是「固定的」,這表示如果使用 --bundle-create,container 建立綁定,則該綁定會標記為容器組建。如果您現在將 --bundle-apply 用於此綁定,則會在容器中自動再次組建。不過,這不適用於執行綁定的應用程式,綁定的應用程式預設仍然會在容器外執行。

容器化組建的擴充命令列介面顯示在上述 --bundle-create--bundle-apply 的選項說明文字中。

擷取環境變數 #

新增綁定支援之前,所有環境變數都對 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 指定的環境變數都會被擷取到套件中。對於沒有提供選填的 <env-var-value> 的變數,套件會擷取建立套件時該變數的值。選擇 -E 這個前綴是為了讓此選項看起來類似相關的 -D<java-system-property-key>=<java-system-property-value> 選項(這會讓 Java 系統屬性在建置時可用)。

組合 –bundle-create 和 –bundle-apply #

如同在使用套件建置中已提及,可以基於現有套件建立新的套件。--bundle-apply 的說明訊息中有一個簡單的範例。一個更有趣的範例是使用現有套件來建立新的套件,該套件會建置原始應用程式的 PGO 優化版本。

假設您已經將應用程式建置到名為 application.nib 的套件中。要產生該套件的 PGO 優化變體,首先建置一個原生可執行檔的變體,該變體會在執行時產生 PGO 分析資訊 (您稍後會用到它)

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

現在執行產生的可執行檔,以便收集分析資訊

./target/application

完成後,停止應用程式。

查看目前的工作目錄,您可以找到一個新檔案 default.iprof。它包含因為您從使用 --pgo-instrument 建置的可執行檔執行應用程式而建立的分析資訊。現在您可以從現有的套件建立新的優化套件

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

新的套件應該比原始套件大。原因正如可以猜到的,是因為現在套件包含 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"

如預期,新的套件包含您傳遞給 native-image 以建置優化套件的 --pgo 選項。從這個新套件建置原生可執行檔會直接產生 PGO 優化的可執行檔(請參閱建置輸出中的 PGO: on

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

執行套件化的應用程式 #

如稍後在套件檔案格式中所述,套件檔案是一個 JAR 檔案,其中包含用於啟動套件化應用程式的啟動器。這表示您可以使用任何 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 引數執行套件化的應用程式需要提供 native-image-agent 函式庫。native-image-agent 的輸出會寫入到 _.output/launcher/META-INF/native-image/-agent_。如果原生映像檔代理程式的輸出應該使用 `,update-bundle` 插入到套件中,則啟動器也需要 `native-image`。`update-bundle` 選項會執行命令 `native-image --bundle-apply=.nib --bundle-create=.nib -cp.output/launcher`,在附加 `native-image-agent` 的情況下執行套件化應用程式之後。

container 選項實現與容器化映像檔建置類似的行為。但是,唯一的例外是,在這種情況下,應用程式是在容器內部執行,而不是 native-image。每個套件都包含一個 Dockerfile,用於在容器中執行套件化的應用程式。但是,可以透過將 ,dockerfile=<path-to-dockerfile> 新增至 --container 引數來覆寫此 Dockerfile。

套件啟動器只會使用它知道的選項,所有其他引數都會傳遞給套件化的應用程式。如果套件啟動器解析到 -- 而沒有指定的選項,則啟動器會停止解析引數。所有剩餘的引數也會傳遞給套件化的應用程式。

套件檔案格式 #

套件檔案是一個 JAR 檔案,具有明確定義的內部佈局。在套件內部,您可以找到以下內部結構

[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 #

套件檔案本身的佈局是版本化的。在 META-INF/nibundle.properties 中有兩個屬性宣告給定套件檔案所基於的佈局版本。套件目前使用以下佈局版本

BundleFileVersionMajor=0
BundleFileVersionMinor=9

GraalVM 的未來版本可能會變更或擴充套件檔案的內部結構。版本控制使我們能夠在考慮向後相容性的情況下發展套件格式。

輸入資料 #

此目錄包含傳遞給 native-image 建置器的所有輸入資料。檔案 input/stage/build.json 保存了建立套件時傳遞給 native-image 的原始命令列。

在套件建置中重新套用沒有意義的參數已經被過濾掉。這些包括

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

與建置相關的環境變數的狀態會擷取到 input/stage/environment.json 中。對於建立套件時看到的每個 -E 引數,其鍵值對的快照會記錄在檔案中。剩餘的檔案 path_canonicalizations.jsonpath_substitutions.json 包含 native-image 工具根據原始命令列引數指定的輸入檔案路徑執行的檔案路徑轉換記錄。

輸出資料 #

如果原生可執行檔是作為建置套件的一部分而建置的(例如,未使用 dry-run 選項),則套件中也會有一個 output 目錄。它包含建置的可執行檔以及建置過程中產生的任何其他檔案。大多數輸出檔案都位於 output/default 目錄中(可執行檔、其偵錯資訊和偵錯來源)。如果可執行檔不是以套件模式建置,則本來會寫入任意絕對路徑的建置器輸出檔案可以在 output/other 中找到。

與我們聯繫