JavaFX の Uber Jar から GraalVM ネイティブイメージを生成(Windows platform)


はじめに

Windows 環境で、JavaFx アプリの Uber Jar から、GraalVM Native Image によりネイティブイメージを生成する手順です。

GraalVM Native Image で JavaFx アプリを動かすのは、まだまだ途上という感じではあります。 特に macOS では、JDK-20 版ではネイティブイメージ生成時にエラーとなったり、JDK-17 版でもネイティブイメージは生成できるものの、別途 AppDelegate.m, launcher.c などを準備しないとウインドウが表示されないなどあるため、GluonFX plugin などを使うことも検討してください(GluonFX plugin for Maven)(Gluon Substrate)(GluonFX plugin for Gradle)。 特にシングルバイナリへのこだわりがなければ、jlink で配布パッケージを作成したほうが、トラブルもなく素直でおすすめです。

ここではプラグインを使わず、Windows 向けネイティブイメージ生成を説明します。


サンプル JavaFx アプリケーションの準備

プロジェクトの作成

> mkdir graalvmfx
> cd graalvmfx
> gradle init --type java-application --dsl kotlin --test-framework junit-jupiter --project-name graalvmfx --package graalvmfx

app/build.gradle.kts では、org.openjfx.javafxplugin プラグインにて JavaFx を構成します。

plugins {
    application
    id("org.openjfx.javafxplugin") version "0.0.14"
}

repositories {
    mavenCentral()
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(20))
    }
}

application {
    mainClass.set("graalvmfx.Launcher")
}

javafx {
    version = "20.0.2"
    modules("javafx.controls")
}

tasks.jar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from(
        configurations.runtimeClasspath.get()
            .map { if (it.isDirectory) it else zipTree(it) }
    ) {
        exclude("module-info.class")
    }
    manifest {
        attributes("Main-Class" to "graalvmfx.Launcher")
    }
}

Jar タスクで、JavaFX ランタイムを含めた Uber Jar を作成します。 ここではモジュール非対応にするので、 module-info.class を除外しています。


Application は簡単に、以下のような例とします。

package graalvmfx;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage stage) {
        Label label = new Label("Hello, JavaFX");
        Scene scene = new Scene(new StackPane(label), 320, 240);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

Uber Jar とした場合、Application を継承したクラスから直接JavaFXアプリを起動できないので、以下のようにランチャーを作成しておきます。

package graalvmfx;

public class Launcher {
    public static void main(String[] args) {
        App.main(args);
    }
}

これで以下のように Uber Jar として実行できます。

> ./gradlew build
> java -jar app/build/libs/app.jar

JavaFX 16 以降では、JavaFX はクラスパスからのロードをサポートしていないため、警告が出力されますが、ここでは気にしません。


Visual Studio Build Tools の準備

ネイティブイメージ生成には、Visual Studio Build Tools が必要となるため、インストールしておきます。

> winget add Microsoft.VisualStudio.2022.BuildTools

Visual Studio Build Tools 単品ではなく、 Visual Studio コミュニティを入れてしまっても良いです。

> winget add Microsoft.VisualStudio.2022.Community

インストーラでは「C++によるデスクトップ開発」を選択します。


GraalVM の準備

GraalVM を準備します。 ここでは、使い捨てのディレクトリにダウンロードします。

> curl -o graalvm-jdk-20_windows-x64_bin.zip https://download.oracle.com/graalvm/20/latest/graalvm-jdk-20_windows-x64_bin.zip
> Expand-Archive -Path graalvm-jdk-20_windows-x64_bin.zip -DestinationPath . -Force

今回は、ディレクトリ graalvm-jdk-20.0.2+9.1 として解凍されました。


ネイティブイメージの作成(フォールバックイメージ)

ネイティブイメージ作成には、Visual Studio の SDK のパスが通ったプロンプトから操作します。 Visual Studio インストールで作成されるスタートメニューから「x64 Native Tools Command Prompt for VS 2022」を選択してプロンプトを起動します。

> "graalvm-jdk-20.0.2+9.1\bin\native-image.cmd" -jar app/build/libs/app.jar

実行すれば、以下のようなログと共に app.exe が生成されます。

========================================================================================================================
GraalVM Native Image: Generating 'app' (executable)...
========================================================================================================================
For detailed information and explanations on the build output, visit:
https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildOutput.md
------------------------------------------------------------------------------------------------------------------------[1/8] Initializing...                                                                                    (9.7s @ 0.15GB) Java version: 20.0.2+9, vendor version: Oracle GraalVM 20.0.2+9.1
 Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: ML-inferred
 C compiler: cl.exe (microsoft, x64, 19.37.32822)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
[2/8] Performing analysis...  [*****]                                                                   (18.6s @ 0.55GB)   3,605 (71.77%) of  5,023 types reachable
   4,381 (49.77%) of  8,802 fields reachable
  18,232 (46.59%) of 39,137 methods reachable
   1,110 types,   106 fields, and   604 methods registered for reflection
      61 types,    51 fields, and    52 methods registered for JNI access
       1 native library: version
[3/8] Building universe...                                                                               (2.5s @ 0.47GB)
Warning: Reflection method java.lang.Class.getConstructor invoked at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$6(LauncherImpl.java:726)
Warning: Reflection method java.lang.Class.getConstructor invoked at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$8(LauncherImpl.java:794)
Warning: Reflection method java.lang.Class.getDeclaredConstructor invoked at com.sun.javafx.tk.Toolkit.getToolkit(Toolkit.java:262)
Warning: Aborting stand-alone image build due to reflection use without configuration.
Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
------------------------------------------------------------------------------------------------------------------------                        2.6s (8.2% of total time) in 84 GCs | Peak RSS: 1.19GB | CPU load: 4.49
========================================================================================================================
Finished generating 'app' in 31.0s.
Generating fallback image...
Warning: Image 'app' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).

