TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

JavaFX のアプリを exe 化するときにハマったこと

業務で Windows で動作するアプリケーションを作ることになり、様々な検討の結果 JavaFX を利用することになりました。 Java のアプリケーションとしては実行可能 Jar が真っ先に思い浮かびますが、想定している利用者がエンジニアではない人ということもあり、exe ファイルとして作ることにしました。 「JavaFX exe 化」などで調べると先人の知恵がいくつも出てくるのですが、自分の環境関係でハマったポイントがいくつかあったので、そのあたりを中心に書こうと思います。

JavaFX とは?

JavaFX is an open source, next generation client application platform for desktop, mobile and embedded systems built on Java. It is a collaborative effort by many individuals and companies with the goal of producing a modern, efficient, and fully featured toolkit for developing rich client applications.


筆者訳)

JavaFX は、Java 上に構築されたデスクトップ、モバイル、および組み込みシステム用のオープンソースの次世代クライアント アプリケーション プラットフォームです。 リッチなクライアント・アプリケーションを開発するための、モダンで効率的、かつ完全な機能を備えたツールキットを生み出すことを目標に、多くの個人や企業が共同で取り組んでいます。

簡単に言うと JavaFX は GUI を簡単に作ることができるライブラリ群です。 ボタンなどの配置や CSS の適用などが柔軟に行えるほか、Webアプリケーションではなくデスクトップアプリケーションとして作成できることから今回採用しました。

そもそも Java の exe ファイルを動かすには何が必要か?

JavaFX は JRE に加え、 JavaFX 用のモジュールも導入する必要があります。 ただ、すべての利用者に環境を用意してもらうのは難しいので、いくつかのサイトで紹介されていた「JavaFX 用の専用 JRE を作って同梱する」という方法を使い、「インストールが不要なアプリ」を目指しました。

環境

  • Debian 12
  • OpenJDK 21.0.3
  • OpenJFX 21.0.3

※ 執筆時点では Java の最新は22ですが、LTSである 21.0.3 を利用しています。

ファイル(今回の話に関連したファイルを抜粋)

.
|-- app
|   |-- src
|   |   `-- main
|   |       |-- java
|   |       |   `-- com
|   |       |       `-- example
|   |       |           `-- demo
|   |       |               |-- App.java
|   |       |               |-- PrimaryController.java
|   |       |               `-- SecondaryController.java
|   |       `-- resources
|   |           `-- com
|   |               `-- example
|   |                   `-- demo
|   |                       |-- primary.fxml
|   |                       `-- secondary.fxml
|   `-- build
|       `-- libs
|           `-- app-1.0-SNAPSHOT.jar
`-- settings.gradle.kts

まずは専用JREを作る

まずは同梱用の専用JREを作ります。 そのままJava用、JavaFX用のJREを同梱しても使えることは使えるのですが、それだと容量がかなり大きくなってしまうため、必要なものだけが入った小さなものを作ります。

こちらは jdepsjlink コマンドを使えばできるとのことです。

  • jdeps
    • Java クラスの依存関係を調べるためのコマンド
  • jlink
    • 一連のモジュールとその依存性を作成し、カスタム・ランタイム・イメージに最適化するコマンド

ハマりポイントその1 JavaFX の依存関係がうまく取れない

jdeps コマンドを使って依存関係を調べよう、と思ったときに最初のハマりポイントが出てきました。

jdeps -s app/build/libs/app-1.0-SNAPSHOT.jar

#実行結果
   java.base
   not.found

Java 関係は出てきましたが、JavaFX 関係が表示されていません。 サマリ表示をする-sオプションを外して調べてみます。

jdeps app/build/libs/app-1.0-SNAPSHOT.jar
app-1.0-SNAPSHOT.jar -> java.base
app-1.0-SNAPSHOT.jar -> not found
   com.example.demo                                   -> java.io                                            java.base
   com.example.demo                                   -> java.lang                                          java.base
   com.example.demo                                   -> java.lang.invoke                                   java.base
   com.example.demo                                   -> java.net                                           java.base
   com.example.demo                                   -> javafx.application                                 not found
   com.example.demo                                   -> javafx.fxml                                        not found
   com.example.demo                                   -> javafx.scene                                       not found
   com.example.demo                                   -> javafx.stage                                       not found

見事に JavaFX 関係が not found になっています。 この原因ですが、ずばり「JavaFXのモジュールのパスをコマンド実行時に渡す必要があった」ということでした。 モジュールを指定しない jlink コマンドだと Java の標準ライブラリ由来のものは対応していますが、それ以外は対応していない、つまり not found となってしまいます。

