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 IDEはJava SE 9対応版が本来9月にリリースされる予定でした。ですが、まだリリースされておらず、開発途中版のnightly buildを使用しています。
ソースコードの修正
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"
JDK 8で生成したインストーラーファイル(MSI形式)のサイズは61MBでしたが、JDK 9で生成したインストーラーファイル(MSI形式)のサイズは31MBとほぼ半分となりました。
実行
まだいくつか関門があります。
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のブログに書いていました。う〜