じゃじゃ馬ならし

 

言語の中の言語 - Scripting

はじめに

2004 年の JavaOne で、Java でスクリプトをサポートするという発表がありました。特にサーバサイドでの使用を前提にしてたようで、PHP がまず第一にサポートされるスクリプト言語ということになっていました。

もちろん、標準策定は JCP でおこなわれています。具体的には JSR-223 Scripting for Java Platform で標準策定されています。

そして、その機能が Java SE 6 で取り入れられることになったのです。

しかし、PHP ではありませんでした。Java SE 6 で標準でサポートされるのは JavaScript です。

JSR-223 は Java の中でスクリプト言語を使用するための標準で、スクリプト言語の種類を限定しているわけではありません。ドラフトの中で例として使われているものには PHP, JavaScript, Kawa Scheme があります。

同じようなフレームワークとして Apache の Bean Script Framework があります。

どうやら、PHP はサーバサイドということで、Java EE にまわされたようです。Java WSDP のオプションパッケージで Scripting for the Java Platform Cobundle というものがあったのですが。なぜか、Java WSDP のページからはこのオプションへのリンク先が消えてしまっています。

とりあえず、JavaScript から使ってみましょう。

Java SE 6 の JavaScript Engine は Mozilla の Rhino が使用されています。

なお、ここで使用している JDK は 2005.10.6 に公開された build 55 です。ですので、その後のバージョンによって動作しないことも考えられますが、ご了承ください。

 

JavaScript をコマンドから使ってみる

Java SE 6 には JavaScript を使ってみるのにちょうどいいコマンド jrunscript が標準で提供されています。これを使って、JavaScript を実行してみます。

はじめは使い方を調べるためにヘルプを見てみましょう。

C:\temp>jrunscript -help
Usage: jrunscript [options] [arguments...]

where [options] include:
  -classpath <path>    Specify where to find user class files
  -cp <path>           Specify where to find user class files
  -D<name>=<value>     Set a system property
  -J<flag>             Pass <flag> directly to the runtime system
  -l <language>        Use specified scripting language
  -e <script>          Evaluate given script
  -encoding <encoding> Specify character encoding used by script files
  -f <script file>     Evaluate given script file
  -f -                 Interactive mode, read script from standard input
                       If this is used, this should be the last -f option
  -help                Print this usage message and exit
  -?                   Print this usage message and exit
  -q                   List all scripting engines available and exit

If [arguments..] are present and if no -e or -f option is used, then first
argument is script file and the rest of the arguments, if any, are passed
as script arguments. If [arguments..] and -e or -f option is used, then all
[arguments..] are passed as script arguments. If [arguments..], -e, -f are
missing, then interactive mode is used.
C:\temp>

ここには書いてありませんが、-l を指定しないとデフォルトで JavaScript が使えるようになります。また、インタラクティブモードは -f - を使用すると記述されていますが、-f もしくは -e を使用しなければインタラクティブモードになるようです。

-q を使用すると、使用できる言語の一覧が表示できるようなので、やってみます。

C:\temp>jrunscript -q
Language ECMAScript 0 implemention "Mozilla Rhino" @IMPLEMENTATION.VERSION@
C:\temp>

JavaScript が使えることは分かりますが、まだバグがあるようですね ^^;; それに -l の後に記述できる文字列を書いてくれないのはちょっと問題。

実際に -l ECMAScript と指定すると、そんな言語使えないといわれてしまいます。

C:\temp>jrunscript -l ECMAScript
 
script engine for language ECMAScript can not be found
 
C:\temp>

これはちょっといただけないですね。ちゃんと -l js と記述できるような情報を表示しないと。

やっと、JavaScript を使えるところまでいきました。一番はじめは... やっぱり Hello, World! でしょうか。

jrunscript とだけ入力すると、プロンプトが表示されます

C:\temp>jrunscript
js>

これでインタラクティブモードが開始されます。ここで、print メソッドを使用して、Hello, World! を出力してみます。

C:\temp>jrunscript
js> print('Hello, World!');
Hello, World!
js>