モジュールの指定は公式情報にも書かれているので、基本的なことができていませんでした。

ということで、JavaFXのmodule(--module-path "/usr/local/javafx-sdk-21/lib")を追加して実行します。

jdeps --module-path "/usr/local/javafx-sdk-21/lib" app/build/libs/app-1.0-SNAPSHOT.jar
app-1.0-SNAPSHOT.jar -> java.base
app-1.0-SNAPSHOT.jar -> javafx.fxml
app-1.0-SNAPSHOT.jar -> javafx.graphics
   com.example.demo                                   -> java.io                                            java.base
   com.example.demo                                   -> java.lang                                          java.base
   com.example.demo                                   -> java.lang.invoke                                   java.base
   com.example.demo                                   -> java.net                                           java.base
   com.example.demo                                   -> javafx.application                                 javafx.graphics
   com.example.demo                                   -> javafx.fxml                                        javafx.fxml
   com.example.demo                                   -> javafx.scene                                       javafx.graphics
   com.example.demo                                   -> javafx.stage                                       javafx.graphics

今度はうまくできました。

さて、ここまで出来れば専用 JRE を作ることができます。 先ほど jdeps コマンドで取得した依存関係を使い、jlink コマンドで以下のようなオプションを付けて実行します。 なお、jdeps ではモジュールを追加しても出てこないのですが、JavaFXを動かすためには javafx.controls も追加する必要があります。

# jlink実行
jlink --module-path "/usr/local/jdk-21/jmods:/usr/local/javafx-jmods-21.0.3" \
  --add-modules java.base,javafx.controls,javafx.graphics,javafx.fxml \
  --output ./custom/jre-min

これで専用 JRE が /custom/jre-min に作られたので、次に進みます。 (しかし、実はこの時点で問題が出ていたというのは後から気づくことになります)

(寄り道)jpackage を使う

実は jlink でわざわざ JRE を個別で作らなくても、 jpackage コマンドを使えば JRE 込みの実行ファイルを作ることができます。 インターネットで「JavaFX exe 化」と検索して出てくるページの多くがこちらを使っていたため、私も最初はこれを検討していました。 しかし、 jpackage コマンド使用時、生成されるファイル形式を選択する -t(--type)オプションは、コマンドを実行する環境に依存する、という制約がありました。 そのため、私の環境(= Linux 環境)だと、app-image rpm deb しか選べず、 exe ファイルを選択することができませんでした。

Windows 環境を用意してそこでビルドできればいいか、という考えも頭をよぎりましたが、Linux の Docker コンテナで動かすことを考えると、Linux 環境で完結させる方が後々やりやすいと思い、jpackage を使わない方法での解決を目指し、今回の方法をとることになりました。

補足
  • Mac で開発している場合も同様に exe ファイルは作れません
  • 特に制約がなければ、使いたい環境の OS を用意してそこで開発&ビルドをするのが一番楽だと思います

Linux 環境で exe ファイルを作りたい!

上記のような考えもあり、jpackage に頼らない exe ファイル作成方法を調べていると以下のようなものがありました。

  • exewrap
  • launch4j

今回は、このプロジェクトのビルドに gradle を利用していたため、親和性のありそうな launch4j を利用することにしました。

launch4j を build.gradle に組み込む

