OpenJFX時代のJDK選び - もしくはOpenJFX時代のアプリケーション配布

Java 11からJavaFXOracle JDKに含まれなくなったことはみなさんご存知の通り。もともとOpenJDKのJDKプロジェクトにはJavaFX (OpenJFX)は含まれていなかったので、Oracle OpenJDKなどにもJavaFXは含まれていません。

でも、ディストリビューションによってはJavaFXを含んでいるものがあります。たとえば、Liberica JDKなどがJavaFXを含んでいます。

なので、JDKソムリエの @yamadamn さんはJavaFXを使うんだったらLiberica JDKを推すのですが、それはハッキリいってまちがいです。

その理由は、デスクトップアプリケーション特有の視点、つまり

アプリケーションを配布しなくてはいけない

ということに対しての配慮がないからです。

これはどういうことかというと、アプリケーションのユーザーにアプリケーションの実行環境を用意してもらわなくてはいけないということです。

はっきりいうと、ユーザーにLeberica JDKをインストールさせるのは、つらすぎるのです。というか、アプリケーションを実行させるために、事前にJDKをインストールさせるというのが無理があると思うのです。

もし、ユーザーのPCにすでに他のJDKがインストールされていたら、それとは別のJDKをインストールさせるのでしょうか?

たとえば、仕事で使うためにAdoptOpenJDK 11がインストールされているとしましょう。ここにJavaFXのアプリケーションを実行させるためだけに、Liberica JDKをインストールさせて、JDKを切り替えさせるなんてことさせるのでしょうか?

どう考えても無理ですよね。

ましてや、ほとんどのユーザーはJDKをインストールしていないのです。アプリケーションを実行させるためにだけ、アプリケーションとは別にJDKをインストールさせるということは、ユーザにとってハードルが高いのです。

Java 8まではJDKといえば、ほぼOracle JDKだけだったので、まだ許されていたかもしれません。しかし、今は複数のOpenJDKディストリビューションが存在しています。GoogleJDKを検索しても、Liberica JDKはかなり下位にならないと登場しません。そんな誰が作ったかもわからないようなもの (もちろん、Javaアプリの開発者である私たちはLiberica JDKが何かは分かっているはずですが) をユーザにインストールさせるのは酷だと思うわけです。

では、どうすればいいか?

配布するアプリケーションにJREをバンドルさせる

ということです。この観点からJDKを選ぶべきなのです。

JREを同胞させる方法はいくつかありますが、最後に紹介します。

Java 8はやめよう

悪いことはいいません、なるべくJava 11以降のバージョンにしましょう。

OpenJFXではJavaFX 8のバイナリを公開していませんし、ソースのメンテナンスもされていません。たとえば、JavaFX 11以降のバージョンでセキュリティの問題が発見されて、フィックスされたとしても、それがバックポートされることはないのです (ただし、有償のOracle JDK 8だけは、JavaFX 8のサポートを行うようです)。

ということなので、メンテナンスされていないJavaFX 8を使い続けるのはやっぱりよくないと思うわけです。

OpenJFXではJavaFX 11をLTSとして公開しているので、JavaFXはこれを使うのがよいと思います。

gluonhq.com

とはいうものの、上のリンクからダウンロードして使うことはまずないはずです。Mavenのセントラルレポジトリのものを使えるからです。

JavaFX 8からJavaFX 11への変更では、ほとんどコードの変更は必要ないはずです。モジュールに対応させるため、一部のAPIのパッケージが変更されたりしていますが、普通にデスクトップアプリを作るときにはほぼ関係ありません。

異なるのはビルドと実行方法です。

JavaFXのアプリケーションで使用するようなライブラリは、ほとんどのものがJavaFX 11に対応しています。たとえば、GUI部品のライブラリであるControlsFXもJavaFX 11に対応しています。

github.com

問題はJavaの方で、Java 11に対応していないライブラリも多くあります。でも、デスクトップアプリで使うライブラリはたかが知れているのではないでしょうか。デスクトップアプリと通信するサーバー側はいろいろライブラリ使うかもしれませんけど、そちらはJava 11にする必要はないので。

私が関与したJavaFX 11へのマイグレーションはそれほど多いわけではないのですが、JAXBがJava SEに含まれなくなったことだけを考慮すれば、他のライブラリ(たとえば、Apache Commonsのライブラリとか)はJava 11でも問題なく動作しています。

なぜLiberica JDKはやめるべきなのか

Liberica JDKはユーザーにとってハードルが高すぎる話は前述しましたけど、開発する時にもあまりお勧めできません。

というのも、Liberica JDKJavaFXが同胞されているため、他のJDKJavaFXを使った場合とはアプリケーションのビルド/実行方法がまったく違ってしまうからです。