あっさりと動いてしまいました。

-e を使用する場合には、ダブルクオンテーションで囲む必要があるらしいです。

C:\temp>jrunscript -e "print('Hello, World!');"
Hello, World!
C:\temp>

何も囲まなかったり、シングルクオンテーションで囲むと例外が発生します。

C:\temp>jrunscript -e print('Hello, World!');
script error: sun.org.mozilla.javascript.internal.EvaluatorException: unterminat
ed string literal (#1) in  at line number 1
C:\temp>

また、スクリプトの中でダブルクオンテーションを使用する場合は、\ でエスケープして \" と表記しなくてはいけないようです。

インタラクティブモードもしくは -e でスクリプトを実行する場合、for 文とかメソッドなどの節は 1 行に書かなければいけないようです。完全にインタープリタモードなのでこれはしかたないかもしれません。

実際に、インタラクティブモードで使うことはほとんどなくて、-f でスクリプトファイルを指定することがほとんどだと思うので、これはこれでいいと思います。-f でスクリプトファイルを指定する場合は、テキストファイルで JavaScript のコードを記述するだけです。

 

jrunscript を解剖する

Java SE 6 と今までの Java SE との大きな違いはソースコードが、公開初期の段階から提供されていることです。

今までは、FCS が出た後にすべてのソースコードが公開されていましたが、Java SE 6 は https://Java SE 6.dev.java.net/ で毎週バイナリと一緒にソースコードが提供されています。

すべてのソースコードが提供されているということは、jrunscript のコードもあるということです。

jrunscript の実体は com.sun.tools.script.shell.Main クラスです。なぜ、こんなことが分かるかというと、ソースがインストールされたディレクトリで grep (Windows ならエクスプローラの検索) で jrunscript を探してみたからです。すると j2se/make/sun/jrunscript/Makefile が見つかります。 これをオープンしてみると、PACKAGE には com.sun.tools.script..shell と記述されていますし、JAVA_ARGS には com.sun.tools.script.shell.Main と記述されているのです。

それでは、com.sun.tools.script.shell.Main クラスのソースを見ていきましょう。ここでの使い方を見れば、Java からどのようにスクリプト言語を扱うか理解できると思います。

まず、main メソッドはこうなっています。

    public static void main(String[] args) {
        // parse command line options
        String[] scriptArgs = processOptions(args);
        
        // process each script command
        for (Command cmd : scripts) {
            cmd.run(scriptArgs);
        }
        
        System.exit(EXIT_SUCCESS);
    }

processOptions メソッドで jrunscript の引数をチェックして、そのあと for 文でコマンドを実行するという感じでしょうか。

というわけで processOptions メソッドですが、このメソッドは長いのですべて見てもしかたありません。重要なところを抜き出してみましょう。まず、processOptions メソッドからコールされる checkClassPath メソッドの一番最後に記述されている部分です。

        engineManager = new ScriptEngineManager();

javax.script.ScriptEngineManager クラスはどうやらスクリプトエンジンのファクトリクラスのようです。ScriptEngineManager オブジェクトである endingeManager を探してみると、listScriptEngines メソッドというのがありました。

どうやら jrunscript の -q オプションを指定したときにコールされるメソッドのようです。

    private static void listScriptEngines() {
        List factories = engineManager.getEngineFactories();
        for (ScriptEngineFactory factory: factories) {
            getError().println(getMessage("engine.info",
                    new Object[] { factory.getLanguageName(),
                            factory.getLanguageVersion(),
                            factory.getEngineName(),
                            factory.getEngineVersion()
            }));
        }
        System.exit(EXIT_SUCCESS);
    }

これを見れば分かるように、ScritEngineManager#getEngineFactories メソッドでスクリプトエンジンをすべて取得できるようです。

指定されたスクリプトエンジンを生成するのは getScriptEngine メソッドです。

    private static ScriptEngine getScriptEngine(String lang) {
        ScriptEngine se = engines.get(lang);
        if (se == null) {
            se = engineManager.getEngineByName(lang);
            if (se == null) {
                getError().println(getMessage("engine.not.found",
                        new Object[] { lang }));
                        System.exit(EXIT_ENGINE_NOT_FOUND);
            }
            
            // initialize the engine
            initScriptEngine(se);
            // to avoid re-initialization of engine, store it in a map
            engines.put(lang, se);
        }
        return se;
    }

単に ScriptEngineManager#getEngineByName メソッドで取得できるようです。引数は自明だと思いますが、言語の名前です。それ以外にも拡張子、MIME タイプで指定する getEngineByExtension/getEngineByMimeType というメソッドがあります。

スクリプトエンジンが取得できたら、スクリプトを実行します。実行は evaluateString メソッドなどでおこなわれています。

    private static Object evaluateString(ScriptEngine se,
            String script, boolean exitOnError) {
        try {
            return se.eval(script);
        } catch (ScriptException sexp) {
            getError().println(getMessage("string.script.error",
                    new Object[] { sexp.getMessage() }));
                    if (exitOnError)
                        System.exit(EXIT_SCRIPT_ERROR);
        } catch (Exception exp) {
            exp.printStackTrace(getError());
            if (exitOnError)
                System.exit(EXIT_SCRIPT_ERROR);
        }
        
        return null;
    }

例外処理以外では実質 1 行だけです。ScriptEngine#eval メソッドをコールしています。ScriptEngine クラスの Javadoc を見ると、6 種類の eval メソッドがオーバロードされています。基本的には引数が文字列か Reader オブジェクトかの違いのようです。

ここでいったんまとめてみましょう。

Java の中からスクリプトを使用するには

  1. ScriptEngineManager オブジェクトを生成する
  2. ScriptEngineManager#getEngineByName/getEngineByExtension/getEngineByMime メソッドを使用して ScriptEngine オブジェクトを取得する
  3. ScriptEngine#eval でスクリプトを実行する

という手順を踏めばいいということが分かりました。

ということで、次から実際にサンプルを作ってスクリプトを操ってみましょう。

 

スクリプトをプログラムから実行する

ここでもはじめは "Hello, World!" にしましょう。

サンプルのクラス名は ScriptSample1 です。

サンプルのソースコード ScriptSample1.java

 

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
 
public class ScriptSample1 {
    public ScriptSample1() {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        
        String script = "print('Hello, World!');";
 
        if (engine != null) {
            try {
                engine.eval(script);
            } catch (ScriptException ex) {
                ex.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) {
        new ScriptSample1();
    }
}

前節の手順をそのままコードにしただけなので、説明はいらないでしょう。

これを実行してみると、次のようになりました。

C:\temp>java ScriptSample1
Hello, World!
 
C:\temp>

うまく実行できたので、気をよくしてこんなコードを書いてみました。

サンプルのソースコード ScriptSample2.java

 

        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        
        String[] scripts = {"function printHelloWorld() {",
                            "    var text = 'Hello, World!';",
                            "    for (var i = 0; i < 10; i++) {",
                            "        print(text);",
                            "    }",
                            "}",
                            "printHelloWorld();"};
 
        if (engine != null) {
            try {
                for (String script: scripts) {
                    engine.eval(script);
                }
            } catch (ScriptException ex) {
                ex.printStackTrace();
            }
        }

単に "Hello, World!" を 10 回表示するだけのコードです。

しかしこれを実行すると...

C:\temp>java ScriptSample2
javax.script.ScriptException: sun.org.mozilla.javascript.internal.EvaluatorExcep
tion: missing } after function body (#1) in  at
line number 1
        at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.ja
va:93)
        at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.ja
va:108)
        at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:241)

        at ScriptSample2.(ScriptSample2.java:21)
        at ScriptSample2.main(ScriptSample2.java:30)
 
