5. スクリプティング機能
2007.02.07 株式会社四次元データ 鈴木 圭
- 5.1. Hello Scripting
- 5.2. ScriptEngine の内部アーキテクチャ
- 5.3. ScriptEngine のその他の機能
- 5.4. jrunscript
EoD(Ease of Developing)の強化の一貫として導入されたスクリプティング機能について解説します。スクリプティング機能とは、Java プログラムと JavaScript などのスクリプト言語を連携して開発するための機能です。単純なスクリプトの実行だけではなく、Java からスクリプトで定義されたメソッドにアクセスすることや、スクリプトから Java オブジェクトにアクセスすることなどが可能です。
スクリプティング機能に関しては JSR-223 Scripting for Java™ Platform において定義されています。JSR 223 で定義される Scripting API は、特定のスクリプト言語に依存しない汎用的なスクリプティング・フレームワークとして設計されていますが、標準ではオープン・ソースの JavaScript の実装である Mozilla Rhino が搭載されます。本稿では、特に断らない限りスクリプト言語として Mozilla Rhino による JavaScript を使用することを前提として解説します。
5.1 Hello Scripting
まずは最も簡単な例として、文字列で指定したスクリプト(JavaScript コード)を実行するプログラムを示します:
import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; public class ScriptingMain { public static void main(String[] arguments) { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); try { String script = "print('Hello Scripting')"; engine.eval(script); } catch(ScriptException exception) { exception.printStackTrace(); } } }
このプログラムに含まれる、スクリプティング機能に関するクラス/インタフェースを以下に示します:
- ScriptEngineManager クラス
- スクリプティング機能の開始点を提供します。ScriptEngine などのインスタンスは ScriptEngineManager を通して取得します。 - ScriptEngine インタフェース
- スクリプトの実行エンジンを表すインタフェースです。eval メソッドによってスクリプトを評価することができます。 - ScriptException クラス
- スクリプティング機能によるエラーを表す例外クラスです。例外の発生したファイル名や行番号を得ることができます。
スクリプティング機能を利用するために、最初に javax.script.ScriptEngineManager クラスをインスタンス化しています。そして ScriptEngineManager クラスの getEngineByName メソッドで JavaScript の実行エンジンを取得しています。getEngineByName メソッドは与えられた名前に対応するスクリプトの実行エンジンを返すメソッドです。ここでは JavaScript の実行エンジンを取得したいので、引数に "javascript" を渡しています(詳しくは後述)。
次に ScriptEngine#eval メソッドでスクリプトを評価しています。eval メソッドには引数に java.io.Reader を取るバージョンもあります。ここでは "Hello Scripting" と表示するスクリプトを渡しています。スクリプトの評価でエラーが発生した場合は javax.script.ScriptException が投げられます。ScriptException にはエラーの発生したファイル名や行番号を取得するメソッドが含まれます。
Mustang のスクリプティング機能は、単純なスクリプトの実行だけではなく以下のようなことも可能です:
- 5.1.1. スクリプト上で Java オブジェクトを生成する
- 5.1.2. スクリプトから Java のオブジェクトにアクセスする
- 5.1.3. Java からスクリプトのオブジェクトにアクセスする
5.1.1. スクリプト上で Java オブジェクトを生成する
スクリプト上で Java のクラスを生成する具体的な方法は、使用するスクリプティング言語によって異なります。ここでは Mustang に標準搭載の Mozilla Rhino の場合について説明します。
スクリプト上で Java オブジェクトを生成する方法は、他の JavaScript のクラスを生成するときと同じく「new」によって行います:
javaDate = new java.util.Date();
この例では生成するクラスを完全限定名によって指定していますが、importPackage によって事前にパッケージをインポートすることで、クラス名だけを指定してインスタンス化することもできます:
importPackage(javax.swing); frame = new JFrame("sample"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(400, 300); frame.setLocationByPlatform(true); frame.setVisible(true);
スクリプト上で Java オブジェクトを扱う場合、Java オブジェクトの public フィールド/メソッドに対してアクセスすることができます。
注意として、Date クラスなど Java にも JavaScript にも存在するクラスの場合、importPackage でパッケージをインポートし、なおかつ new に(完全限定名ではなく)クラス名だけを指定すると、エラーとはならず JavaScript で定義されているクラスがインスタンス化されるようです:
importPackage(java.util); // java.util.Date ではなく JavaScript の Date クラスが生成される date = new Date();
5.1.2. スクリプトから Java のオブジェクトにアクセスする
スクリプトから Java のオブジェクトにアクセスするためには、事前に Java 側からスクリプト環境にオブジェクトを渡す必要があります。Java プログラムからスクリプト側にオブジェクトを渡すには ScriptEngine#put(String key, Object value) メソッドを使用します。put メソッドの第一引数にはキーとなる値、第二引数には渡したいオブジェクトを指定します。put メソッドの第一引数に指定した値は、スクリプト上でアクセスするときの変数名となります。
以下の例では、"message" というキーで "Hey!!" という文字列を関連付けた後に、sample.js を実行しています:
ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); engine.put("message", "Hey!!"); engine.eval(new FileReader("sample.js"));
sample.js の中では、渡されたオブジェクトを message という変数として扱うことができます:
// sample.js print(message);
5.1.3. Java からスクリプトのオブジェクトにアクセスする
ScriptEngine の実装によっては、Java プログラムからスクリプトで定義された関数にアクセスすることを許している場合があります(スクリプト言語によって「関数」ではなく「メソッド」や「プロシージャ」などと呼ばれることもありますが、ここでは Java の用語の「メソッド」との区別を明確にするために「関数」で統一します)。Java プログラムからの関数呼び出しを許している ScriptEngine は javax.script.Invocable インタフェースを実装しています。ScriptEngine が Invocable を実装しているかどうかは、以下のように判定することができます:
ScriptEngine scriptEngine; ... if(scriptEngine instanceof Invocable) { ... scriptEngine は Invocable を実装している }
Invocable の実装は任意なので、全ての ScriptEngine の実装で使用可能ではありませんが、Mustang に標準搭載されている JavaScript エンジン Mozilla Rhino による実装は Invocable に対応しています。
Invocable ではトップ・レベルの関数を呼び出すメソッドと指定したオブジェクトの関数を呼び出すメソッドが定義されています:
- Object invokeFunction(String name, Object... args)
- トップ・レベルの関数を呼び出します。 - Object invokeMethod(Object object, String name, Object... args)
- 指定したオブジェクトの関数を呼び出します。
トップ・レベルの関数を呼び出す
トップレベルの関数を呼び出す場合は invokeFunction メソッドを使用します。invokeFunction メソッドの第一引数は呼び出す関数の名前、第二引数には関数に渡す引数を指定します。
以下にサンプル・コードを示します:
ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); // スクリプトでトップ・レベルの関数 echo を定義する. engine.eval("function echo(message) { print(message); }"); // 定義した echo 関数を呼び出す. Invocable invocable = (Invocable)engine; invocable.invokeFunction("echo", "hello");
この例では、スクリプト側で echo というトップ・レベルの関数を定義し、それを Java 側から呼び出しています。
これを実行すると、以下のような出力が得られます:
hello
オブジェクトの関数を呼び出す
特定のオブジェクトの関数を呼び出すには invokeMethod メソッドを使用します。invokeMethod の第一引数と第二引数には対象のオブジェクトと関数の名前を指定します。第三引数は関数に渡す引数です。第一引数に渡すオブジェクトに関して、例えば以下のようにスクリプトの中で person という名前のオブジェクトが作成されているとします:
// JavaScript のコード person = new Object();
その場合、invokeMethod の第一引数には(スクリプトの中で定義されている)person に対応する Java オブジェクトを指定します。スクリプトで定義されているオブジェクトに対応する Java オブジェクトを取得する方法はいくつかありますが、その中の一つとして ScriptEngine#get メソッドを使用する方法があります:
// Java のコード ScriptEngine engine; ... Object person = engine.get("person");
以下に invokeMethod の使用例を示します:
ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); // hoge オブジェクトを作成し、echo 関数を定義する. engine.eval("hoge = new Object();"); engine.eval("hoge.echo = function(message) { print(message); }"); // スクリプトの実行コンテキストから hoge という名前のオブジェクトを取得する. Object hoge = engine.get("hoge"); // hoge オブジェクトの echo 関数を呼び出す. Invocable invocable = (Invocable)engine; invocable.invokeMethod(hoge, "echo", "hello");
トップ・レベル関数を呼び出す場合の例と似ていますが、このコードではスクリプト側で echo 関数を持つ hoge オブジェクトを作成し、それを Java 側から呼び出しています。
5.2. ScriptEngine の内部アーキテクチャ
スクリプティング機能の簡単な使い方についての説明は終わりましたので、次は ScriptEngine の内部アーキテクチャに関する説明を行います:
5.2.1. スクリプトの実行エンジンの情報
先ほどは ScriptEngine のインスタンスの取得に ScriptEngineManager#getEngineByName メソッドを使用しましたが、getEngineByName メソッドに渡すことのできる値については詳しく説明しませんでした。そこで、ここではスクリプトの実行に関する、もう少し詳しい説明を行います。
ScriptEngine の取得
ScriptEngineManager は ScriptEngine インスタンスを取得するためのいくつかのメソッドを持ちます:
- ScriptEngine getEngineByExtension(String extension)
- 拡張子に対応する ScriptEngine を返します。 - ScriptEngine getEngineByMimeType(String mimeType)
- MIME タイプに対応する ScriptEngine を返します。 - ScriptEngine getEngineByName(String shortName)
- 指定されたショート・ネームを持つ ScriptEngine を返します。
これらのメソッドの引数に渡すことのできる値は、javax.script.ScriptEngineFactory の getExtensions、getMimeTypes、getNames メソッドで得ることができます。
ScriptEngineFactory
ScriptEngineFactory は ScriptEngine をインスタンス化する役割を持つインタフェースです。ScriptEngineManager の getEngineFactories メソッドを使用すると、利用可能な ScriptEngineFactory を得ることができます。ScriptEngineFactory には、対応している言語やバージョン、実装の名前("Mozilla Rhino" など)、そのバージョンなどを得るためのメソッドなどが含まれており、スクリプト言語や実装に関する情報を得ることができます。以下に ScriptEngine の情報を得るためのメソッドを示します:
- String getEngineName()
- ScriptEngine の実装のフル・ネームを返します。 - String getEngineVersion()
- ScriptEngine の実装のバージョンを返します。 - String getLanguageName()
- サポートするスクリプト言語の名前を返します。 - String getLanguageVersion()
- サポートするスクリプト言語のバージョンを返します。 - List<String> getExtensions()
- ScriptEngineManager#getEngineByExtension に渡すことのできる拡張子のリストを返します。 - List<String> getMimeTypes()
- ScriptEngineManager#getEngineByMimeType に渡すことのできる MIME タイプのリストを返します。 - List<String> getNames()
- ScriptEngineManager#getEngineByName に渡すことのできるショート・ネームを返します。
以下にスクリプトの実行エンジンの情報を一覧するためのプログラムを示します:
import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; public class ScriptEngineInfoMain { public static void main(String[] arguments) { ScriptEngineManager manager = new ScriptEngineManager(); for(ScriptEngineFactory factory : manager.getEngineFactories()) { printEngineInfo(factory); System.out.println(); } } private static void printEngineInfo(ScriptEngineFactory factory) { System.out.println("Engine name: " + factory.getEngineName()); System.out.println("Engine version: " + factory.getEngineVersion()); System.out.println("Language name: " + factory.getLanguageName()); System.out.println("Language version: " + factory.getLanguageVersion()); for(String extension : factory.getExtensions()) { System.out.println("Extension: " + extension); } for(String mimeType : factory.getMimeTypes()) { System.out.println("MimeType: " + mimeType); } for(String name : factory.getNames()) { System.out.println("Short name: " + name); } } }
出力例:
Engine name: Mozilla Rhino Engine version: 1.6 release 2 Language name: ECMAScript Language version: 1.6 Extension: js MimeType: application/javascript MimeType: application/ecmascript MimeType: text/javascript MimeType: text/ecmascript Short name: js Short name: rhino Short name: JavaScript Short name: javascript Short name: ECMAScript Short name: ecmascript
5.2.2. ScriptContext と Bindings
全てのスクリプトは必ず javax.script.ScriptContext インタフェースで表される実行コンテキストに関連付けられて実行されます。ScriptContext には、入出力先の Reader や Writer、Java プログラムとスクリプト間のオブジェクトのバインディングなどの、スクリプトの実行に関する情報が関連付けられています。
Java プログラムとスクリプト間のオブジェクトのバインディングは javax.script.Bindings インタフェースによって表されます。前節の「Java オブジェクトをスクリプトに渡す」では「Java オブジェクトをスクリプトに渡すには ScriptEngine#put(String key, Object value) を使用する」と説明しましたが、その背後では ScriptEngine に関連付けられた ScriptContext の持つ Bindings にオブジェクトを関連付ける、という処理が行われます。
ScriptContext と Bindings のスコープ
ScriptContext では、Bindings のスコープとしてグローバル・スコープ及びエンジン・スコープが定義されています。グローバル・スコープの Bindings に関連付けられたオブジェクトは、同じ ScriptEngineFactory により生成された全ての ScriptEngine で共有されます。エンジン・スコープの Bindings に関連付けられたオブジェクトは、それを利用する ScriptEngine だけで有効となります。
これらのスコープは int 型の定数として定義されています:
- ScriptContext.GLOBAL_SCOPE
- ScriptContext.ENGINE_SCOPE
特定のスコープの Bindings の取得/設定は getBindings/setBindings メソッドによって行います:
- Bindings getBindings(int scope)
- 指定したスコープの Bindings を取得します。 - void setBindings(Bindings bindings, int scope)
- 指定したスコープの Bindings を設定します。
ScriptContext で定義されているスコープはグローバル・スコープとエンジン・スコープの二つですが、ScriptContext の実装クラスは、これ以外のスコープをサポートすることもできます。サポートされているスコープは ScriptContext#getScopes メソッドで得ることができます。
Bindings によるオブジェクトの関連付け
Bindings は java.util.Map<String, Object> を継承したインタフェースです。文字列をキーとしてオブジェクトの関連付けを行います。キーに指定した文字列はスクリプト上でアクセスするときの変数名となります。
ScriptContext のバインディング関係のメソッド
ScriptContext にはオブジェクトのバインディングを行うメソッドが含まれています。オブジェクトのバインディングを行う場合は、getBindings で取得した Bindings の get/put メソッドを利用するよりも、これらのメソッドを利用する方が簡単です。以下にオブジェクトのバインディングに関するメソッドを示します:
- Object getAttribute(String name)
- 最も狭いスコープから順番に検索し、最初に見つかったオブジェクトを返します。見つからなかった場合は null を返します。 - Object getAttribute(String name, int scope)
- 指定したスコープからオブジェクトを検索します。見つからない場合は null を返します。 - int getAttributeScope(String name)
- 指定したオブジェクトが関連付けられているスコープを返します。指定された名前のオブジェクトが見つからない場合は -1 を返します。 - Object removeAttribute(String name, int scope)
- 指定したスコープのオブジェクトを削除し、そのオブジェクトを返します。 - void setAttribute(String name, Object value, int scope)
- 指定したスコープにオブジェクトを登録します。
ScriptEngine のバインディング関係のメソッド
ScriptContext 同様、ScriptEngine にもオブジェクトのバインディングに関するメソッドとして get/put が存在します。get/put は関連付けられている ScriptContext のエンジン・スコープの Bindings に対してオブジェクトの取得/設定を行います:
- Object get(String key)
- 関連付けられている ScriptContext のエンジン・スコープの Bindings からオブジェクトを取得します。 - void put(String key, Object value)
- 関連付けられている ScriptContext のエンジン・スコープの Bindings のオブジェクトを設定します。
5.3. ScriptEngine のその他の機能
ScriptEngine の持つ他の機能として、スクリプトのコンパイルや生成があります。
5.3.1. スクリプトのコンパイル
ScriptEngine の実装によっては、スクリプトのコンパイルを行うことができます。コンパイル機能を持つ ScriptEngine は javax.script.Compilable インタフェースを実装しています。ScriptEngine が Compilable を実装しているかどうかは、以下のように判定することができます:
ScriptEngine scriptEngine; ... if(scriptEngine instanceof Compilable) { ... scriptEngine は Compilable を実装している }
Mustang に標準搭載されている JavaScript エンジン Mozilla Rhino による実装は Compilable に対応しています。Compilable インタフェースにはスクリプトをコンパイルするメソッド compile が定義さています:
- CompiledScript compile(Reader reader)
- 指定された Reader から読み込んだスクリプトのコンパイルを行います。 - CompiledScript compile(String script)
- 文字列で指定されたスクリプトのコンパイルを行います。
どちらも戻り値としてコンパイル済みスクリプトを表すインタフェース javax.script.CompiledScript を返します。CompiledScript にはスクリプトを実行するためのメソッド eval が定義されています:
- Object eval()
- コンパイル済みスクリプトを実行します。 - Object eval(Bindings bindings)
- 指定された Bindings をエンジン・スコープの Bindings としてコンパイル済みスクリプトを実行します。 - Object eval(ScriptContext context)
- 指定された ScriptContext でコンパイル済みスクリプトを実行します。
コンパイルした場合としない場合の実行速度の比較
実際にスクリプトをコンパイルした場合とそうでない場合でどの程度、実行速度に違いが見られるのか比較してみました。比較用のスクリプトは以下のものを使用しました:
String script = "function add(a, b) { return a + b; }";
この JavaScript コードをコンパイルした場合としなかった場合で繰り返し回数を変えて比較しました(比較用の Java コードはこちら)。結果は以下のようになりました:
繰り返し回数 | コンパイルしなかった場合 | コンパイルした場合 |
---|---|---|
1024 回 | 546 (ms) | 110 (ms) |
2048 回 | 703 (ms) | 172 (ms) |
4096 回 | 922 (ms) | 356 (ms) |
8192 回 | 1328 (ms) | 719 (ms) |
結果は環境はコンパイルするスクリプトによりますが、同じスクリプトを何度も実行することが分かっている場合には、事前にコンパイルしておくことを検討しましょう。
5.3.2. スクリプトの生成
ScriptEngineFactory は ScriptEngine を作成する役割を持ったインタフェースですが、それだけではなく、実装しているスクリプト言語の文法に沿ったスクリプトを生成する機能も含まれています。
現状では以下に示す三つのメソッドだけが含まれています:
- String getOutputStatement(String toDisplay)
- 指定された文字列を表示するためのステートメントを返します。 - String getMethodCallSyntax(String objectName, String methodName, String... args)
- 関数呼び出しのためのステートメントを返します。 - String getProgram(String... statements)
- 指定されたステートメントを順番に実行するスクリプトを返します。
使い方は getOutputStatement 及び getMethodCallSyntax メソッドを用いてステートメント(スクリプトの断片)を生成し、それを getProgram に渡すことで一つのスクリプトを作成します。
サンプル・コード:
import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; public class ScriptingGenerationMain { public static void main(String[] arguments) throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); ScriptEngineFactory factory = engine.getFactory(); String[] statements = { factory.getOutputStatement("hello"), factory.getMethodCallSyntax("target", "function", "param1", "param2"), }; String program = factory.getProgram(statements); System.out.println(program); } }
出力結果:
print("hello");target.function(param1,param2);
5.4. jrunscript
最後になりますが、Mustang ではスクリプト機能を手軽に試すことができるコマンドライン・ツール jrunscript が導入されました。
jrunscript には、入力されたスクリプトを逐次実行するインタラクティブ・モード、指定されたファイルを実行するバッチ・モード、そして引数で渡したスクリプトを実行するモードがあります。
インタラクティブ・モード
引数を与えずに jrunscript を起動すると、インタラクティブ・モードとして動作します:
jrunscript
バッチ・モード
-f オプションでファイル名を与えると、バッチ・モードとして指定されたファイルを実行します:
jrunscript -f hello.js
引数で指定されたスクリプトを実行
-e オプションを使用すると、実行するスクリプトをコマンドライン引数で与えることができます:
jrunscript -e "print('hello')"
まとめ
Mustang で新しく追加されたスクリプティング機能について一通り解説を行ってきましたが、いかがだったでしょうか。「ついにここまで来たか」と思いつつも「何に使えるのか」を見極めるのは難しいところです。汎用のスクリプティング・フレームワークとして、設定ファイルの記述、マクロ機能の実現、Web コンテンツの生成など、色々なユース・ケースが想定されていますが、決定的な「使いどころ」はスクリプティング機能が広く使われるようになるにつれて徐々に見出されていくのではないでしょうか。特に Web コンテンツの生成については、JSR 223 の制定過程で取り上げられるなど、スクリプティング機能の「使いどころ」になり得る分野かもしれません(最終的に仕様から除外されてしまいましたが)。
さて次回は、Mustang の管理、診断機能について解説を行います。