他のJDKを使った場合、JavaFXは単なるライブラリとして扱います。

ほんのちょっとだけ普通のライブラリと違うのが、モジュールアプリケーションではなくてもモジュールの場所を指定するモジュールパス(--module-path)と使用するモジュールの指定(--add-modules)をしなくてはいけないということです。でも、これは普通のライブラリでクラスパスを指定するのと同じようなものです。

しかも、MavenやGradleを使う場合、JavaFXプラグインがあるので、モジュールパスだのなんだのを気にせずに、まったく普通のライブラリと同じように扱うことができます。

ところが、Liberica JDKではモジュールパスもモジュールの指定も必要ありません。いうなれば、Java 8でのJavaFXの扱いと同じなのですが、もう時代は変わっているのです。

もし、作っているアプリケーションが自分だけで使うのであれば、Liberica JDKでもなんら問題ありません。でも、OSSとしてGitHubなどで公開したいのであれば、他のJDKを使っている人たちでもビルド/実行ができるようにしておかなくてはなりません。

そうすると、結局、Liberica JDKに同胞されているJavaFXではなくて、外部の(たとえば、Mavenのセントラルレポジトリにある) JavaFXを使用する設定にならざるをえません。

だとしたら、JavaFXのためにLiberica JDKを使う意義はなくなりますよね。

もし、Liberica JDKを使うのであれば、バンドルされているJavaFXは使わないのがお勧めです。

では、どのJDKを使えばいいのか?

アプリケーションにJREをバンドルするという前提で考えなくてはなりません。

アプリケーションをフリーで配布するのであれば、有償のサポート付きのJDKを使うことはまずないはずですね。となると、無償のJDKを使うことになります。

繰り返しになりますが、JavaFXは単なるライブラリと割り切った方がよいです。そうすれば、どのJDKを使おうがまったく問題ありません。

だいたいのアプリケーションはMavenもしくはGradleを使って開発されていると思いますが、MavenのセントラルレポジトリにあるJavaFXを使うようにすれば、JDKはどれを使っても同じです。

前述したように、Liberica JDKを使う場合も、バンドルされているJavaFXを使わずに、MavenのセントラルレポジトリのJavaFXを使うようにしましょう。

アプリケーションのリリース間隔が半年未満なのであれば、Oracle OpenJDKでも構わないと思います。新しいバージョンをリリースするときには、新しいJREをバンドルするようにしましょう。

できれば、アプリケーションのバージョンアップ時に、旧バージョンを使っているユーザに通知だせるような仕組みにしておくといいですね。

とはいうものの、ほとんどのアプリケーションはLTSを使いたがるでしょうから、無償でLTSを出しているAdoptOpenJDKやLiberica JDKあたりに落ち着くとは思います。

アプリケーションの配布

アプリケーションを配布する時にはJREをバンドルさせるのがお勧めですが、それにはいくつかの方法があります。

  • jlinkを使用してカスタムJREを作成し、そこにアプリケーションのJARやリソース、起動用のバッチ/シェルスクリプトなどを追加する
  • 上のパッケージをjpackageを使ってネイティブアプリケーション化する
  • Gluon Client Pluginを使ってネイティブアプリケーションを作成する

カスタムJREを作成して、アプリケーションのパッケージを作るのが今のところ一番のお勧めです。

jpackageはJava 8まで存在したjavapackager (もしくはjavafxpackager)をリファインしたものです。現状はまだベータ版で、Java 14がリリースのターゲットになっています。

なので、jpackageを使うのは正式リリース後のJava 14以降がよいと思います。

jdk.java.net

最後のGluon Client PluginはGraalVMを使ってネイティブイメージを作成するものです。現状、GraalVMはJava 8ベースなのですが、Gluon Client PluginではJava 11もサポートしています。

docs.gluonhq.com

私もGluon Client Pluginはまだ使っていないので、試してみたら別エントリーで紹介したいと思います。

まとめ

いろいろと雑多な書き方になってしまったので、最後にまとめておきます。

  • JavaFXはもはや普通のライブラリとして扱うべき
  • JavaFX 8はやめてJavaFX 11へ移行しよう
  • JavaFXアプリケーションの配布する時には、JREをバンドルするようにしよう

じゃあ、実際にJavaFXをアプリケーションを作成して、パッケージングするにはどうすればというのは、別エントリーで!

ループアニメーション

このエントリーはJavaFX Advent Calendarの25日目です。

qiita.com

12月15日に開催されたJJUG CCCでStream APIについて登壇しました。その内容はさておき、そのプレゼン資料で使用したアニメーションについて紹介しておきます。

知っている人には当たり前化もしれないですけど。