C:\temp>

やっぱり、節は 1 行で扱わないとダメなようです。

        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        
        String[] scripts = {"function printHelloWorld() {"
                            + "    var text = 'Hello, World!';"
                            + "    for (var i = 0; i < 10; i++) {"
                            + "        print(text);"
                            + "    }"
                            + "}",
                            "printHelloWorld();"};
 
        if (engine != null) {
            try {
                for (String script: scripts) {
                    engine.eval(script);
                }
            } catch (ScriptException ex) {
                ex.printStackTrace();
            }
        }

これで大丈夫でしょう。実行すると、うまく表示されました。

C:\temp>java ScriptSample2
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
 
C:\temp>

次はファイルからスクリプトを読みこんで見ます。

サンプルのソースコード ScriptSample3.java

 

    public ScriptSample3(String filename)
            throws FileNotFoundException, IOException {
        FileReader reader = new FileReader(filename);
 
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
 
        if (engine != null) {
            try {
                engine.eval(reader);
            } catch (ScriptException ex) {
                ex.printStackTrace();
            }
        }
 
        reader.close();
    }

サンプルに作った JavaScript ファイル sample.js は

function printHelloWorld() {
    var text = 'Hello, World!';
    for (var i = 0; i < 10; i++) {
        print(text);
    }
}
  
