業務で 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を同梱しても使えることは使えるのですが、それだと容量がかなり大きくなってしまうため、必要なものだけが入った小さなものを作ります。
こちらは jdeps
と jlink
コマンドを使えばできるとのことです。
- 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ジムを契約するか悩み中