このプレゼンではStreamということで、流れるものをモチーフにした絵や写真、音楽などを使用しました。タイトルやセクションのタイトルで使用したアニメーションもそう。ベルトコンベアで文字が流れてくるという感じにしています。

このスクリーンショットは文字が切れてしまっているのではなくて、流れている途中なのです。

このアニメーションなんですが、実をいうととても簡単に作ってます。

原理的には短いアニメーションをループさせているだけ。

まず、簡単なサンプルで示しましょう。

package net.javainthebox.loopanim;

import java.util.stream.IntStream;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

public class LoopAnimation extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Pane root = new Pane();
        init(root);

        Scene scene = new Scene(root, 400, 100);
        stage.setScene(scene);
        stage.show();
    }

    void init(Pane pane) {
        Rectangle rectangle = new Rectangle(0.0, 50.0, 40.0, 10.0);
        rectangle.setFill(Color.WHITE);
        rectangle.setStroke(Color.BLACK);
        pane.getChildren().add(rectangle);

        startAnimation(rectangle);
    }

    void startAnimation(Node node) {
        TranslateTransition trans = new TranslateTransition(Duration.millis(1_000), node);
        trans.setToX(40.0);
        trans.setInterpolator(Interpolator.LINEAR);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();
    }

    public static void main(String... args) {
        launch(args);
    }
}

このサンプルを実行すると、長方形が移動を繰り返すというアニメーションを実行します。

この長方形は40ピクセルだけ横方向に移動しています。肝となるのは長方形の幅も40ピクセルだということ。

長方形を描画しているのが、initメソッドです。長方形を生成している部分を抜き出しました。

        Rectangle rectangle = new Rectangle(0.0, 50.0, 40.0, 10.0);

第1, 2引数が長方形の左上の座標。第3引数が幅で、第4引数が高さです。

これに対し、アニメーションしているのが、startAnimationメソッドです。抜粋したのが、以下。

    void startAnimation(Node node) {
        TranslateTransition trans = new TranslateTransition(Duration.millis(1_000), node);
        trans.setToX(40.0);
        trans.setInterpolator(Interpolator.LINEAR);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();
    }

移動のアニメーションを行うのが、TranslateTransitionクラスです。コンストラクタの第1引数がアニメーションの長さ、第2引数がアニメーションを行うターゲットです。

第1引数で1秒を指定しているので、1秒のサイクルで繰り返しを行っています。

そのあとの、setToXメソッドが移動した後のX座標を示しています。0から移動しているので、40ピクセルでちょうど長方形の幅と一緒になっています。

次のsetInterpolatorメソッドははじめゆっくり、中は急いで、最後はまたゆっくりなどのように、アニメーションの動きの見た目のスムーズさなどを指定します。ただ、ここでははじめゆっくりとかやっていると、ぎこちなくなるので、常に同じ速さでアニメーションするLINEARを指定しています。

setCycleCountメソッドはアニメーションの繰り返し回数を指定しますが、ここでは無限ループにしています。

最後にplayメソッドでアニメーションの開始です。

これで、長方形が繰り返し移動するアニメーションができます。

長方形1つだけなら意味ないと思いますよね。そうです、この長方形を並べてあげれば、ベルトコンベアのベルト的なものになるわけです。

移動する時に左側が欠けてしまわないように、画面の外側の部分から用意しておきます。ということで、initメソッドを以下のように変更しました。

    void init(Pane pane) {
        Group group = new Group();
        IntStream.range(0, 11)
                .forEach(i -> {
                    Rectangle rectangle = new Rectangle(40.0*i - 40.0, 50.0, 40.0, 10.0);
                    rectangle.setFill(Color.WHITE);
                    rectangle.setStroke(Color.BLACK);
                    group.getChildren().add(rectangle);
                });
        pane.getChildren().add(group);
        
        startAnimation(group);
    }

複数の長方形をGroupオブジェクトでまとめています。

このGroupオブジェクトをアニメーションすれば、あら不思議、永遠にベルトコンベアが動いているように見えます。

http://f.hatena.ne.jp/skrb/20181225223729

スクリーンショットなので、動きがよく分からないとは思いますが、ぜひサンプルを試してみてください!

同じ原理を使えば、歯車とか、エスカレータなどのアニメーションもできるはず。実際にCCCのプレゼンでは歯車も一緒に回転させています。

ということで、ループアニメーションの紹介でした。

非モジュールJavaFXのアプリをJlinkで配布可能パッケージにする

このエントリーはJavaFX Advent Calendarの24日目です。
qiita.com

Java SE 9からjlinkコマンドが使えるようになり、カスタムJREを作れるようになりました。

ところが、jlinkを使うにはモジュールアプリケーションでないとダメだというような間違った言説が流れているような気がします。

