torutkのブログ

ソフトウェア・エンジニアのブログ

AnalogClockプログラムをJava SE 9のモジュール化

既存のJavaプログラムを、Java SE 9モジュール対応させてみることにします。
対象は、次のリポジトリにある、デスクトップにアナログ時計を表示するJavaFXプログラムです。
https://github.com/torutk/AnalogClockGadget

作業開始時点では、JDK 8でビルド・実行するプログラムとなっています。Java SE以外の依存ライブラリはありません。

ビルド環境

NetBeans IDE 8.2で作成していたプロジェクトを、Java SE 9でビルド・実行するためにNetBeans IDE dev 201709300002版で開いて修正していきます。NetBeans IDEJava SE 9対応版が本来9月にリリースされる予定でした。ですが、まだリリースされておらず、開発途中版のnightly buildを使用しています。

Javaプラットフォームの変更

元のリポジトリにあるビルド設定では、JDK 8 32bit版を使用しています。これを今回はJDK 9に変更します。しかし、JDK 9は現時点では64bit版しか存在しないので、プロジェクトの設定をJDK 9の64bit版に変更します。

  • [ファイル]メニュー > [プロジェクトプロパティ(AnalogClockSvg)] > [ライブラリ] > [Javaプラットフォーム]を、[JDK 9(デフォルト)]に設定

この変更だけでJDK 9でビルドおよび実行できるようになりました。

ソースコードの修正

module-info.javaを追加

[ファイル]メニュー > [新規ファイル] > [Java] > [Java Module Info]をクリックし、[場所]が[ソース・パッケージ]になっていることを確認して作成します。

module-info.javaが生成されると、すぐにJavaのソースファイルがエラーとなってしまいます。
これは、module-info.javaが生成されたことにより、モジュールの依存性を記述していないモジュールに含まれるクラスを使用している(importしている)箇所がエラーとなるからです。

そこで、module-info.javaに必要な記述を行います。module-info.javaを記述前に生成したjarファイルに対して、依存関係を表示させます。

$ jdeps -s dist/AnalogClockGadget.jar 
AnalogClockGadget.jar -> java.base
AnalogClockGadget.jar -> javafx.base
AnalogClockGadget.jar -> javafx.controls
AnalogClockGadget.jar -> javafx.fxml
AnalogClockGadget.jar -> javafx.graphics

このうち、java.baseは暗黙的に依存関係が定義されているので、それ以外を追加します。
ただし、最低限必要なのはjavafx.fxmlとjavafx.controlsの2つなのでこれを記述します。

module AnalogClockGadget {
    requires javafx.controls;
    requires javafx.fxml;
}

ビルド

NetBeansのプロジェクト設定を弄る

プロジェクトプロパティから、[ビルド] > [パッケージング]を見ると、

Create JLINK distributionというチェックボックスがあり、その下に、Create Launcher、Strip Debug Informationのチェックボックスがあります。

個々にチェックを付けてみましたが、ビルド結果生成されるものに変化がなく・・・

次は、プロジェクトプロパティから、[ビルド] > [デプロイメント]を見ると

[ネイティブ・パッケージングの有効化]にチェックが付いていないので、これにチェックを付けます。
プロジェクトを右クリックして、[パッケージとして] > [イメージ]を実行します。

すると、次のエラーが発生しました。

"モジュール: []をランタイム・イメージに追加しています。"
Exception: jdk.tools.jlink.plugin.PluginException: java.lang.module.FindException: Module not found

モジュール名が空のためエラーとなっているようですが、モジュール名をどこに記述するとよいかは探りきれませんでした。
NetBeans IDEのパッケージとしてビルドはいったん中止し、javapackagerコマンドを使う方法を進めます。

createmsi.batを修正

AnalogClockのJDK 8版では、バッチファイルからjavapackagerコマンドを実行してWindows用のネイティブインストーラー(MSI)を生成しています。そこで、このバッチファイルのjavapackager呼出し行を修正してモジュール対応にします。

修正にあたっては、AOEさんのブログ、JDK9でのjavapackagerについて - AOEの日記 を参照しながら、javapackagerの起動オプションを変更してみました。

javapackager -deploy -native msi ^
-v ^
-outdir dist -outfile AnalogClockGadget ^
-p dist ^
-m AnalogClockGadget/com.torutk.gadget.analogclock.AnalogClockApp ^
-name "AnalogClock" ^
-BappVersion=0.4.0 ^
-title "Analog Clock Gadet" ^
-vendor Takahashi ^
-description "Analog Clock on desktop"

すると、MSI形式インストーラーが生成されました。

JDK 8で生成したインストーラーファイル(MSI形式)のサイズは61MBでしたが、JDK 9で生成したインストーラーファイル(MSI形式)のサイズは31MBとほぼ半分となりました。

インストールディレクトリの変化

JDK 8では、appディレクトリの下に実行可能JARファイルがインストールされていました。
JDK 9からは、appディレクトリの下に実行可能JARファイルは存在せず、他にもありません。一方、runtime\libの下にmodulesという大きな(数十MB)ファイルが存在します。

実行

まだいくつか関門があります。

Failed to launch JVM

さて、MSI形式でインストールしたexeを実行してみたところ、「Failed to launch JVM」のエラーダイアログが表示されてしまいました。

module AnalogClockGadget {
    requires javafx.controls;
    requires javafx.fxml;
    exports com.torutk.gadget.analogclock; // 追加
}