printHelloWorld();

実行した結果は先ほどと同じです。

C:\temp>java ScriptSample3 sample.js
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
 
C:\temp>

 

 

スクリプトから Java にアクセスする

今までは単にスクリプトを Java から実行するだけでしたが、それだけだとちょっとと思いませんか。やはり、スクリプトの中から Java のクラスやオブジェクトにアクセスできないと。

これに関しては Javadoc をにらんでいてもしかたないので、JSR-223 Scripting for Java Platform のドラフトを読んでみました。

ドラフトは 160 ページ以上もあるのです。読むのイヤだなぁと思っていたら、半分は Javadoc でした ^^;;

で、スクリプトの中から Java にアクセスする方法です。

これはスクリプト言語依存の話なのですが、ドラフトには PHP と JavaScript と Kawa Scheme の例がのっています。

さきほどのサンプル ScriptSample3 を使用して次のスクリプト date.js を実行してみます。

importPackage(java.util);
 
var date = new Date();
print(date);
print('Milliseconds: ' + date.getTime());
 
importPackage(java.lang);
 
print('Current Milliseconds: ' + System.currentTimeMillis());

ほとんど普通の JavaScript と同じように使えます。ただし、クラスを使う前には importPackage メソッドをコールする必要があるようです。

オブジェクトの生成には new を使用し、メソッドは . で続けて記述します。

クラスメソッドも特に違和感はないですね。ただし、Java のプログラムと違い java.lang パッケージといえども、importPackage が必要なようです。

さて、実行してみましょう。

C:\temp>java ScriptSample3 date.js
Fri Oct 14 2005 04:53:34 GMT+0900 (JST)
Milliseconds: 1129244014953
Current Milliseconds: 1129244015015
 
C:\temp>

特に問題もなく、実行することができました。

しかし、importPackage メソッドを使ってしまうと、java.awt.List と java.util.List のようにバッテイングしてしまう名前が使えないような気がしますよね。

ドラフトには importPackage メソッドを使用した例しか記述されていませんでしたが、ちょっと試してみましょう。

先ほどの date,js を次のように変更して実行してみます。

var date = new java.util.Date();
print(date);
print('Milliseconds: ' + date.getTime());
  
print('Current Milliseconds: ' + java.lang.System.currentTimeMillis());

 

C:\temp>java ScriptSample3 date2.js
Fri Oct 14 05:10:21 JST 2005
Milliseconds: 1129245021765
Current Milliseconds: 1129245021937
  
C:\temp>

importPackage をしないで、直接パッケージもこみでクラスを指定しても実行することができました。

ここで気をつけなくてはいけないことがあります。JavaScript で Java のメソッドをコールする場合、メソッドの引数が自動的に Java の型に変更されます。しかし、JavaScript は動的型づけ言語なので Java の型と一対一に対応しないということがあるということです。

基本的には文字列の方が数値より先にあてはめられるようです。