jlinkで作るカスタムJREは非モジュールアプリケーションでも可能なんです!!!!

ということで、JavaFXの非モジュールアプリでカスタムJREを作ってみましょう。

サンプルは超簡単なラベルを表示するだけのものです。

package net.javainthebox.hello;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;

public class Hello extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        var label = new Label("Hello, JavaFX!");
        label.setAlignment(Pos.CENTER);
        var scene = new Scene(label, 200, 50);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String... args) {
        launch(args);
    }

}

これをMavenでビルドするのは、以下のエントリーでも書いた通り(pom.xmlに記述した各種バージョンは新しいものにしてくださいね)。

skrb.hatenablog.com

もちろん、自分でビルドするのでも全然かまいません。

ここでは、C:\hello\srcにソースファイルがあるとします。また、JavaFX SDKは"C:\Program Files\Java\javafx-sdk-11.0.1"に配置してあります。

C:\hello\src>javac --module-path "C:\Program Files\Java\javafx-sdk-11.0.1\lib" --add-modules javafx.controls -d ..\bin net\javainthebox\hello\Hello.java

C:\hello\src>cd ..

C:\hello>jar --create --file hello.jar -C bin .

これで、helloディレクトリ直下にhello.jarができました。試しに実行してみましょう。

C:\hello>java --module-path "C:\Program Files\Java\javafx-sdk-11.0.1\lib" --add-modules javafx.controls -cp hello.jar net.javainthebox.hello.Hello

これでラベルだけのフレームが表示されるはずです。

カスタムJREを作成する

jlinkを使う前にjdepsを使わなくてはいけないような風潮がありますが、使っているモジュール(ここではJavaFXのモジュール)が明らかであれば、jdepsを使う必要はありません。

また、非モジュールアプリケーションの場合、jdepsの--print-module-depsや--list-depsオプションは使用できません(JDKのモジュールしか表示されません)。

もし、jdepsを使うのであれば、オプションなしに使用します(もしくはサマリー表示の-s)。

C:\hello>jdeps --module-path "C:\Program Files\Java\javafx-sdk-11.0.1\lib" -s hello.jar
hello.jar
hello.jar -> java.base
hello.jar -> javafx.controls
hello.jar -> javafx.graphics
javafx.base -> java.base
javafx.base -> java.desktop
javafx.controls -> java.base
javafx.controls -> javafx.base
javafx.controls -> javafx.graphics
javafx.fxml -> java.base
javafx.fxml -> java.scripting
javafx.fxml -> java.xml
javafx.fxml -> javafx.base
javafx.fxml -> javafx.graphics
javafx.graphics -> java.base
javafx.graphics -> java.desktop
javafx.graphics -> java.xml
javafx.graphics -> javafx.base
javafx.graphics -> jdk.unsupported
javafx.media -> JDK removed internal API
javafx.media -> java.base
javafx.media -> javafx.base
javafx.media -> javafx.graphics
javafx.swing -> java.base
javafx.swing -> java.datatransfer
javafx.swing -> java.desktop
javafx.swing -> javafx.base
javafx.swing -> javafx.graphics
javafx.swing -> jdk.unsupported.desktop
javafx.swt -> java.base
javafx.swt -> javafx.base
javafx.swt -> javafx.graphics
javafx.swt -> 見つかりません
javafx.web -> java.base
javafx.web -> java.desktop
javafx.web -> java.xml
javafx.web -> javafx.base
javafx.web -> javafx.controls
javafx.web -> javafx.graphics
javafx.web -> javafx.media
javafx.web -> jdk.jsobject
javafx.web -> jdk.xml.dom

ずらずらと出てきますが、重要なのは上の方の3行。hello.jarが直接使用しているのはjava.base、javafx.controls、javafx.graphicsモジュールの3つだということ。javafx.graphicsモジュールはjavafx.controlsモジュールが依存しているので、実質はjava.baseとjavafx.controlsだけでOKです。

これが分かればjlinkでJREが作れます。というか、javacやjavaで実行するときに--add-modulesでjavafx.controlsを加えているので、jdepsなど使わなくても何を使っているかは自明ですけどね。

なお、jlinkでJREを作る時、JavaFX SDKのJARファイルよりJMODファイルを使う方がおススメです。JARでもいいのですが、JARファイルにはネイティブライブラリ(DLLやSO)が含まれていないので、別途コピーする必要があります。

JMODファイルであればネイティブライブラリも含んでいるので、簡単です。ここではJMODファイルは"C:\Program Files\Java\javafx-sdk-11.0.1\mods"に置いてあるとします。

C:\hello>jlink --module-path "C:\Program Files\Java\javafx-sdk-11.0.1\mods" --add-modules java.base,javafx.controls --compress=2 --output jre

