Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

Rhino事始め - JavaとRhinoのコラボ(4)

今回が最後です。長かった。


ChefインタフェースをJavaScriptで実装します。JavaScriptは以下のようなイメージです。名前はchef1.jsとしました。

var output = java.lang.System.out;

var impl = {
	introduce : function () {
		output.println("JavaScriptが調理します。");
	}, 
	cut : function(food) {
		output.println(food + "をみじん切りにした。");
	}, 
	boil : function(food) {
		output.println(food + "をじっくりと煮込んだ。");
	}, 
	fry : function(food) {
		output.println(food + "を油でさっと炒めた。");
	}, 
	finish : function () {
		output.println("コショウをさっと振って仕上げた。");
	},
};

new Packages.jstest.Chef(impl); 


まず、インタフェースと同名のプロパティ(メソッド)を持つJavaScriptのオブジェクトを用意してます。そして、new インタフェース名(実装オブジェクト); の書式でChefインタフェースでラッピングしているイメージです。最後に評価された値がJavaに返るので、ここではnewの結果がJavaに返ることになります。


では、これをJavaで使えるように管理するJSChefManagerを実装します。Rhinoのエンジンのライフサイクルを意識したいので、init()、close()のメソッドを持たせ、開始時と終了時に必ずこれを呼ばせます。


また、今までの例では一つのJavaScriptを読むだけでしたが、今回はchefsディレクトリのスクリプトを全て読んでインスタンス化させるように作ります。こうすると、chefsディレクトリにあたらしいChef実装をテキストで書いて保存するだけで、新しいChefがフレームワークに参加できるようになります。*1

package jstest;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;

public class JSChefManager {
    static private final File JS_DIR = new File("chefs");
    private Context cx;
    private Scriptable global;
    private ArrayList<Chef> chefs = new ArrayList<Chef>();

    public void init() throws IOException{
        /* エンジンの初期化とグローバルスコープの準備 */
        cx     = Context.enter();
        global = cx.initStandardObjects();

        File[] files = JS_DIR.listFiles();
        if(files == null) return;
        for(File script : files){
            /* ローカルスコープを準備 */
            Scriptable scope = cx.newObject(global);
            scope.setParentScope(null);
            scope.setPrototype(global);

            /* スクリプトの実行(実装インスタンスの生成) */
            FileReader r = new FileReader(script);
            Object ret = cx.evaluateReader(scope, r, script.getAbsolutePath(), 1, null);
            chefs.add((Chef) Context.jsToJava(ret, Chef.class));
            r.close();
        }
    }

    public ArrayList<Chef> getChefs(){
        return chefs;
    }

    public void close(){
        Context.exit();
    }
}


close()とアクセサはほとんど何もしてないので、今回はinit()が目玉となります。一つ目のポイントは、Context#jsToJava()です。こいつを通してやらないと、evaluateReader()の戻り値をJavaから利用することはできません。


もう一つが、ローカルスコープを作成している部分です。今回は複数のJavaScriptを読み込むので、globalをスコープとして使用してしまうと、変数がぶつかってしまう可能性があります。そのため、globalオブジェクトから新たなオブジェクトを作っています。そして、globalとのスコープチェーンを切って変わりにglobalへプロトタイプチェーンをつないでいます。こうすることで、globalで定義されたプロパティが利用でき、かつ、varを省略して変数を宣言してもglobalに値を保存しないような状態のオブジェクトができます。これをそれぞれのスクリプトのスコープとしています。


では、次のようなmain()を作って実行してみましょう。JSChefManagerのinit()とclose()をきちんと呼ぶのを忘れずに・・・。

    public static void main(String[] args) throws Exception {
        JSChefManager   chefManager = new JSChefManager();
        try{
            chefManager.init();

            ArrayList<Chef> chefs = new ArrayList<Chef>();
            chefs.add(new JavaChef());
            chefs.addAll(chefManager.getChefs());

            Kitchen kitchen = new Kitchen(chefs);
            kitchen.run();
        }finally{
            chefManager.close();
        }
    }

[結果]
【カレー対決】
Javaさんの出番です。
じゃがいも、豚肉をカットした。
じゃがいも、豚肉を焼いた。
ルー、じゃがいも、豚肉をボイルした。
完成した。

JavaScriptが調理します。
じゃがいも、豚肉をみじん切りにした。
じゃがいも、豚肉を油でさっと炒めた。
ルー、じゃがいも、豚肉をじっくりと煮込んだ。
コショウをさっと振って仕上げた。

【ミートソース対決】
Javaさんの出番です。
パスタをボイルした。
たまねぎをカットした。
トマトソース、挽肉、たまねぎを焼いた。
完成した。

JavaScriptが調理します。
パスタをじっくりと煮込んだ。
たまねぎをみじん切りにした。
トマトソース、挽肉、たまねぎを油でさっと炒めた。
コショウをさっと振って仕上げた。


動きました! JavaScriptの実装もJavaの実装とまったく同様に動いていることにも注目してください*2。後はchefsディレクトリに同様のJavaScriptを追加するだけで、料理に参加するシェフが増えていきます。ためしにいろいろと作ってみてください。




と言うことで、長々と長期間に渡ってRhinoについていろいろ紹介してみました。ドキュメントは少なめですが、いじってみればみるほど奥が深くて面白いです。JavaScriptは異端な言語だとばかり思ってましたが、意外とまっとうな言語だったんですねえ。

*1:ただしセキュリティには注意。

*2:ただし、速度は若干異なる可能性があるので注意