nashorn のグローバルスコープ

こんにちは、開発担当の Masa です。

最近 Java8 の ScriptEngine(nashorn) を調査していますが、グローバルスコープ
関連でつまずいたので現象と回避方法を紹介します。

以下はグローバルスコープのオブジェクトを参照したり、Java オブジェクトを生成する
サンプルです。
※本記事のサンプルは Java SE 8u51 で実行しました。

まずはスクリプトからアクセスする Java クラスを適当に作成します。

package jp.co.hos.sample2;

public class SampleMain2 { }

 
次にスクリプトを実行する側を実装します。
ScriptManager の Bindings を使ってグローバルスコープにオブジェクトを登録して
います。

package jp.co.hos.sample1;

import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Sample1 {
    public static void main(String[] args) {
        ScriptEngineManager manager = new ScriptEngineManager();
        try {
            // グローバルスコープにオブジェクトを登録します
            manager.getBindings().put("global", "global scope");
            
            ScriptEngine engine1 = manager.getEngineByName("nashorn");
            // グローバルスコープのオブジェクトが参照できるか確認します
            engine1.eval("print('engine1 output:' + global);");
            
            ScriptEngine engine2 = manager.getEngineByName("nashorn");
            // Java のクラスを参照できるか確認します
            engine2.eval("load(\"nashorn:mozilla_compat.js\");");
            engine2.eval("importPackage(Packages.jp.co.hos.sample2);");
            engine2.eval("var sample2 = new SampleMain2();");
            // グローバルスコープのオブジェクトが参照できるか確認します
            engine2.eval("print('engine2 output:' + global);");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 
実行結果:

engine1 output:global scope
engine2 output:global scope

正常に動作します。
ScriptManager  の Bindings に直接オブジェクトを登録している部分を、engine1 で
定義したオブジェクトを Bindings ごしに登録するように修正します。
ScriptManager の Bindings に engine1 の Bindings(エンジンスコープ)を設定
しています。

        ScriptEngineManager manager = new ScriptEngineManager();
        try {
            ScriptEngine engine1 = manager.getEngineByName("nashorn");
            // グローバルスコープにオブジェクトを登録します
            engine1.eval("var global = 'global scope';");
            manager.setBindings(engine1.getBindings(ScriptContext.ENGINE_SCOPE));
            // グローバルスコープのオブジェクトが参照できるか確認します

 
実行結果:

engine1 output:global scope
engine2 output:global scope

これも正常に動作します。
そこで、次の一行を追加します。

            ScriptEngine engine1 = manager.getEngineByName("nashorn");
            engine1.eval("load(\"nashorn:mozilla_compat.js\");");

 
実行結果:

javax.script.ScriptException: ReferenceError: "SampleMain2" is not defined in at line number 1

engine1 で互換性モジュールを読み込んだだけでエラーになりました。
エラーの発生箇所は engine2 で SampleMain2 のインスタンス生成スクリプトを評価
しているところです。

load で評価した mozilla_compat.js の内容が衝突してる??いやいや、そんなはずは...
とりあえず、現象とエラー内容からグローバルスコープが怪しいのはわかるので対策を。

 
load の変わりに loadWithNewGlobal を使用することで新しいグローバル・オブジェクトを
使用して mozilla_compat.js が評価されます。
※これはあくまで例です。この方法では engine1 で importPackage を評価することが
できません。

            ScriptEngine engine1 = manager.getEngineByName("nashorn");
            engine1.eval("loadWithNewGlobal(\"nashorn:mozilla_compat.js\");");
            // グローバルスコープにオブジェクトを登録します
            engine1.eval("var global = 'global scope';");
            manager.setBindings(engine1.getBindings(ScriptContext.ENGINE_SCOPE));

 
または、以下のように ScriptManager の Bindings に engine1 の Bindings 内のマッピング
を全て追加します。

            ScriptEngine engine1 = manager.getEngineByName("nashorn");
            engine1.eval("load(\"nashorn:mozilla_compat.js\");");
            // グローバルスコープにオブジェクトを登録します
            engine1.eval("var global = 'global scope';");
            manager.getBindings().putAll(engine1.getBindings(ScriptContext.ENGINE_SCOPE));

 
実行結果:

engine1 output:global scope
engine2 output:global scope

どちらも正常に動作しました。

尚、mozilla_compat.js と importPackage は推奨されていません。