これで、jreディレクトリにJREができました。

お分かりだろうとは思いますが、このJREにはアプリケーションのJARファイルは含まれていません。カスタムJREに含められるのはあくまでもモジュールだけです。この場合は標準のモジュールとJavaFXのモジュールで構成されています。

他にモジュールで提供されているライブラリを使用しているのであれば、それもJREに含めることができます。

逆にいうと、非モジュールのライブラリやアプリケーションJARはJREには含まれていないので、実行にはクラスパスで指定する必要があります。

では、実行してみましょう。ここでは、クラスパスでhello.jarを指定しています。

C:\hello>jre\bin\java -cp hello.jar net.javainthebox.hello.Hello

JREのモジュールにjavafx.controlsなどが含まれているので、--module-pathや--add-modulesなどのオプションは必要ありません。

では、配布可能にするためにjreディレクトリにhello.jarもコピーしておきましょう。

C:\hello>copy hello.jar jre
        1 個のファイルをコピーしました。

後は、実行に便利なようにバッチファイル、もしくはシェルスクリプトを作っておきましょう。たとえば、hello.batを作ったとします。ファイルの内容は以下の1行だけ。

bin\java -cp hello.jar net.javainthebox.hello.Hello

試しに、hello.batで正しく実行できるかどうか試しておいてください。

これで、jreディレクトリをそのまま配布することができます(ZIPファイルにするとか、ディレクトリ名を変えるとかはご自由に)。

OpenJFX11 + OpenJDK11 + Maven で JavaFX を動かす

前回のエントリーはOpenJFXとOpenJDKでJavaFXを動かそうというものでした。

skrb.hatenablog.com

でも、OpenJFX SDKにパスを通さなくてはいけないなど、ちょっとめんどうだったのもたしか。

そんなおり、GluonのJoahn VosさんがOpenJFXをMavenでも使えるようにプルリク出してくれました。

github.com

この結果、Maven CentralにOpenJFXが登録されました!!

さっそく使ってみましょう。

サンプルコードはこちら。

package net.javainthebox.hello;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;

public class Hello extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        var label = new Label("Hello, JavaFX + Maven!");
        label.setAlignment(Pos.CENTER);
        var scene = new Scene(label, 200, 50);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String... args) {
        launch(args);
    }

}

JavaFXHello Worldです。

さて、これをコンパイル、実行するためのpom.xmlがこちら。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.javainthebox</groupId>
    <artifactId>hello</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>10</maven.compiler.source>
        <maven.compiler.target>10</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>11-ea+19</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <release>11</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>net.javainthebox.hello.Hello</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

このサンプルではjavafx.controlsモジュールを使用しているので、dependencyにはjavafx.controlsを指定しています。Maven Centralではモジュールのピリオドをハイフンで置き換えた名前になっています。

javafx.controlsはjavafx.graphics、javafx.baseに依存しているので、それらも自動的にロードされます。
これ以外にjavafx.fxml、javafx.media、javafx.swing、javafx.webモジュールもちゃんと登録されています。

では、まずはコンパイルしてみましょう。

C:\hello>mvn clean compile
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< net.javainthebox:hello >-----------------------
[INFO] Building hello 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello ---
[INFO] Deleting C:\Users\yuichi\Desktop\hello\target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\yuichi\Desktop\hello\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.7.0:compile (default-compile) @ hello ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to C:\Users\yuichi\Desktop\hello\target\classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.103 s
[INFO] Finished at: 2018-07-25T21:44:22+09:00
[INFO] ------------------------------------------------------------------------

C:\hello>

あっさり成功しました。もちろん、初回はモジュールのダウンロードがあるはずです。

では、実行してみましょう。

C:\hello>mvn exec:java
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< net.javainthebox:hello >-----------------------
[INFO] Building hello 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ hello ---

ここまで表示されれば、フレームが表示されているはずです。
OpenJFX SDKを使うよりは、とても簡単にJavaFXのアプリケーションを書けるようになりました。

ところで、JavaFXはネイティブライブラリが必要です。Maven Centralを見てみると、やっぱりプラットフォームごとに分けられてました。下のリンクはjavafx.graphicsのものです。

Central Repository: org/openjfx/javafx-graphics/11-ea+19

pom.xmlを見てみると、プラットフォームによって該当するJARファイルも依存があるように書かれてますね。

現状の問題点

Mavenで実行するとき、exec:javaであれば問題ないのですが、exec:execで別プロセスとして実行させると以下のようなメッセージを出して、実行ができません。

