JavaFXを直接実行できるjshellを作った

この記事はJavaFX Advent Calendar 2015の6日目の記事です.
昨日はy_q1mさんの「JavaFX と ScriptEngine を組み合わせた学習用アプリケーションを作る - Qiita」でした.
明日はaoetkさんです.

まえがき

現在JDK9に向けてOpenJDKではREPLツールであるjshellが作られています.
jshellでは簡単にJavaのプログラム要素を実行させることが出来ます.

そこで・・・

まぁ,そうなりますよね.
JavaFXをjshellから叩きたくなる.

ただ,残念ながら今のjshellからJavaFXのウィンドウを出そうとすると・・・

JavaFXが初期化,スタートアップされていない旨の例外が出てしまいます.
JavaFXの初期化,スタートアップにはいくつかの方法があります.
そのうちの一つに,com.sun.javafx.application.PlatformImpl#startupを使用する方法があります*1
Java9からはこのメソッドが公開メソッドになり,javafx.application.Platform#startupというメソッドからアクセスできるようになるらしいです*2
まだ,公開メソッドになっていなかったので,ここではPlatformImpl#startupを使用します.

JavaFXを初期化,スタートアップすれば実行できるのかというと・・・

ご覧の通り,jshellでコードを実行するスレッドがmainのため,JavaFXのスレッドじゃないぞと怒られてしまいます.

Platform#runLaterを用いればJavaFXのスレッドで実行させることが出来るため,jshellからJavaFXのコードを実行させることが出来ます.

変数宣言とウィンドウやコントロールのnewを同時に出来ないし,かなりうざいです.
それに毎回毎回ボイラープレートなPlatform.runLater(()->...)を書きたくないです

これに対して,mike_neckさんは外部にホックするプログラムを書くことによってもう少し綺麗に書けるようにしています.
KullaでJavaFXの操作を対話的にやってみる - mike-neckのブログ
ですが,これはこれで余計なボイラープレートが発生してしまっています.

本題

そこで,普通に書いてJavaFXのスレッドで実行させる様にjshell自体を改造しました.

その名もjfxshellです.
リポジトリは以下にあります.
Bitbucket
コミットは以下です.
*3 *4
bitter_fox / jfxshell / commit / fa3505467601 — Bitbucket


こちらからjarをダウンロードできます.
JDK9を用いて,「java -jar jfxshell.jar」として実行してください.

以下のように直感的にJavaFXのコードを実行させることが出来ます.
ちなみに,JavaFXのパッケージを一通り最初にimportするようにしているため,JavaFXのクラスはimportせずに使用することが出来ます.

素敵ですね.

実装

jshellは入力に応じて,Javaのコードとして実行できるように,ラップを行います.
例えば,「System.out.println("HelloWorld")」という入力はクラスやメソッドでラップされて以下のようになります*5

public class $REPL17 {
    public static Object do_it$() throws Throwable {
        System.out.println("HelloWorld");
        return null;
    }
}

jfxshellではこれをPlatform.runLaterでラップする様にしています.
上記のコードはjfxshellでは以下のようになります.

class $REPL14 {
    public static Object do_it$() throws Throwable {
        java.util.concurrent.CountDownLatch $$$cdl = new java.util.concurrent.CountDownLatch(1);
        com.sun.javafx.application.PlatformImpl.startup(() -> {});
        javafx.application.Platform.setImplicitExit(false);
        Throwable[] $$$thrown = {null};
        javafx.application.Platform.runLater(() -> {
            try {
                System.out.println();

            } catch (Throwable e) {
                $$$thrown[0] = e;
            } finally {
                $$$cdl.countDown();
            }
        });
        $$$cdl.await();
        if ($$$thrown[0] != null) {
            throw $$$thrown[0];
        }
        return null;
    }
}

statupは最初の一回だけ呼びだせば良いのですが,面倒くさいので毎回呼ぶように合わせてラップしています.
また,Stageが閉じられた際にJavaFXのシステムが終了しないように,毎回,setImplicitExit(false)を呼び出しています.
最後に同期を取るために,CountDownLatchを使っています.
これは,com.sun.javafx.application.PlatformImpl.runAndWaitとして同様の機能が実装されており,このメソッドがPlatform.runAndWaitとして公開されれば,そのメソッドで置き換える予定です.
その他,例外処理とか色々しています.

Let's enjoy JavaFX with jfxshell!!

*1:他にも,JFXPanelのインスタンス化などの方法もありますが,内部でこのメソッドを呼んでいます

*2:JavaOneで言っていた

*3:javaが先にJDKが持っているjdk.jshell,jdk.internal.jshellパッケージを読み込んでしまうので,それらに既にあるクラスを書き換えても反映されなかったために,色々面倒臭い事をしている><.もしも自分の実装を先に読ませる,あるいは上書きさせる方法があったら教えてください・・・クラスローダを自前で書くしか無い?

*4:/Xbootclasspathを指定するとシステム標準で読み込まれるクラスを指定できるけど,JDK9からは/Xbootclasspathはサポートされなくなるし,唯一残る/Xbootclasspath/aで指定してもNoClassDefFoundErrorが出てしまう

*5:importは省略