つまり foo(String s) と foo(int x) を JavaScript で x.foo(100) と記述したときには x.foo("100") がコールされるということです。

こういうこまかなところはやってみないと分からないところも多いですね。

 

GUI も操作してみる

せっかくなので、GUI もやってみましょうか。次のようなスクリプト frame.js を実行してみました。

importPackage(javax.swing);
 
var frame = new JFrame("Sample");
frame.setVisible(true);

 

C:\temp>java ScriptSample3 frame.js
javax.script.ScriptException: sun.org.mozilla.javascript.internal.EcmaError: Ref
erenceError: "javax" is not defined. (#1) in  at
 line number 1
        at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.ja
va:93)
        at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:227)

        at ScriptSample3.<init>(ScriptSample3.java:17)
        at ScriptSample3.main(ScriptSample3.java:28)
  
C:\temp>

ありゃ、だめなようです。javax というパッケージはないといっています。これ以外にも調べてみたのですが、javax や org ではじまるパッケージは全滅でした。

Rhino のドキュメントを読んでみると、importPackage を使わずに、代わりに Packages とつければ大丈夫のようです。

importPackage(java.awt);
 
var frame = new Packages.javax.swing.JFrame("Sample");
frame.setLayout(new FlowLayout());
 
var button = new Packages.javax.swing.JButton("OK");
frame.add(button);
 
frame.setSize(100, 100);
frame.setVisible(true);

たいしたことはやっていないのですが、フレームを作ってそこにボタンを貼っているだけです。

これを実行すると、ちゃんとフレームが表示されました。でも、ボタンをクリックしてもクローズボタンをクリックしても何もおきません。

せっかくなので、イベント処理を加えてみましょう。

JavaScript なので、動的にメソッドが定義できるのですが、Java のオブジェクトに対してもそれがおこなえるかどうかです。多分、無理だとは思うのですが...

importPackage(java.awt);
 
var frame = new Frame("Sample");
frame.setLayout(new FlowLayout());
 
var button = new Button("OK");
button.processMouseEvent = function(event) {
    java.lang.System.exit(0);
}
 
frame.add(button);
 
frame.setSize(100, 100);
frame.setVisible(true);

これを実行してみると...

C:\temp>java ScriptSample3 frame3.js
javax.script.ScriptException: sun.org.mozilla.javascript.internal.EvaluatorExcep
tion: Java class "java.awt.Button" has no public instance field or method named
"processMouseEvent". (#7) in  at line number 7
        at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.ja
va:93)
        at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:227)

        at ScriptSample3.<init>(ScriptSample3.java:17)
        at ScriptSample3.main(ScriptSample3.java:28)
		   
C:\temp>

一瞬ダメかと思ったのですが、エラーメッセージをよく読むと public なメソッドがないと書いてあるではないですか。試しに public なメソッドでもやってみたのですが、どうもこの方法はダメなようです。

Rhino のドキュメントを見てみると、どうやらインタフェースをインプリメントした無名クラスはスクリプト内で作れるようです。

たしかに processMouseEvent メソッドは protected なので、public なメソッドで試してみましょう。一番簡単な例で toString メソッドで試してみます。

importPackage(java.awt);

var frame = new Packages.javax.swing.JFrame("Sample");
frame.setLayout(new FlowLayout());
 
var button = new Packages.javax.swing.JButton("OK");
 
obj = {actionPerformed: function(event) {
    print("ActionPerfomed: " + event);
}}
 
listener = new java.awt.event.ActionListener(obj);
button.addActionListener(listener);
 
frame.add(button);
 
frame.setSize(100, 100);
frame.setVisible(true);

書き方はちょっと面倒ですが、できれば御の字です。実行して、ボタンをクリックしてみると、

