Hatena::ブログ(Diary)

きつねとJava!

2015-12-05

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

この記事はJavaFX Advent Calendar 2015の6日目の記事です.
昨日はy_q1mさんの「no title」でした.
明日はaoetkさんです.

まえがき

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

そこで・・・

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

ただ,残念ながら今のjshellからJavaFXのウィンドウを出そうとすると・・・
f:id:bitter_fox:20151109181803p:image:w360

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

JavaFX初期化スタートアップすれば実行できるのかというと・・・
f:id:bitter_fox:20151109181802p:image:w640

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

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

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

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

本題

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

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


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

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

素敵ですね.

実装

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のインスタンス化などの方法もありますが,内部でこのメソッドを呼んでいます

*2JavaOneで言っていた

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

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

*5:importは省略

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/bitter_fox/20151205/1449327728
Connection: close