ControlsFX 準備編 その2

このEntryは JavaFX Advent Calendar の最終日のエントリーです。

昨日は id:yumix_h さんの JavaFXのHTMLEditorの機能と限界 でした。

今日もControlsFXの紹介です。

23日のエントリー でモジュールではないアプリケーションからの使い方を紹介しました。

今日はモジュールアプリケーションからControlsFXを使用します。

でも、その前にProject Jigsawのモジュールについて簡単に解説しましょう。ここでは、JavaFXでの例で示していますけど、他のモジュールアプリケーションでも同じようなことはあるはずです。

モジュール

module-info.javaがあればモジュールです。そして、module-info.javaには依存性(requires文)、公開範囲(exports文)を記述します。

基本的にはこれだけですが、FXMLを使用している場合、コントローラクラスがリフレクションでアクセスされるため、それを許可するopens文も欠かせません。

では、前回のサンプルがどのようなモジュールに依存しているか調べてみましょう。そのためには、jdepsコマンドを使用します。

C:\controlsfx\controlsfxdemo>jdeps -cp lib\controlsfx-9.0.0.jar -s dist\controlsfxdemo.jar
controlsfxdemo.jar -> lib\controlsfx-9.0.0.jar
controlsfxdemo.jar -> java.base
controlsfxdemo.jar -> javafx.fxml
controlsfxdemo.jar -> javafx.graphics

java.base、javafx.fxml、javafx.graphicsの3つのモジュールに依存していることが分かります。ただし、java.baseだけはmodule-info.javaに記述する必要はありません。

公開するのは、net.javainthebox.fxパッケージなので、これを含めてmodule-info.javaを記述します。

ここでは、モジュール名をnet.javainthebox.controlsfxdemoとします。

module net.javainthebox.controlsfxdemo {
    requires javafx.fxml;
    requires javafx.graphics;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

しかし、これだけではビルド、実行させることができません。もちろん、ControlsFXの扱いのためです。

controlsfx-9.0.0.jarはモジュールではないので、注意する必要があります。

モジュールの種類

実をいうと、モジュールには3種類あります。

module-info.javaが存在するのが普通のモジュールですが、それ以外に2種類のモジュールがあります。

  • 通常のモジュール
  • 自動モジュール (Automatic Module)
  • 名前なしモジュール (Unnamded Module)

Automatic ModuleもUnnamed Moduleもmodule-info.javaがないモジュールです。これらの違いはモジュールが直接依存しているのか、間接的に依存しているかという違いです。

  • モジュールが直接アクセスするJARファイル -> Automatic Module
  • モジュールがアクセスするJARファイルがアクセスするJARファイル -> Unnamed Module

ということになります。

moudle-info.javaを含むモジュールはクラスパスを使用することができず、すべてモジュールパスで扱います。モジュールでないJARファイルもモジュールパスで指定したディレクトリにおけば、モジュールとして扱われるのです。

そして、そのモジュールとして扱われるJARファイルをAutomatic Moduleと呼びます。

モジュールパスでアクセスするということは、module-info.javaに記述しなくてはいけません。そのため、Automatic Moduleとして扱われるJARファイルはモジュール名が必要になります。

この場合、マニフェストファイルMANIFEST.MFのAutomatic-Module-Name属性で指定された名前を使用します。Automatic-Module-Name属性が指定されていない場合、JARファイルの名前がモジュール名として使用されます(バージョン表記などが含まれていても、それは取り除かれます)。

controlsfx-9.0.0.jarファイル場合、Automatic-Module-Name属性は記述されていません。そのため、ファイル名からcontrolsfxがモジュール名として使用されます。

さらに、Automatic Moduleが使用するJARファイルがUnnamed Moduleとして扱われます。これは名前がないので、今までと同じようにクラスパスで指定します。

では、module-info.javaにAutomatic Moduleのcontrolsfxモジュールを追加してみます。

module net.javainthebox.controlsfxdemo {
    requires javafx.fxml;
    requires javafx.graphics;

    requires controlsfx;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

これで、ビルドはできるようになります。しかし、実行してみると...

C:\controlsfx\controlsfxdemo>java -p dist;lib -m net.javainthebox.controlsfxdemo/net.javainthebox.fx.ControlsFXDemo
Exception in Application start method
java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(Unknown Source)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at java.base/sun.launcher.LauncherHelper$FXHelper.main(Unknown Source)
Caused by: java.lang.RuntimeException: Exception in Application start method
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.NoClassDefFoundError: javafx/scene/control/TextField
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
        at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
        at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.findClassInModuleOrNull(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadTypeForPackage(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadType(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.importClass(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.processImport(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.processProcessingInstruction(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.load(Unknown Source)
        at net.javainthebox.controlsfxdemo/net.javainthebox.fx.ControlsFXDemo.start(ControlsFXDemo.java:14)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(Unknown Source)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(Unknown Source)
        ... 1 more
Caused by: java.lang.ClassNotFoundException: javafx.scene.control.TextField
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        ... 33 more
Exception running application net.javainthebox.fx.ControlsFXDemo

C:\controlsfx\controlsfxdemo>

javafx.scene.control.TextFieldクラスが見つからないという例外が起こってしまいました。

これはcontrolsfxがJavaFXのモジュールに依存しているためです。しかし、Automatic Moduleでは依存性が記述できないため、TextFieldクラスを含むモジュールをロードできないためです。

その場合、controlsfxが依存するモジュールも一緒にmodule-info.javaに記述します(起動時オプションで指定する方法もあります)。

controlsfxが依存しているモジュールを調べるために、jdepsコマンドを使用してみましょう。

C:\controlsfx\controlsfxdemo>jdeps -s lib\controlsfx-9.0.0.jar
controlsfx-9.0.0.jar -> java.base
controlsfx-9.0.0.jar -> java.desktop
controlsfx-9.0.0.jar -> javafx.base
controlsfx-9.0.0.jar -> javafx.controls
controlsfx-9.0.0.jar -> javafx.graphics
controlsfx-9.0.0.jar -> javafx.media
controlsfx-9.0.0.jar -> javafx.web

ここで表示されたすべてのモジュールを記述する必要はありません。実際に使用しているモジュール、ここではTextFieldクラスが含まれるjavafx.controlsモジュールだけを追加すればOKです。

module net.javainthebox.controlsfxdemo {
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.graphics;

    requires controlsfx;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

これでモジュールのアプリケーションからControlsFXを使用することができます。

なお、NetBeansではビルドはできるものの、実行はエラーになってしまいます。いろいろ試してみたのですが、結局実行できませんでした。

なので、実行だけはコマンドラインから行っています。

準備編だけで長くなってしまったので、個々のコントロールについてはまた後日。