C:\Users\yuichi\Desktop\hello>mvn exec:exec
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< net.javainthebox:hello >-----------------------
[INFO] Building hello 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.6.0:exec (default-cli) @ hello ---
エラー: JavaFXランタイム・コンポーネントが不足しており、このアプリケーションの実行に必要です
[ERROR] Command execution failed.
org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1)
    at org.apache.commons.exec.DefaultExecutor.executeInternal (DefaultExecutor.java:404)
    at org.apache.commons.exec.DefaultExecutor.execute (DefaultExecutor.java:166)

ロードされたクラスを調べてみたのですが、Applicationクラスはロードされているものの、その他のJavaFXに関連するクラスがロードされていません。もうちょっと調べてみようと思ってます。

Using JavaFX with OpenJFX + OpenJDK

Hello, JavaFX developers using Oracle JDK!

You may know, we can't get Oracle JDK for free from Java SE 11. Therefore, we have no choice to move to OpenJDK from Oracle JDK.

But, JavaFX is NOT included in OpenJDK!!

You may think "OpenJFX is a project in OpenJDK project, isn't it?" Yes, OpenJFX is a project in OpenJDK project. But, OpenJFX isn't included in JDK project of OpenJDK project.

So, we should use OpenJFX with OpenJDK.

You are worried "I should build OpenJFX?" Don't worry! OpenJFX build can be downloaded in jdk.java.net site now.

But, even if your application is not modular application, you should set module configuration!!

I'll describe as follows. If you'd like to know only setting, you can skip to "JavaFX Configuration for OpenJFX."

Set up OpenJDK and OpenJFX

You can download OpenJDK and OpenJFX belowed URL.

JDK Builds from Oracle

OpenJFX
OpenJFX Early-Access Builds

In this article, I used early access of OpenJDK 11 on Windows.

OpenJDK 11
JDK 11 Early-Access Builds

I used openjfx-11-ea+13 that was released on 9 May and jdk-11-ea+15 that was released on 25 May.

After downloading openjdk-11-ea+15_windows-x64_bin.tar.gz, you should decompress it.

OpenJDK doesn't include installer, so you just add the path to jdk-11\bin directory.

In the same way, decompress openjfx-11-ea+13_windows-x64_bin-sdk.zip after download it.

There are 3 directories in javafx-sdk-11 directory: bin, legal and lib.

There are some native libraries (DLL in Windows) in bin directory, so you also add the path to bin directory.

There are some modular JAR files in lib directory.

Try to execute JavaFX sample application

First of all, let's try to compile and execute a sample application.

Here is the sample application.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;

public class FxSample extends Application {
    public void start(Stage stage) {
        Button button = new Button("OK");
        button.setOnAction(e -> System.out.println("Clicked!"));

        Scene scene = new Scene(button, 300, 200);
        stage.setScene(scene);

        stage.setTitle("FxSample");
        stage.show();
    }

    public static void main(String... args) {
        Application.launch(args);
    }
}

The application indicates "OK" button only, and print out "Cliecked!" when you click the button.

Of course, I don't make module.

I located FxSample.java in javafx-sdk-11 directory.

C:\javafx-sdk-11>java -version
openjdk version "11-ea" 2018-09-25
OpenJDK Runtime Environment 18.9 (build 11-ea+15)
OpenJDK 64-Bit Server VM 18.9 (build 11-ea+15, mixed mode)

C:\javafx-sdk-11>javac -cp lib\* FxSample.java

C:\javafx-sdk-11>java -cp lib\*;. FxSample
Error: JavaFX runtime components are missing, and are required to run this application

C:\javafx-sdk-11>

Compile was done, but error occurred in execution!

I don't understand the error message. What is "runtime components"?

At first I thought JVM didn't load DLL, but the problem seems to be not that.

To check what happens, I add -verbose option for execution.

C:\javafx-sdk-11>java -verbose -cp lib\*;. FxSample
[0.008s][info][class,load] opened: C:\jdk-11\lib\modules
[0.018s][info][class,load] java.lang.Object source: jrt:/java.base
[0.019s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.019s][info][class,load] java.lang.Comparable source: jrt:/java.base
[0.023s][info][class,load] java.lang.CharSequence source: jrt:/java.base
[0.024s][info][class,load] java.lang.String source: jrt:/java.base
[0.025s][info][class,load] java.lang.reflect.AnnotatedElement source: jrt:/java.base
[0.025s][info][class,load] java.lang.reflect.GenericDeclaration source: jrt:/java.base
[0.026s][info][class,load] java.lang.reflect.Type source: jrt:/java.base
[0.027s][info][class,load] java.lang.Class source: jrt:/java.base
     <<snip, snip, snip>>

You can see what class was loaded by using -verbose option. For example, java.lang.Object class in java.base module was loaded firstly.

I checked all loaded classes, but JavaFX classes weren't loaded!!