ただし、生成された実行ファイルは、Generating fallback image... とあるようにフォールバックイメージです。 リフレクションを使用しているため、Java の存在する環境でしか動かないフォールバックイメージが生成されています。


ネイティブイメージ作成では、Javaコードを事前に静的解決してコンパイルするため、リフレクションにより動的にコードが切り替わるケースでは、それが実際にどのように使われるかを静的解決できるよう設定ファイルを準備する必要があります。

これは手動で作成することもできますが、トレース・エージェントと共にコードを実行することで、自動的にリフレクション構成を生成生成することができます。

リフレクション構成を生成は、META-INF/native-image に配備することでネイティブイメージ作成時に自動で読み取られます(コマンドラインオプションで渡すこともできます)。


リフレクション構成ファイルとネイティブイメージの生成

リフレクション構成ファイルの格納ディレクトリを作成します。

> mkdir -p app\src\main\resources\META-INF\native-image

エージェントを指定してアプリケーションを実行します。

> "graalvm-jdk-20.0.2+9.1\bin\java" -agentlib:native-image-agent=config-output-dir=app\src\main\resources\META-INF\native-image -jar app/build/libs/app.jar

以下のようなファイルが生成されます。

> tree /f app\src\main\resources\META-INF\native-image

└─native-image
    │  jni-config.json
    │  predefined-classes-config.json
    │  proxy-config.json
    │  reflect-config.json
    │  resource-config.json
    │  serialization-config.json
    │
    └─agent-extracted-predefined-classes

reflect-config.json は以下のような構成になっています(詳細はReachability Metadataを参照してください)。

