はじめに
Nashorn の開発チームでは Nashorn から JavaFX にアクセスする仕組みを幾つか検討している様です。現在検討されている実装とその pros / cons について紹介している記事がありましたので、翻訳してみました。ご参照下さい。
おことわり
以下は jlaskey による Nashorn Blog への投稿の翻訳です。原文はhttps://blogs.oracle.com/nashorn/entry/to_shell_or_not_toでご覧頂けます。翻訳文の URL はhttps://blogs.oracle.com/nashorn_ja/entry/to_shell_or_not_toです。
訳文
Nashorn から JavaFX にアクセスする方法を検討しています。二種類の実装を試していますが、そのどちらもそれぞれ解決しないといけない問題があります。
問題のポイントは JavaFX と JDK と Nashorn の依存関係にあります。JavaFX は JDK と一緒に配布されていますが、JDK の一部ではありません。ビルドサイクルも JDK とは異なります。一方、Nashorn は JDK の一部です。Nashorn が JDK の一部であるという事は、JDK の外にある JavaFX に依存するコードは含められない事になります。さらに、JavaFX のプログラムを作る際には JavaFX に依存したコードを書く必要があります。具体的には javafx.application.Application クラスのサブクラスのインスタンスから処理を開始する様にプログラムを記述します。その為 Nashorn と JavaFX を橋渡しするプログラムを実装する際も JavaFX に依存したコードを書く必要がある事になりますが、そうするとそれは先ほどの依存関係があるため、JavaFX のコードは JDK には含められず、独立した場所に置いておく必要があります。
JavaFX へのアクセスを実装する一つ目の方法は jjs と同じようなシェルプログラムを用意する方法です。シェルプログラムの中で JavaFX の Application クラスの init, start, finish メソッドをオーバーライドしておき、外部スクリプトから即座に JavaFX の機能が使える様にします。このシェルプログラムの実装は Nashorn のソースコードリポジトリの中のnashorn/tools/fxshellに入れてあります。実装は以下の様になっています。
/*
* Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.nashorn.tools;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.stage.Stage;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
/**
* このシェルは Nashorn JavaScript 向けに書かれたアプリケーションから JavaFX を実行する為のプログラムです
*/
public class FXShell extends Application {
/**
* スクリプトエンジンマネージャ
*/
private ScriptEngineManager manager;
/**
* Nashorn スクリプトエンジンファクトリー
*/
private NashornScriptEngineFactory factory;
/**
* Nashorn スクリプトエンジンのメインインスタンス
*/
private ScriptEngine engine;
/**
* FX ランチャーがこのクラスのインスタンスを作成する際に必要
*/
public FXShell() {
}
/**
* メインエントリーポイント。実際には使用されない
* @param args コマンドライン引数
*/
public static void main(String[] args) {
launch(args);
}
/*
* アプリケーション側でオーバーライドする
*/
@Override
public void init() throws Exception {
// スクリプトエンジンマネージャ
this.manager = new ScriptEngineManager();
// Nashorn スクリプトエンジンファクトリーの取得。引数の処理に必要。
for (ScriptEngineFactory engineFactory : this.manager.getEngineFactories()) {
if (engineFactory.getEngineName().equals("Oracle Nashorn") &&
engineFactory instanceof NashornScriptEngineFactory) {
this.factory = (NashornScriptEngineFactory)engineFactory;
}
}
// 取得出来なかった場合
if (this.factory == null) {
System.err.println("Nashorn script engine not available");
System.exit(1);
}
// コマンドラインと Java Network Launch Protocol パラメータの取得
final Parameters parameters = getParameters();
// スクリプトの場所とコマンドライン引数の格納用
final List<String> paths = new ArrayList<>();
final List<String> args = new ArrayList<>();
// 適切な JNLP 名前付きパラメータの取得
final Map<String, String> named = parameters.getNamed();
for (Map.Entry<String, String> entry : named.entrySet()) {
final String key = entry.getKey();
final String value = entry.getValue();
if ((key.equals("cp") || key.equals("classpath")) && value != null) {
args.add("-classpath");
args.add(value);
} else if (key.equals("source") && value != null &&
value.toLowerCase().endsWith(".js")) {
paths.add(value);
}
}
// コマンドライン引数として適切な値の取得
boolean addNextArg = false;
boolean addAllArgs = false;
for (String parameter : parameters.getUnnamed()) {
if (addAllArgs || addNextArg) {
args.add(parameter);
addNextArg = false;
} else if (parameter.equals("--")) {
args.add(parameter);
addAllArgs = true;
} else if (parameter.startsWith("-")) {
args.add(parameter);
addNextArg = parameter.equals("-cp") || parameter.equals("-classpath");
} else if (parameter.toLowerCase().endsWith(".js")) {
paths.add(parameter);
}
}
// 取得したパラメータで Nashorn スクリプトエンジンを作成する
engine = factory.getScriptEngine(args.toArray(new String[args.size()]));
// 外部スクリプトの読み込み
for (String path : paths) {
load(path);
}
// 外部スクリプトに init 関数が定義されていた場合は実行する
try {
((Invocable) engine).invokeFunction("init");
} catch (NoSuchMethodException ex) {
// init 関数は存在していなくても良い
}
}
@Override
public void start(Stage stage) throws Exception {
// 外部スクリプトに start 関数が定義されていた場合は実行する
try {
((Invocable) engine).invokeFunction("start", stage);
} catch (NoSuchMethodException ex) {
// start 関数は存在していなくても良い
}
}
@Override
public void stop() throws Exception {
// 外部スクリプトに stop 関数が定義されていた場合は実行する
try {
((Invocable) engine).invokeFunction("stop");
} catch (NoSuchMethodException ex) {
// stop 関数は存在していなくとも良い
}
}
/**
* 指定された JavaScript ファイルの読み込みと実行
* @param path JavaScript ファイルの UTF-8 でエンコードされたパス名
* @return 最後に評価された関数の返り値(使用されない)
*/
private Object load(String path) {
try {
FileInputStream file = new FileInputStream(path);
InputStreamReader input = new InputStreamReader(file, "UTF-8");
return engine.eval(input);
} catch (FileNotFoundException | UnsupportedEncodingException | ScriptException ex) {
ex.printStackTrace();
}
return null;
}
}
このプログラムのコンパイルは、まず Nashorn のリポジトリをダウンロードし、(cd make ; ant build-fxshell) を実行します。すると nashorn/dist/nashornfx.jar ファイルが作成されますので、java -cp dist/nashornfx.jar jdk.nashorn.tools.FXShell
このシェルプログラム方式の優位点は、JavaFX の実行に必要な処理の殆ど全てをシェルプログラム側が引き受けてくれることです。このシェルプログラムを使ってコードを書く場合は、自分のスクリプトには start メソッドと幾つかのクラスの定義を記述するだけで済みます。このシェルプログラムを JDK に含ませる事が出来れば良いのですが、先ほどの依存関係により JDK に JavaFX に依存したコードを入れることが出来ません。
JavaFX へのアクセスのもう一つの実装方法は、jjs プログラムを利用する方法です。Nashorn リポジトリにある最新の Java.extend では、javafx.application.Application クラスのサブクラスを作成することが出来ます。これを利用して JavaFX にアクセスするプログラムを書くことが出来ます。プログラムの制御が JavaFX 側で管理されること、JavaFX の初期化処理を行う場所に制約があることに注意が必要です。
この実装方法の簡易的なプロトタイプとして fxinit.js を作成しました。見慣れない部分もあるかもしれませんが、中で実装している処理はとても簡単です。
GLOBAL = this;
javafx = Packages.javafx;
com.sun.javafx.application.LauncherImpl.launchApplication(
(Java.extend(javafx.application.Application, {
init: function() {
// FX のパッケージとクラスはこれより以前には使用出来ないため、
// ここで定義する必要がある
Stage = javafx.stage.Stage;
scene = javafx.scene;
Scene = scene.Scene;
Group = scene.Group;
chart = scene.chart;
control = scene.control;
Button = control.Button;
StackPane = scene.layout.StackPane;
FXCollections = javafx.collections.FXCollections;
ObservableList = javafx.collections.ObservableList;
Chart = chart.Chart;
CategoryAxis = chart.CategoryAxis;
NumberAxis = chart.NumberAxis;
BarChart = chart.BarChart;
XYChart = chart.XYChart;
Series = chart.XYChart$Series;
Data = chart.XYChart$Data;
TreeView = control.TreeView;
TreeItem = control.TreeItem;
if (GLOBAL.init) {
init();
}
},
start: function(stage) {
if (GLOBAL.start) {
start(stage);
}
},
stop: function() {
if (GLOBAL.stop) {
stop();
}
}
})).class, new (Java.type("java.lang.String[]"))(0));
このプログラムの使用方法は簡単です。JavaFX のHelloWorld.java サンプルを Nashorn 向けに JavaScript で書き直したプログラムがこちらです。
function start(stage) {
stage.title = "Hello World!";
var button = new Button();
button.text = "Say 'Hello World'";
button.onAction = function() print("Hello World!");
var root = new StackPane();
root.children.add(button);
stage.scene = new Scene(root, 300, 250);
stage.show();
}
load("fxinit.js");
load("fxinit.js") で fxinit.js を読み込む場所には注意が必要です。fxinit.js を読み込んだ時点でプログラムの制御が JavaFX 側に移ります。それより後ろに書いたコードはアプリケーション終了時に実行されることになってしまいます。
それから、JavaFX クラスの一部は JavaFX の実行が開始されていないと初期化する事が出来ません。その為 JavaScript プログラムのトップレベルでこれらのクラスを使用することは出来ません。必ず JavaFX の実行が開始されてから呼び出されるメソッドの中で使わなくてはいけません。この様に、fxinit.js の様な実装方法は、プログラムの書き方に制約が生じてしまいます。
これら以外に三番目の実装方法も検討しています。コマンドラインが複雑になりますが、jjs を使って jjs fxinit.js -- myscript.js -- my scripts args と実行する様なイメージです。ここでの -- は、引き続いて引数が出現する事を意味しています。この場合は jjs によってまず fxinit.js が読み込まれ、JavaFX が起動してから myscript.js が実行されます。この様にすれば、myscript.js の部分のプログラムはこれまで見てきたような制約が無く記述する事が出来ます。コマンドラインの記述が複雑になることだけが問題です。
皆さまのご意見をお聞かせ下さい。