That is why execution was failed.

I wonder why JavaFX classes weren't loadded in spite of setting classpath.

I remembered the case of Java EE modules such as JAXB in Java SE 9 or 10.

These modules are included in Java SE, but are not included in standard modulepath. Therefore, we should add --add-modules options when using these modules even if we execute non-modular application.

JavaFX case may be the same.

If so, we should set modulepath and --add-modules option.

JavaFX Configuration for OpenJFX

We set --module-path option or -p option for modulepath. value of modulepath is not file, but directory that modular jar files are located in.

In this case, I set lib directory for modulepath.

If modular application, we describes modular dependencies in module-info.java file. But, we use --add-modules option when non-modular application.

Which modules the application use? You can check them by jdeps tool.

C:\javafx-sdk-11>jdeps --module-path lib -s FxSample.class
FxSample.class -> java.base
FxSample.class -> javafx.base
FxSample.class -> javafx.controls
FxSample.class -> javafx.graphics
javafx.base -> java.base
javafx.base -> java.desktop
javafx.controls -> java.base
javafx.controls -> javafx.base
javafx.controls -> javafx.graphics
    <<snip, snip, snip>>

We don't use -p option in jdeps tool, so use --module-path. The other option -s is for showing dependency summary only.

In the result, we can know FxSample class uses java.base, javafx.base, javafx.controls and javafx.graphics.

But, we don't need to set java.base module because it is loaded by default.

Moreover, javafx.controls module uses javafx.base and javafx.graphics. So, we should set only javafx.controls for --add-modules.

Then, let's execute!

C:\javafx-sdk-11>java -p lib --add-modules javafx.controls FxSample

If going well, you can see the window below.

OpenJFX + OpenJDK で JavaFX を動かす

今までOracle JDKJavaFXを使っていたみなさま、こんにちは。

なんと、Java SE 11からOracle JDKは無償版がなくなりました!(開発のみであれば、無償でも使用できますけど...) しかたないので、OpenJDKを使わざるをえないと思われている方も多いと思います。

ところが、OpenJDKにはJavaFXが含まれていないのです!!

「えっ、OpenJFXってOpenJDKのプロジェクトじゃないの?」と思われるかもしれません。確かにOpenJFXはOpenJDKのプロジェクトなのです。でも、OpenJDKのプロジェクトがJava SEのRIであるOpenJDKに含まれるというわけではありません。

OpenJFXも、Java SEのRIであるところのOpenJDKには含まれていないのです。

とはいっても、OpenJFXを自分でビルドして使うのはハードルが高すぎますね。そうしたら、OpenJFXのビルドを公開してくれるようになりました!

これで、OpenJFXとOpenJDKでJavaFXを使えます!!

ところが、結論を先にいうと、「OpenJFX + OpenJDKはモジュールではないアプリケーションでも、モジュールを指定しないと動きません」です。

以下に解説しますが、手っ取り早くやり方を知りたい人は途中をすっ飛ばして、「OpenJFXでのJavaFXの起動設定」を読んでください。

OpenJDKとOpenJFXのセットアップ

OpenJFXのダウンロードは下のURLからできます。

OpenJFX Early-Access Builds

http://jdk.java.net はOpenJDKも公開しています。今回は、まだリリース前ですがOpenJDK 11のEaryly Accessを試してみましょう。使用したのは、5月25日に公開されたjdk-11-ea+15です。

OpenJDKはインストーラーは含まれていないので、自分でtar.gzのファイルを展開して、binにパスを通しておきます。

次にOpenJFXです。OpenJFXは5月9日に公開されたopenjfx-11-ea+13を使用しました。

ダウンロードしたopenjfx-11-ea+13_windows-x64_bin-sdk.zipを展開すると、bin、legal、libの3つのディレクトリが含まれていることが分かります。binディレクトリにはネイティブライブラリ(Windowsの場合はDLL)が配置されているので、ここにもパスを通しておきます。

libディレクトリにはJARファイルがモジュールごとに配置されてます。

とりあえず、JavaFXのサンプルを実行してみる

では、JavaFXのプログラムをビルドして、実行してみましょう。

今回のサンプルはこちら。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;

public class FxSample extends Application {
    public void start(Stage stage) {
        Button button = new Button("OK");
        button.setOnAction(e -> System.out.println("Clicked!"));

        Scene scene = new Scene(button, 300, 200);
        stage.setScene(scene);

        stage.setTitle("FxSample");
        stage.show();
    }

    public static void main(String... args) {
        Application.launch(args);
    }
}

ボタンを表示して、ボタンをクリックすると"Clicked!"と標準出力に出力するだけのプログラムです。

もちろん、モジュールにはしません。