[
{
  "name":"com.sun.glass.ui.Screen"
},
{
  "name":"com.sun.glass.ui.win.WinGestureSupport"
},
{
  "name":"com.sun.glass.ui.win.WinPlatformFactory",
  "methods":[{"name":"<init>","parameterTypes":[] }]
},
...

Uber Jar を生成し、ネイティブイメージを再度生成します(--no-fallback オプションでフォールバックイメージの生成を抑制することもできます)。

> gradlew build
> "graalvm-jdk-20.0.2+9.1\bin\native-image.cmd" -jar app/build/libs/app.jar

以下のようにフォールバックされることなくネイティブイメージが生成できます。

========================================================================================================================
GraalVM Native Image: Generating 'app' (executable)...
========================================================================================================================
For detailed information and explanations on the build output, visit:
https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildOutput.md
------------------------------------------------------------------------------------------------------------------------
[1/8] Initializing...                                                                                    (8.4s @ 0.15GB)
 Java version: 20.0.2+9, vendor version: Oracle GraalVM 20.0.2+9.1
 Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: ML-inferred
 C compiler: cl.exe (microsoft, x64, 19.37.32822)
 Garbage collector: Serial GC (max heap size: 80% of RAM)
[2/8] Performing analysis...  [*****]                                                                   (51.1s @ 1.43GB)
   7,304 (83.60%) of  8,737 types reachable
  13,648 (64.62%) of 21,121 fields reachable
  40,345 (57.88%) of 69,705 methods reachable
   2,164 types,   122 fields, and   959 methods registered for reflection
      90 types,    76 fields, and   108 methods registered for JNI access
       3 native libraries: crypt32, ncrypt, version
[3/8] Building universe...                                                                               (4.3s @ 1.12GB)
[4/8] Parsing methods...      [****]                                                                    (13.3s @ 1.21GB)
[5/8] Inlining methods...     [***]                                                                      (1.5s @ 1.49GB)
[6/8] Compiling methods...    [*********]                                                               (88.1s @ 1.14GB)
[7/8] Layouting methods...    [**]                                                                       (4.9s @ 1.80GB)
[8/8] Creating image...       [**]                                                                       (4.2s @ 0.85GB)
  22.30MB (54.61%) for code area:    21,140 compilation units
  18.17MB (44.49%) for image heap:  216,555 objects and 68 resources
 374.80kB ( 0.90%) for other data
  40.83MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 origins of code area:                                Top 10 object types in image heap:
   8.40MB java.base                                            4.37MB byte[] for code metadata
   6.48MB app.jar                                              3.59MB byte[] for embedded resources
   3.52MB java.xml                                             1.72MB byte[] for java.lang.String
   2.83MB svm.jar (Native Image)                               1.60MB java.lang.String
 274.70kB com.oracle.svm.svm_enterprise                        1.53MB byte[] for general heap data
 210.09kB jdk.jfr                                              1.25MB java.lang.Class
 173.26kB java.logging                                       394.24kB byte[] for reflection metadata
  61.47kB jdk.proxy1                                         342.38kB com.oracle.svm.core.hub.DynamicHubCompanion
  57.41kB jdk.crypto.mscapi                                  269.75kB java.util.HashMap$Node
  35.18kB org.graalvm.sdk                                    257.00kB int[][]
 140.94kB for 8 more packages                                  2.24MB for 1622 more object types
------------------------------------------------------------------------------------------------------------------------
Recommendations:
 PGO:  Use Profile-Guided Optimizations ('--pgo') for improved throughput.
 HEAP: Set max heap for improved and more predictable memory usage.
 CPU:  Enable more CPU features with '-march=native' for improved performance.
 QBM:  Use the quick build mode ('-Ob') to speed up builds during development.
 BRPT: Try out the new build reports with '-H:+BuildReport'.
------------------------------------------------------------------------------------------------------------------------
                       10.6s (6.0% of total time) in 202 GCs | Peak RSS: 3.18GB | CPU load: 5.91
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 C:\Users\...\graalvmfx\app.exe (executable)
========================================================================================================================
Finished generating 'app' in 2m 57s.

app.exe が生成され、ダブルクリックで起動します。

しかし、同時にコマンドプロンプトも起動してしまいます。


ネイティブイメージ実行時のコマンドプロンプトを抑制する

生成されたネイティブイメージを実行すると、アプリケーションは起動しますが、コマンドプロンプトが同時に立ち上がります。

これを抑制するには、Visual Studio Build Tools の editbin で実行ファイルを編集します。

> editbin /SUBSYSTEM:WINDOWS app.exe

これで、ネイティブイメージの作成は完了です。