C:\temp>java ScriptSample3 frame4.js
ActionPerfomed: java.awt.event.ActionEvent[ACTION_PERFORMED,cmd=OK,when=11296798
29624,modifiers=Button1] on javax.swing.JButton[,32,5,51x26,alignmentX=0.0,align
mentY=0.5,border=javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@1a8c
4e7,flags=296,maximumSize=,minimumSize=,preferredSize=,defaultIcon=,disabledIcon
=,disabledSelectedIcon=,margin=javax.swing.plaf.InsetsUIResource[top=2,left=14,b
ottom=2,right=14],paintBorder=true,paintFocus=true,pressedIcon=,rolloverEnabled=
true,rolloverIcon=,rolloverSelectedIcon=,selectedIcon=,text=OK,defaultCapable=tr
ue]
  
C:\temp>

おぉ、できました。

 

すでに存在しているオブジェクトにアクセスする

先ほどまでは、スクリプトの中でオブジェクトを生成して、そのオブジェクトに対してメソッドコールをしていました。

でも、スクリプトを実行するときには、すでに Java 側で存在しているオブジェクトがいろいろあるはずです。これらのオブジェクトをスクリプトからアクセスすることはできないのでしょうか。

さっそく、やってみましょう。

オブジェクトにアクセスするためには、スクリプトの中の変数とオブジェクトの対応表を作らなければいけないようです。このために使われるのが Bindings インタフェースです。

ドラフトでは Bindings インタフェースは Namespace インタフェースとなっていますが、変更されたようです。

Bindings インタフェースは Map<String, Object> の派生インタフェースになっているので、使い方は Map とほぼ同じです。

サンプルのソースコード ScriptSample4.java

 

    public ScriptSample4() {
        String script = "sample.foo('bar');";
        StringReader reader = new StringReader(script);
 
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
 
        if (engine != null) {
            Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
            bindings.put("sample", this);
            
            try {
                engine.eval(reader);
            } catch (ScriptException ex) {
                ex.printStackTrace();
            }
        }

        reader.close();
    }
 
    public void foo(String msg) {
        System.out.println("Foo: " + msg);
    }