gradle でビルドをしている場合はとてもシンプルで、 build.gradle に書き込むだけで使うことができます。 (参考:gradle launch4j

# build.gradle.kts
# pluginsに追加
# kotlinのbuild.gradleだと、2系と3系で書き方が異なるので適宜変更してください ※以下では3系を利用

id("edu.sc.seis.launch4j") version "3.0.5"

# launch4j用の設定
# オプションの値は環境によって変わります
tasks.withType<edu.sc.seis.launch4j.tasks.DefaultLaunch4jTask> {
    outfile.set("HelloWorld.exe")
    mainClassName.set("com.example.demo.App")
    bundledJrePath.set("./custom/jre-min")
    requires64Bit.set(true)
}

ここまでできれば ./gradlew createExe で exe ファイルを作るだけです。 コマンドを実行すると、 build ディレクトリ以下の launch4j ディレクトリに以下のようなファイルが作られました。

lib/
HelloWorld.exe

Linux 上では exe ファイルの検証ができないので、Windows 環境に持ってきて動かしてみます。

ハマりポイントその2 Runtime Environment

さて、上記の手順で作った exe ファイルを Windows 環境に持ってきて実行したところ、上記のようなエラーが出てきました。 専用 JRE は作っていたはずなのに、と思いましたがそれはそうです。「作っただけ」だったのです。 bundledJrePath のオプションは「exe ファイル実行時に参照する JRE のパス」で、 作った JRE は別途追加してあげる必要がありました。 そのため、 jlink で作った JRE を含め、以下のような形で設置する必要があります。

lib/
custom/
  jre-min/
HelloWorld.exe

※customの階層はbuild.gradle側で設定した値に合わせているだけなので、状況に応じて変更可能です。

ハマりポイントその3 jmods の違い

さて、これで大丈夫だろう、ということで JRE を含めて Windows 環境に持ってきて実行するとまたしてもこの通知がでました。

JRE は追加したのになぜ、といろいろ調べているうちに出てきた結論が「専用 JRE を作るときの jmods がLinux用だった」というものです。

改めて jlink を実行したときのコマンドを載せます。

jlink --module-path "/usr/local/jdk-21/jmods:/usr/local/javafx-jmods-21.0.3" \
  --add-modules java.base,javafx.controls,javafx.graphics,javafx.fxml \
  --output ./custom/jre-min

今回引っ掛かったのはこの jmods の部分でした。

"/usr/local/jdk-21/jmods:/usr/local/javafx-jmods-21.0.3"

これが Linux 用の jmods のため、Windows に持ってきても動かなかったというわけです。 ということで Windows 用の jmods をダウンロードして使います。

# Windows用のディレクトリを作る
mkdir /usr/local/windows
cd /usr/local/windows

# Windows用のJDKをダウンロードし、展開してjmodsフォルダだけを利用する
wget https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.zip
unzip jdk-21_windows-x64_bin.zip

/usr/local/windows/jdk-21.0.3/jmods


# JavaFXは公式からダウンロード
wget https://download2.gluonhq.com/openjfx/21.0.3/openjfx-21.0.3_windows-x64_bin-jmods.zip
unzip openjfx-21.0.3_windows-x64_bin-jmods.zip

/usr/local/windows/javafx-jmods-21.0.3

これで jlink を使って再度専用 JRE を作成します。

jlink --module-path "/usr/local/windows/jdk-21.0.3/jmods:/usr/local/windows/javafx-jmods-21.0.3" \
  --add-modules java.base,javafx.controls,javafx.graphics,javafx.fxml \
  --output ./custom/jre-min

他はそのままで、この専用 JRE を Windows 環境に持っていくと無事アプリが起動しました。

今回の流れをまとめると以下になります。

1. jdepsで必要な依存関係を調べる
jdeps --module-path "/usr/local/javafx-sdk-21/lib" app/build/libs/app-1.0-SNAPSHOT.jar
2. jlinkで専用JREを作る(jmodsはWindows用のものを利用。javafx.controlsも忘れずに)
jlink --module-path "/usr/local/windows/jdk-21.0.3/jmods:/usr/local/windows/javafx-jmods-21.0.3" \
  --add-modules java.base,javafx.controls,javafx.graphics,javafx.fxml \
  --output ./custom/jre-min
3. build.gradle.ktsに以下を記載し、./gradlew createExe でexeファイルを作る
id("edu.sc.seis.launch4j") version "3.0.5"

tasks.withType<edu.sc.seis.launch4j.tasks.DefaultLaunch4jTask> {
    outfile.set("HelloWorld.exe")
    mainClassName.set("com.example.demo.App")
    bundledJrePath.set("./custom/jre-min")
    requires64Bit.set(true)
}
4. 専用JREを bundledJrePath に記載した場所に置く
5. Windows側に持ってきてexeファイルを実行する

まとめ

今回は JavaFX のアプリで exe ファイルを作るときにハマったポイントを書いてみました。 いろいろやりましたが、Windows 環境で jpackage コマンドを使えるならそれが一番楽だと思います。 ただ、どうしても Linux 環境で exe ファイルを作りたい、という要件があり、同じようなことでハマっている人がいれば参考にしていただけると幸いです。

参考にしたサイト

中西 崇彰(ナカニシ タカアキ)
もう若手とは言えないエンジニア
最近は運動不足を感じて徒歩1分ぐらいの24hジムを契約するか悩み中


シナジーマーケティング株式会社では一緒に働く仲間を募集しています。