ネイティブバンドル形式でexeから起動するときは、mainメソッドを持つクラスのパッケージをモジュール定義で公開(exports)しておくことが現時点では必要のようです。

時計の画面がでない

さて、最初の関門を通過したら、次はエラーダイアログも何もでず、起動してもプログラムがすぐに終了してしまいます。

空のウィンドウだけ表示するようにしたところ、こちらは画面が表示されました。
そこで、モジュール化したときにFXMLファイル、CSSファイル、バンドル・プロパティファイルを外部から読み込む際に何か起きていないかを追ってみました。

すると、FXMLファイルのロードで例外発生となっていました。
FXMLファイルの場所は、次のコードで取得しています。

root = FXMLLoader.load(getClass().getResource("AnalogClockView.fxml"), bundle);

getClass().getResource("AnalogClockView.fxml") の値を調べてみたところ、

実行可能JARファイルを実行した場合

jar:file:/D:/work/AnalogClockGadget/dist/AnalogClockGadget.jar!/com/torutk/gadget/analogclock/AnalogClcokView.fxml

モジュール化してインストールしたexeから実行した場合

jrt:/AnalogClockGadget/com/torutk/gadget/analogclock/AnalogClockView.fxml

となり、FXMLLoaderのエラーとなります。

となっていました。

今日は時間切れでここまで。

原因判明

原因は、getClass().getResource(...)の値ではなく、FXMLをロードした際にJavaFXライブラリからアプリケーションへリフレクションを行うことができなかったためでした。

FXMLはインジェクションの仕組みがあり、コントローラークラスのフィールドやメソッド(publicに限らず)にFXMLの要素を割り当てるためリフレクション(ディープリフレクション)を使います。

そのため、モジュール定義ファイル(module-info.java)には、javafx.fxmlモジュールからアプリケーションのモジュールにディープリフレクションをかけるための記述(opens)を足す必要があります。

module AnalogClockGadget {
    requires javafx.controls;
    requires javafx.fxml;
    exports com.torutk.gadget.analogclock;
    opens com.torutk.gadget.analogclock to javafx.fxml; // 追加
}

このopensがない場合、javaコマンドから実行してもエラーとなります。
ちゃんとエラーメッセージの中に原因と対処が含まれていました。

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private
javafx.scene.shape.SVGPath com.torutk.gadget.analogclock.AnalogClockViewController.hourHand
accessible: module AnalogClockGadget does not "opens com.torutk.gadget.analogclock" to module
javafx.fxml

長いですがopensがないときのエラーを以降に記述します。

> java -p dist -m AnalogClockGadget/com.torutk.gadget.analogclock.AnalogClockApp
Exception in Application start method
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
        at AnalogClockGadget/com.torutk.gadget.analogclock.AnalogClockApp.zoom(AnalogClockApp.java:88)
        at AnalogClockGadget/com.torutk.gadget.analogclock.AnalogClockApp.lambda$parseParameters$8(AnalogClockApp.java:105)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:189)
        at java.base/java.lang.Thread.run(Thread.java:844)
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
        at AnalogClockGadget/com.torutk.gadget.analogclock.AnalogClockApp.lambda$parseParameters$9(AnalogClockApp.java:106)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:189)
        at java.base/java.lang.Thread.run(Thread.java:844)
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
        at AnalogClockGadget/com.torutk.gadget.analogclock.AnalogClockApp.lambda$parseParameters$10(AnalogClockApp.java:107)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:189)
        at java.base/java.lang.Thread.run(Thread.java:844)
java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:473)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:372)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:945)
Caused by: java.lang.RuntimeException: Exception in Application start method
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:973)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:198)
        at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: javafx.fxml.LoadException:
file:///D:/work/AnalogClockGadget/dist/AnalogClockGadget.jar!/com/torutk/gadget/analogclock/AnalogClockView.fxml:12

        at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2625)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2603)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2466)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3253)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3210)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3179)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3152)
        at javafx.fxml/javafx.fxml.FXMLLoader.load(FXMLLoader.java:3144)
        at AnalogClockGadget/com.torutk.gadget.analogclock.AnalogClockApp.start(AnalogClockApp.java:41)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:919)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(PlatformImpl.java:449)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:189)
        ... 1 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private javafx.scene.shape.SVGPath com.torutk.gadget.analogclock.AnalogClockViewController.hourHand accessible: module AnalogClockGadget does not "opens com.torutk.gadget.analogclock" to module javafx.fxml
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
        at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
        at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:176)
        at java.base/java.lang.reflect.Field.setAccessible(Field.java:170)
        at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleFields(FXMLLoader.java:3495)
        at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.access$3900(FXMLLoader.java:3344)
        at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3460)
        at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3456)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleMembers(FXMLLoader.java:3455)
        at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.getControllerFields(FXMLLoader.java:3394)
        at javafx.fxml/javafx.fxml.FXMLLoader.injectFields(FXMLLoader.java:1170)
        at javafx.fxml/javafx.fxml.FXMLLoader.access$1600(FXMLLoader.java:105)
        at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:865)
        at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:759)
        at javafx.fxml/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2722)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2552)
        ... 16 more
Exception running application com.torutk.gadget.analogclock.AnalogClockApp

このリフレクションに対応する問題、過去、既に問題に直面して解決をJDK 9 EA上でNetBeans IDE 開発版を動かす(動いた) - torutkのブログに書いていました。う〜