Bindings オブジェクトを新たに生成することもできるのですが (ScriptEngine#createBindings を使用)、ここではすでに ScriptEngine オブジェクトが保持している Bindings オブジェクトに追加していきます。

ScriptEngine#getBindings メソッドの引数は Bindings のスコープを表しています。ScriptContext.ENGINE_SCOPE がエンジンごとに保持するスコープで、ScriptContext.GLOBAL_SCOPE が ScriptEngineManager が保持するスコープになります。

この例では ENGINE_SCOPE にして、sample に対応するのが SimpleScript4 オブジェクトを対応させています。

実際のスクリプトは script 変数で示してありますが、sample に対して foo メソッドをコールするだけです。

うまくいけば SimpleScript4#foo がコールされるはずです。やってみましょう。

C:\temp>java ScriptSample4
Foo: bar
 
C:\temp>

この機能を使用すれば、任意のオブジェクトをスクリプトからアクセスすることができそうです。

 

コンパイルとインボーク

よく使用するのだけれども変更がほとんどないスクリプトってありますよね。

そういうスクリプトは事前にコンパイルしておいて、高速に呼び出すことが可能です。

ただし、このコンパイルの機能はスクリプトエンジンに依存しているので、できないものもあります。幸い、Rhino はできるようです。

コンパイルが可能なスクリプトエンジンは Compilable インタフェースをインプリメントしています。まず、Rhino でインプリメントしているかどうか確認してみましょう。

サンプルのソースコード ScriptSample5.java

 

    public ScriptSample5() {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
 
        if (engine != null) {
            Class cls = engine.getClass();
            System.out.println("Engine Class: " + cls);
            System.out.println("Interfaces:");
            for (Class ifClass : cls.getInterfaces()) {
                System.out.println(ifClass);
            }
        }
    }

実行してみると、次のようになりました。

C:\temp>java ScriptSample5
Engine Class: class com.sun.script.javascript.RhinoScriptEngine
Interfaces:
interface javax.script.Invocable
interface javax.script.Compilable
 
C:\temp>

確かに Compilable インタフェースをインプリメントしているようです。

さて、Compilable インタフェースの定義されているメソッドは compile です。引数は eval メソッドと同じように String オブジェクトもしくは Reader オブジェクトです。

異なるのは戻り値で、CompiledScript オブジェクトになります。CompiledScript クラスには eval メソッドが定義されているので、これでスクリプトが実行できます。

サンプルのソースコード ScriptSample6.java

 

    public ScriptSample6() {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
         
        String script = "function printHelloWorld() {"
                      + "    var text = 'Hello, World!';"
                      + "    for (var i = 0; i < 10; i++) {"
                      + "        print(text);"
                      + "    }"
                      + "}"
                      + "printHelloWorld();";
  
        if (engine != null) {
            try {
                Compilable compilable = (Compilable)engine;
                CompiledScript compiledScript = compilable.compile(script);
 
                compiledScript.eval();
            } catch (ScriptException ex) {
                ex.printStackTrace();
            }
        }
    }

実行結果は同じなので、のせませんが、ちゃんと実行できました。CompiledScript オブジェクトはなんども eval メソッドで実行可能なので、一度 CompiledScript オブジェクトを作っておけばいつでもどこでも実行できます。

さて、先ほど Rhino スクリプトエンジンがインプリメントしているインタフェースを調べましたが、Compilable インタフェース以外にもう 1 つインタフェースをインプリメントしていました。

さて、今まではスクリプトを単体で実行していましたが、Java からスクリプトのメソッドをコールしてみたくなりませんか。これをおこなうのが Invocable インタフェースです。

先ほど、Rhino がどのようなインタフェースをインプリメントしているか調べましたが、ちゃんと Invocable インタフェースがインプリメントされていることが分かります。

ということで、スクリプトのメソッドを Java からコールしてみましょう。

サンプルのソースコード ScriptSample7.java

 

    public ScriptSample7() {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");

        String script = "function printHelloWorld() {"
                      + "    var text = 'Hello, World!';"
                      + "    for (var i = 0; i < 10; i++) {"
                      + "        print(text);"
                      + "    }"
                      + "}";
        
        if (engine != null) {
            try {
                engine.eval(script);

                Invocable invocable = (Invocable)engine;
                invocable.invoke("printHelloWorld");

            } catch (NoSuchMethodException ex) {
                ex.printStackTrace();
            } catch (ScriptException ex) {
                ex.printStackTrace();
            }
        }
    }

使い方は一度スクリプトエンジンでスクリプトを評価 (つまり eval メソッドをコールすることです) しておいてから、Invoke#invoke メソッドでメソッドをコールするだけです。

invoke の引数はメソッド名と引数です。メソッドの引数は可変長引数になっているので、なければ省略できます。

これを実行すると、

C:\temp>java ScriptSample7
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
 
C:\temp>

となり、ちゃんとメソッドがコールされていることが分かります。

 

おわりに

今回は Java SE 6 に標準で添付されている Rhino JavaScript Engine を使っていろいろと遊んでみました。JSR-223 はたとえていうなら Script 向けの JAXP のようなものですね。

JSR-223 で策定されているインタフェースに則ってスクリプトエンジンを構築すれば、どのスクリプト言語でも Java から扱うことができます。

こんご、Groovy を筆頭にした Java で記述されたスクリプトエンジンは、JSR-223 に移行することが考えられます。また、Java で書かれていなくても PHP のように Java と連携を持てるものも増えてくるのではないでしょうか。

JSR-223 では Web での使用に関しても (というか、本来の目的はそっちだったわけですが)、仕様が策定されています。JCP を使わずに、スクリプトで Web コンテンツを記述することができます。

そして、バックサイドの Java のアプリケーションサーバとの連携も非常にやりやすくなるはずです。

そのほかにも、スクリプト言語を使って、テストや設定ファイルの記述、BPEL のようなコンポーネント間のグルーとして、またプロトタイプの開発などいろいろな用途がありそうです。

すべて、Java というのもいいのですけど、適材適所でスクリプト言語を使用していくのがこれからのスタイルなのでしょう。

 

参考:

 

(Nov. 2005)