では、さっそくコンパイルして、実行してみましょう。

C:\javafx-sdk-11>java -version
openjdk version "11-ea" 2018-09-25
OpenJDK Runtime Environment 18.9 (build 11-ea+15)
OpenJDK 64-Bit Server VM 18.9 (build 11-ea+15, mixed mode)

C:\javafx-sdk-11>javac -cp lib\* FxSample.java

C:\javafx-sdk-11>java -cp lib\*;. FxSample
エラー: JavaFXランタイム・コンポーネントが不足しており、このアプリケーションの実行に必要です

C:\javafx-sdk-11>

コンパイルはできたのですが、実行ができません!

しかも、このエラーメッセージ、全然意味が分かりません。ランタイムコンポーネントって何なんでしょう?

はじめはDLLが読み込まれていないのかと思ったのですが、どうやら違ったようです。

そこで、何が起こっているのか知るためにverboseオプションをつけて起動してみました。

C:\javafx-sdk-11>java -verbose -cp lib\*;. FxSample
[0.008s][info][class,load] opened: C:\jdk-11\lib\modules
[0.018s][info][class,load] java.lang.Object source: jrt:/java.base
[0.019s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.019s][info][class,load] java.lang.Comparable source: jrt:/java.base
[0.023s][info][class,load] java.lang.CharSequence source: jrt:/java.base
[0.024s][info][class,load] java.lang.String source: jrt:/java.base
[0.025s][info][class,load] java.lang.reflect.AnnotatedElement source: jrt:/java.base
[0.025s][info][class,load] java.lang.reflect.GenericDeclaration source: jrt:/java.base
[0.026s][info][class,load] java.lang.reflect.Type source: jrt:/java.base
[0.027s][info][class,load] java.lang.Class source: jrt:/java.base
     <<以下、長くなるので省略>>

verboseオプションをつけると、どのクラスがロードされているのが分かります。たとえば、一番初めにロードされたのがjava.baseモジュールに含まれるjava.lang.Objectクラスだということが分かります。

ロードされたクラスをチェックしてみると、なんとJavaFXのクラスがまったくロードされていないのでした。

それじゃ、実行できるわけがないですね。

しかし、クラスパスは指定しているのに、なぜクラスがロードされていないのでしょう?

ここで、思い浮かぶのがJava SE 10でのJAXBなどJava EE系のモジュールの扱いです。これらのモジュールは標準のモジュールパスからは外されているので、使用するときにはモジュールでないアプリケーションであっても--add-modulesで指定する必要があるのです。

もしかしたら、ここで起こっていることも同じことなのかもしれません。

だとしたら、解決は簡単で、モジュールパスにlibを追加して、ロードするモジュールを指定すればいいのです。

OpenJFXでのJavaFXの起動設定

では、実際に起動設定を見ていきましょう。

モジュールパスは--module-pathオプションもしくは-pオプションで指定します。モジュールパスで指定するのはモジュールが配置されているディレクトリです。

モジュール化したアプリケーションであれば、どのモジュールを使用するかはmodule-infoに記述します。

しかし、モジュールではないアプリケーションでは、--add-modulesオプションで指定する必要があります。

どのモジュールを使用しているかはjdepsコマンドで調べることができます。

C:\javafx-sdk-11>jdeps --module-path lib -s FxSample.class
FxSample.class -> java.base
FxSample.class -> javafx.base
FxSample.class -> javafx.controls
FxSample.class -> javafx.graphics
javafx.base -> java.base
javafx.base -> java.desktop
javafx.controls -> java.base
javafx.controls -> javafx.base
javafx.controls -> javafx.graphics
    <<以下、省略>>

jdepsコマンドでは、モジュールパスの指定に-pは使用できないので、--module-pathを使用します。もう1つの-sオプションはシンプル表示のためのオプションです。

この結果を見ると、FxSampleクラスはjava.base、javafx.base、javafx.controls、javafx.graphicsの4つのモジュールを使用していることが分かります。

しかし、java.baseモジュールはデフォルトでロードされるので、指定する必要はありません。

また、javafx.controlsモジュールはjavafx.baseモジュールとjavafx.graphicsモジュールを使用しています。このため、javafx.controlsだけを指定すれば、javafx.baseモジュールとjavafx.graphicsモジュールは自動的にロードされます。

この結果、--add-modulesオプションで指定するのはjavafx.controlsモジュールだけでよいことが分かりました。

では、実行してみましょう。

C:\javafx-sdk-11>java -p lib --add-modules javafx.controls FxSample

これで、以下のようなウィンドウが表示されるはずです。

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ではビルドはできるものの、実行はエラーになってしまいます。いろいろ試してみたのですが、結局実行できませんでした。

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

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