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

Java の新しいLTS「Java 17」が正式にリリースされました。
かねてからアナウンスされていた通り、Nashorn スクリプトエンジンが削除されました。

Java17 上で JavaScript を実行したい場合はどうするの!?ということで色々試してみます。

こっそりScriptEngineが実行できたりとか、、、は無いですね。

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine1 = manager.getEngineByName("JavaScript");
System.out.println(engine1);

ScriptEngine engine2 = manager.getEngineByMimeType("application/ecmascript");
System.out.println(engine2);

結果:

null
null
調査方法

多分ここら辺が有名だろうということで「rhino」「nashorn」「GraalJS」を使ってみたいと思います。
(「V8」も対象の予定でしたが、「J2V8」「jav8」ともに更新が止まっている(J2V8 の Android 用は更新されている)ため断念しました)
 
調査項目は以下。Java 11 との互換性がメイン調査項目になります。

  1. importPackage が使えるか(Java7 で標準機能、Java8/11 でオプション)
  2. print が使えるか(Java7/8/11 で標準機能)
  3. load が使えるか(Java8/11 で標準機能)
  4. コンパイルされたスクリプトの実行
  5. Java クラスへのアクセス
  6. Java から受け取ったデータとクラスの使用
  7. グローバルスコープオブジェクトの共有

後ほど実行速度も計測します。
 
なお、調査用プログラムは Java17 で実行しますが、コンパイラー準拠レベルは 1.8 です。

ロードする JS ファイルを用意

JsFunctions.js

const joiner = function() {
    return Array.prototype.join.call(arguments, '-');
};
スクリプトから使用する Java クラスを用意

JavaFunctions.java

public class JavaFunctions {

    public static BigDecimal sum(Object[] args) {
        BigDecimal d = BigDecimal.ZERO;
        for (Object arg : args) {
            try {
                BigDecimal addend = new BigDecimal(String.valueOf(arg));
                d = d.add(addend);
            } catch (NumberFormatException ex) {
            }
        }
        return d;
    }
}
スクリプトに渡す Java クラスを用意

JavaClass.java

public class JavaClass {
    private String pre;
    private int count = 0;
    
    public JavaClass(String s) {
        pre = s;
    }
    
    public int getCount() {
        return count;
    }
    public void setCount(int count) {
        this.count = count;
    }

    public void println(Object arg) {
        System.out.println(pre + "です。count=" + count + ", " + String.valueOf(arg));
    }
}

Rhino を実行する

2021/10/20 時点で最新のリリースは 1.7.13。github から rhino-1.7.13.jar を取得して classpath に追加します。
 
このバージョンから、Java の ScriptEngine インタフェースに対応しています。ScriptEngine インタフェース越しに使用する場合は、更に rhino-engine.jar も取得して classpath に追加します。

Rhino を直接使う

Java8/11 と同様に、 Java7 で標準だった importPackage が通常の手順では使用出来ません。また、printload も通常の手順では使用出来ません。
使用可能にするには、Context.initStandardObjects ではなく new Global(Context) でグローバルスコープを生成する必要があります。
グローバルスコープは初期化されていないため、ImporterTopLevel.init(Context, Scriptable, boolean) を実行して初期化します。
 
print(数値); の結果が Java8/11 とは異なり「50.0」となります(Java7 では同じく「50.0」)。Java8/11 では「50」でした(個人的には「50.0」の方が納得できるのですが)。
 
ES2015(ES6)には対応しているとあったのですが、テンプレートリテラルが正しく解釈されません。1.7.14 で対応するようです。
 
調査用のコードはこんな感じです。

public static void main(String[] args) {
    Context context = Context.enter();
    try {
        // load、importPackage、print を使うために必要
        Global global = new Global(context);
        // スコープを初期化
        ImporterTopLevel.init(context, global, true);
      
        // JSファイルのロード
        context.evaluateString(global, "load('JsFunctions.js')", "<eval>", 1, null);

        // パッケージのインポート
        context.evaluateString(global, "importPackage(Packages.test.js)", "<eval>", 1, null);
        
        // スクリプトをコンパイル
        Script compiledScript = context.compileString("print(sum(10,50))", "<eval>", 1, null);

        // グローバル変数を定義
        context.evaluateString(global, "var gVal = 0", "eval", 1, null);

        JavaClass jClass = new JavaClass("rhino");
        for (int i = 0; i < 1000; i++) {
            // 評価用のスコープを用意(グローバルスコープを引数に渡してグローバルオブジェクトを共有する)
            Scriptable scope = context.initStandardObjects(global);
            
            // グローバル変数を出力後、カウントアップ
            context.evaluateString(scope, "print('グローバル変数 gVal=' + gVal++)", "<eval>", 1, null);
            
            // Javaのクラスを使用するfunctionを定義
            context.evaluateString(scope, "function sum() { return JavaFunctions.sum(Array.prototype.slice.call(arguments)); }", "<eval>", 1, null);
            
            // 内部でJavaのクラスを使用
            context.evaluateString(scope, "print(sum(1,5));", "<eval>", 1, null);
            
            // Javaから受け渡されたデータ、オブジェクトを使用
            ScriptableObject.putProperty(scope, "count", i * 10);
            ScriptableObject.putProperty(scope, "str", "creator");
            ScriptableObject.putProperty(scope, "jClass", jClass);
            // 受け渡されたデータをJavaのクラスに設定
            context.evaluateString(scope, "jClass.setCount(count)", "<eval>", 1, null);
            // 1.7.13 ではテンプレートリテラルの変数が展開されない
            //context.evaluateString(scope, "jClass.println(`str=${str}`)", "<eval>", 1, null); 
            context.evaluateString(scope, "jClass.println('str=' + str)", "<eval>", 1, null); 

            // ロードしたJSファイルの無名関数を使用
            context.evaluateString(scope, "print(joiner('Doc', 'Creator', 4))", "<eval>", 1, null);
            
            // 事前にコンパイルしたスクリプトを実行
            compiledScript.exec(context, scope);
        }
        
    } finally {
        Context.exit();
    }
}

結果:

グローバル変数 gVal=0
6.0
rhinoです。count=0, str=creator
Doc-Creator-4
60.0
・・・(略)
グローバル変数 gVal=999
6.0
rhinoです。count=9990, str=creator
(略)

Java17 で使用するのに特に問題はなさそうです。

Rhino を ScriptEngine インタフェース越しに使う

importPackageload が使用出来ません(せめてオプションで使用可能に出来るように Github/Issues に投稿済み)。
 
直接使用する場合と同様に、テンプレートリテラルが正しく解釈されません。
 
const を使用するとエラーになる バグがあります。
 
const が使えないので var を使用したロードする JS ファイルを別途用意します。
JsFunctions2.js

var joiner = function() {
    return Array.prototype.join.call(arguments, '-');
};

 
調査用のコードはこんな感じです。

public static void main(String[] args) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("rhino");
    try {
        // JSファイルのロードは出来ない(2021/20 時点)
        //engine.eval("load('JsFunctions.js')");
        
        // バグ(https://github.com/mozilla/rhino/issues/798)により const が使えないので
        // var を使用した JS ファイルをロードする
        //engine.eval(new FileReader("JsFunctions.js"));            
        engine.eval(new FileReader("JsFunctions2.js"));
        
        // パッケージのインポートは出来ない(2021/20 時点)
        //engine.eval("importPackage(Packages.test.js)");
        
        // スクリプトをコンパイル
        CompiledScript compiledScript = ((Compilable) engine).compile("print(sum(10,50))");
        
        // グローバル変数を定義
        engine.eval("var gVal = 0");

        JavaClass jClass = new JavaClass("rhino-engine");
        Bindings gBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
        for (int i = 0; i < 1000; i++) {
            // 評価用のコンテキストを用意
            ScriptContext cxLocal = new SimpleScriptContext();
            engine.setContext(cxLocal);
            // グローバルオブジェクトを共有
            cxLocal.setBindings(gBindings, ScriptContext.GLOBAL_SCOPE);
            
            // グローバル変数を出力後、カウントアップ
            engine.eval("java.lang.System.out.println('グローバル変数 gVal=' + gVal++)");

            // Javaのクラスを使用するfunctionを定義(インポート出来ていないのでパッケージから記述)
            engine.eval("function sum() { return Packages.test.js.JavaFunctions.sum(Array.prototype.slice.call(arguments)); }");
            
            // 内部でJavaのクラスを使用
            engine.eval("print(sum(1,5))");
            
            // Javaから受け渡されたデータ、オブジェクトを使用
            Bindings eBindings = cxLocal.getBindings(ScriptContext.ENGINE_SCOPE);
            eBindings.put("count", i * 10);
            eBindings.put("str", "creator");
            eBindings.put("jClass", jClass);
            // 受け渡されたデータをJavaのクラスに設定
            engine.eval("jClass.setCount(count)");
            // 1.7.13 ではテンプレートリテラルの変数が展開されない
            //engine.eval("jClass.println(`str=${str}`);");
            engine.eval("jClass.println('str=' + str)");
            
            // ロードしたJSファイルの無名関数を使用
            engine.eval("print(joiner('Doc', 'Creator', 4))");
            
            // 事前にコンパイルしたスクリプトを実行
            compiledScript.eval();
        }
        
    } catch (ScriptException | FileNotFoundException e) {
        e.printStackTrace();
    }
}

結果:

グローバル変数 gVal=0
6
rhino-engineです。count=0, str=creator
Doc-Creator-4
60
・・・(略)
グローバル変数 gVal=999
6
rhinoです。count=9990, str=creator
(略)

Java の ScriptEngine インタフェースに対応したのがこのバージョンからということで、しばらく様子を見た方が良さそうです。

Nashorn を実行する

2021/10/20 時点で最新のリリースは 15.3。Maven Central から nashorn-core-15.3.jar を取得して classpath に追加します。
依存関係にある asm-7.3.1.jarasm-commons-7.3.1.jarasm-util-7.3.1.jarasm-tree-7.3.1.jar も取得して classpath に追加します。
 
直接使う(org.openjdk.nashorn.internal.xxx の使用)ことを想定していないようなので、ScriptEngine インタフェース越しに使います。

Nashorn を ScriptEngine インタフェース越しに使う

デフォルトの状態ではグローバルスコープのオブジェクトを異なるコンテキスト間で共有できません。共有可能にするためには --global-per-engine オプションを指定する必要があります。
 
importPackage もデフォルトの状態では使用出来ません。使用可能にするには load(“nashorn:mozilla_compat.js”) を実行する必要があります。
 
この時点で ECMAScript Edition 5.1 までの対応となっていますが、const やテンプレートリテラルなど、一部の ES2015(ES6) の機能を使用可能にすることが出来ます。使用可能にするには --language=es6 オプションを指定する必要があります。
 
調査用のコードはこんな感じです。

public static void main(String[] args) {        
    // コンテキスト間でグローバルスコープのオブジェクトを共有するために「--global-per-engine」を指定
    // ES2015(ES6)の一部機能を有効化するために「--language=es6」を指定
    System.setProperty("nashorn.args", "--global-per-engine --language=es6");

    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("nashorn");
    try {
        // JSファイルのロード
        engine.eval("load('JsFunctions.js')");
        
        // パッケージのインポート
        // importPackageを使うために必要
        engine.eval("load('nashorn:mozilla_compat.js')");
        engine.eval("importPackage(Packages.test.js)");
        
        // スクリプトをコンパイル
        CompiledScript compiledScript = ((Compilable) engine).compile("print(sum(10,50))");
        
        // グローバル変数を定義
        engine.eval("var gVal = 0");

        JavaClass jClass = new JavaClass("nashorn-engine");
        Bindings gBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
        for (int i = 0; i < 1000; i++) {
            // 評価用のコンテキストを用意
            ScriptContext cxLocal = new SimpleScriptContext();
            // グローバルオブジェクトを共有
            cxLocal.setBindings(gBindings, ScriptContext.GLOBAL_SCOPE);
            engine.setContext(cxLocal);
            
            // グローバル変数を出力後、カウントアップ
            engine.eval("print('グローバル変数 gVal=' + gVal++)");

            // Javaのクラスを使用するfunctionを定義
            engine.eval("function sum() { return JavaFunctions.sum(Array.prototype.slice.call(arguments)); }");
            
            // 内部でJavaのクラスを使用
            engine.eval("print(sum(1,5))");
            
            // Javaから受け渡されたデータ、オブジェクトを使用
            Bindings eBindings = cxLocal.getBindings(ScriptContext.ENGINE_SCOPE);
            eBindings.put("count", i * 10);
            eBindings.put("str", "creator");
            eBindings.put("jClass", jClass);
            // 受け渡されたデータをJavaのクラスに設定
            engine.eval("jClass.setCount(count)");
            engine.eval("jClass.println(`str=${str}`)");
            
            // ロードしたJSファイルの無名関数を使用
            engine.eval("print(joiner('Doc', 'Creator', 4));");
            
            // 事前にコンパイルしたスクリプトを実行
            compiledScript.eval();
        }
        
    } catch (ScriptException e) {
        e.printStackTrace();
    }
}

なお、ScriptEngineManager ではなく NashornScriptEngineFactory を使うとシステムプロパティを使わずにオプションを指定できます。

NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
// コンテキスト間でグローバルスコープのオブジェクトを共有するために「--global-per-engine」を指定
// ES2015(ES6)の一部機能を有効化するために「--language=es6」を指定
NashornScriptEngine engine = (NashornScriptEngine) factory.getScriptEngine("--global-per-engine", "--language=es6");

結果:

グローバル変数 gVal=0
6
nashorn-engineです。count=0, str=creator
Doc-Creator-4
60
・・・(略)
グローバル変数 gVal=999
6
nashorn-engineです。count=9990, str=creator
(略)

Java11 のプログラムが Java17 でもそのまま使えそうです。

GraalJS を実行する

2021/10/20 時点で最新のリリースは 21.3.0。調査開始時点では 21.2.0 でしたが、このバージョンでは、あるコンテキストで生成した Value を別のコンテキストに引き渡すことが出来ません。
Maven Central から js-21.3.0.jar を取得して classpath に追加します。
依存関係にある regex-21.3.0.jartruffle-api-21.3.0.jargraal-sdk-21.3.0.jaricu4j-69.1.jar も取得して classpath に追加します。
 
ScriptEngine インタフェース越しに使用する場合は、更に js-scriptengine.jar も取得して classpath に追加します。

GraalVM を直接使う

デフォルトの状態ではスクリプト内で Java のクラスにアクセス出来ません。使用可能にするにはコンテキスト生成時に allowHostAccess(HostAccess.ALL)allowHostClassLookup(s -> true) で権限を付与する必要があります(HostAccess.ALL は HostAccess クラスのインスタンスでも可)。
また、ファイルを扱うことも出来ません。使用可能にするにはコンテキスト生成時に allowIO(true) で権限を付与する必要があります。
importPackage もまた、デフォルトの状態では使用出来ません。使用可能にするには nashorn と同じく load(“nashorn:mozilla_compat.js”) を実行する必要があります。また、 load(“nashorn:mozilla_compat.js”) を実行するのには、コンテキスト生成時に option(“js.nashorn-compat”, “true”) でオプションを指定する必要があります。オプションを指定可能にするには、allowExperimentalOptions(true) で権限を付与する必要があります。
なお、権限を個別に付与する代わりに allowAllAccess(true) で全権付与することも可能です。
 
調査した限りでは、グローバルオブジェクトを複数のコンテキストで共有することは出来ませんでした。
外部ファイルのロードなどの時間のかる処理結果の共有は、Source で代用しました。Source にすることで実行する際にキャッシュが効きます。なお、キャッシュを有効化するには対象のコンテキストでエンジンを共有する必要があります。コンテキスト生成時に engine(エンジンのインスタンス) で使用するエンジンを指定します。
 
任意のコンテキスト内で定義された変数の共有はどうやっても無理でした。Bindings.getMemberBindings.putMember を使用して値を受け渡すことは可能です。Value の中身(受け渡すデータ)が Java クラスの場合ははインスタンスが保持されるものの、プリミティブ型の場合は当然ですが、Bindings.putMember の度に別インスタンスとなります。共有と言うにはほど遠い結果となってしまいました。
 
一応調査用のコードと結果を載せておきます。

public static void main(String[] args) {
    try (Engine engine = Engine.create()) {
        // .engine(engine) : 異なる Context に共有の Engine を使用しないと Source でキャッシュが使用されない
        // .allowHostClassLookup(s -> true)、.allowHostAccess(HostAccess.ALL) : Java のクラスにアクセスするために必要
        // .allowIO(true) : 外部ファイルをロードするのに必要
        // .allowExperimentalOptions(true) : option("js.nashorn-compat", "true") を指定するために必要
        // .option("js.nashorn-compat", "true") : load('nashorn:mozilla_compat.js') するために必要
        Builder builder = Context.newBuilder("js")
                .engine(engine)
                .allowHostClassLookup(s -> true)
                .allowHostAccess(HostAccess.ALL)
                .allowIO(true) 
                .allowExperimentalOptions(true)
                .option("js.nashorn-compat", "true");

        // 以下の方が楽ですが
        //Builder builder = Context.newBuilder("js")
        //        .engine(engine)
        //        .allowAllAccess(true)
        //        .option("js.nashorn-compat", "true");
        
        
        try (Context globalCx = builder.build()) {
            // importPackageを 使うために必要
            Source loadcompatibility = Source.create("js", "load('nashorn:mozilla_compat.js')");
            globalCx.eval(loadcompatibility);
            
            // JSファイルのロード
            // Source loadJs = Source.create("js", "load('JsFunctions.js')");
            // こっちの方が若干パフォーマンスが良い
            Source loadJs = Source.newBuilder("js", new File("JsFunctions.js")).build();
            globalCx.eval(loadJs);

            // パッケージのインポート
            Source importPackage = Source.create("js", "importPackage('test.js')");
            globalCx.eval(importPackage);
            
            // スクリプトをコンパイル
            Source source = Source.create("js", "print(sum(10,50))");
            
            // グローバル変数を定義
            Value val = globalCx.eval("js", "var gVal = 0");
            
            JavaClass jClass = new JavaClass("graalvm");
            for (int i = 0; i < 1000; i++) {
                // 評価用のコンテキストを用意
                try (Context localCx = builder.build()) {
                    // グローバルオブジェクトの共有の代わりに Source を実行
                    localCx.eval(loadcompatibility);
                    localCx.eval(loadJs);
                    localCx.eval(importPackage);
                    // グローバル変数の共有っぽい何か
                    localCx.getBindings("js").putMember("gVal", val);
                            
                    // グローバル変数を出力後、カウントアップ
                    localCx.eval("js", "print('グローバル変数 gVal=' + gVal++)");
                    
                    // Javaのクラスを使用するfunctionを定義
                    localCx.eval("js", "function sum() { return JavaFunctions.sum(Array.prototype.slice.call(arguments)); }");
                    
                    // 内部でJavaのクラスを使用
                    localCx.eval("js", "print(sum(1,5))");
                    
                    // Javaから受け渡されたデータ、オブジェクトを使用
                    Value bindings = localCx.getBindings("js");
                    bindings.putMember("count", i * 10);
                    bindings.putMember("str", "creator");
                    bindings.putMember("jClass", jClass);
                    // 受け渡されたデータをJavaのクラスに設定
                    localCx.eval("js", "jClass.setCount(count)");
                    localCx.eval("js", "jClass.println(`str=${str}`)"); 

                    // ロードしたJSファイルの無名関数を使用
                    localCx.eval("js", "print(joiner('Doc', 'Creator', 4))");
                    
                    // キャッシュされたスクリプトを実行
                    localCx.eval(source);
                    
                    // putMemberする毎に新しいValueインスタンスが生成されるので毎回取得(既にグローバルではない)
                    val = localCx.getBindings("js").getMember("gVal");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果:

グローバル変数 gVal=0
6
graalvmです。count=0, str=creator
Doc-Creator-4
60
・・・(略)
グローバル変数 gVal=999
6
graalvmです。count=9990, str=creator
(略)

複数言語を扱うだけに他の候補とは一線を画す、というか、正直に言えば使いにくい。

GraalJS で ScriptEngine インタフェース越しに使う

GraalVM を直接使う場合と同様に、importPackage はデフォルトの状態では使用出来ません。使用可能にするには load(“nashorn:mozilla_compat.js”) を実行する必要があります。load(“nashorn:mozilla_compat.js”) を実行するのには、option(“js.nashorn-compat”, “true”) でオプションを指定する必要があります。このオプションを指定すると、allow~ で指定すべき全ての権限が付与されます。
 
コンテキスト間でグローバルスコープのオブジェクトを共有する際に、setBindings(Bindings, ScriptContext.GLOBAL_SCOPE) だと loadimportPackage の結果が反映されません(バグ?)。代わりに、setBindings(Bindings, ScriptContext.ENGINE_SCOPE) を実行します。
 
調査用のコードはこんな感じです。

public static void main(String[] args) {
    // load('nashorn:mozilla_compat.js') するために必要、権限も全て付与される
    System.setProperty("polyglot.js.nashorn-compat", "true");

    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("graal.js");
    try {
        // JSファイルのロード
        engine.eval("load('JsFunctions.js')");
        
        // パッケージのインポート
        // importPackageを使うために必要
        engine.eval("load('nashorn:mozilla_compat.js')");
        engine.eval("importPackage(Packages.test.js)");
        
        // スクリプトをコンパイル
        CompiledScript compiledScript = ((Compilable) engine).compile("print(sum(10,50))");
        
        // グローバル変数を定義
        engine.eval("var gVal = 0");

        JavaClass jClass = new JavaClass("graaljs");
        Bindings gBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
        for (int i = 0; i < 1000; i++) {
            // 評価用のコンテキストを用意
            ScriptContext cxLocal = new SimpleScriptContext();
            // グローバルスコープでは load や importPackage が引き継がれない。
            //cxLocal.setBindings(gBindings, ScriptContext.GLOBAL_SCOPE);
            cxLocal.setBindings(gBindings, ScriptContext.ENGINE_SCOPE);
            engine.setContext(cxLocal);
            
            // グローバル変数を出力後、カウントアップ
            engine.eval("print('グローバル変数 gVal=' + gVal++)");

            // Javaのクラスを使用するfunctionを定義
            engine.eval("function sum() { return JavaFunctions.sum(Array.prototype.slice.call(arguments)); }");
            
            // 内部でJavaのクラスを使用
            engine.eval("print(sum(1,5))");
            
            // Javaから受け渡されたデータ、オブジェクトを使用
            Bindings eBindings = cxLocal.getBindings(ScriptContext.ENGINE_SCOPE);
            eBindings.put("count", i * 10);
            eBindings.put("str", "creator");
            eBindings.put("jClass", jClass);
            // 受け渡されたデータをJavaのクラスに設定
            engine.eval("jClass.setCount(count)");
            engine.eval("jClass.println(`str=${str}`)");
            
            // ロードしたJSファイルの無名関数を使用
            engine.eval("print(joiner('Doc', 'Creator', 4));");
            
            // 事前にコンパイルしたスクリプトを実行
            compiledScript.eval();
        }
        
    } catch (ScriptException e) {
        e.printStackTrace();
    }
}

結果:

グローバル変数 gVal=0
6
graaljsです。count=0, str=creator
Doc-Creator-4
60
・・・(略)
グローバル変数 gVal=999
6
graaljsです。count=9990, str=creator
(略)

グローバルスコープの問題さえなければ・・・

実行速度を計測する

調査した各種エンジンに Java7(Rhino)、Java8(Nashorn)、Java11(Nashorn)を加えて速度を計測します。
 
条件が同じになるように、rhino/nashorn 共に ScriptEngine 使用時のスコープは ENGINE_SCOPE に変更。

// グローバルオブジェクトを共有
// graal に合わせて ENGINE_SCOPE を使用
//cxLocal.setBindings(gBindings, ScriptContext.GLOBAL_SCOPE);
cxLocal.setBindings(gBindings, ScriptContext.ENGINE_SCOPE);

 
Java8 で Binding の put/remove を実行するとパフォーマンスが極端に悪くなる現象が起きていたので、検証のために追加します。
 
Rhino を直接使う場合:

        ScriptableObject.deleteProperty(global, "obj1");
        ScriptableObject.deleteProperty(global, "obj2");
        ScriptableObject.deleteProperty(global, "obj3");
        ScriptableObject.putProperty(global, "obj4", new JavaClass("obj4"));
        ScriptableObject.putProperty(global, "obj5", new JavaClass("obj5"));
        ScriptableObject.putProperty(global, "obj6", new JavaClass("obj6"));
        context.evaluateString(scope, "obj4.getCount();obj5.getCount();obj6.getCount()", "<eval>", 1, null);
    }
    
} finally {
    Context.exit();
}

 
GraalVM を使う場合:

    // グローバル/ローカルスコープという考え方がないのでとりあえずオブジェクトを追加/削除
    bindings.putMember("obj1", new JavaClass("obj1"));
    bindings.putMember("obj2", new JavaClass("obj2"));
    bindings.putMember("obj3", new JavaClass("obj3"));
    localCx.eval("js", "obj1.getCount();obj2.getCount();obj3.getCount()");
    
    bindings.removeMember("obj1");
    bindings.removeMember("obj2");
    bindings.removeMember("obj3");
    bindings.putMember("obj4", new JavaClass("obj4"));
    bindings.putMember("obj5", new JavaClass("obj5"));
    bindings.putMember("obj6", new JavaClass("obj6"));
    localCx.eval("js", "obj4.getCount();obj5.getCount();obj6.getCount()");
} finally {
    localCx.close();
}

 
Rhino を ScriptEngine インタフェース越しに使う場合と Nashorn を使う場合は同じです。

    // グローバルスコープのオブジェクトを追加/削除
    Bindings bindings = new SimpleBindings();
    cxLocal.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
    bindings.put("obj1", new JavaClass("obj1"));
    bindings.put("obj2", new JavaClass("obj2"));
    bindings.put("obj3", new JavaClass("obj3"));
    engine.eval("obj1.getCount();obj2.getCount();obj3.getCount()");
    
    bindings.remove("obj1");
    bindings.remove("obj2");
    bindings.remove("obj3");
    bindings.put("obj4", new JavaClass("obj4"));
    bindings.put("obj5", new JavaClass("obj5"));
    bindings.put("obj6", new JavaClass("obj6"));
    engine.eval("obj4.getCount();obj5.getCount();obj6.getCount()");
}

 
GraalJS はスコープ周りが少しバグってます。応急処置をしましょう。

    // グローバルスコープのオブジェクトを追加/削除
    Bindings bindings = new SimpleBindings();
    cxLocal.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
    bindings.put("obj1", new JavaClass("obj1"));
    bindings.put("obj2", new JavaClass("obj2"));
    bindings.put("obj3", new JavaClass("obj3"));
    // バグ?で動作しないのでとりあえず応急処置
    //engine.eval("obj1.getCount();obj2.getCount();obj3.getCount()");
    engine.eval("var hoge = !obj1 || !obj2 || !obj3");
    
    bindings.remove("obj1");
    bindings.remove("obj2");
    bindings.remove("obj3");
    bindings.put("obj4", new JavaClass("obj4"));
    bindings.put("obj5", new JavaClass("obj5"));
    bindings.put("obj6", new JavaClass("obj6"));
    engine.eval("obj4.getCount();obj5.getCount();obj6.getCount()");
}

 
調査用のプログラムでは Java7/8/11 で使用不可能な機能を使っているので専用のプログラムを用意します。Java7 で動作するようにこのプログラムのコンパイラー準拠レベルは 1.7 です。

public static void main(String[] args) {
    int limit = 1000;
    if (args.length > 0) {
        limit = Integer.parseInt(args[0]);
    }

    // コンテキスト間でグローバルスコープのオブジェクトを共有するために「--global-per-engine」を指定
    System.setProperty("nashorn.args", "--global-per-engine");

    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("JavaScript");
    boolean version7 = engine.getFactory().getNames().contains("rhino");
    
    long start = System.currentTimeMillis();
    
    try {
        // JSファイルのロード
        if (version7) {
            engine.eval(new FileReader("JsFunctions2.js"));
        } else {
            engine.eval("load('JsFunctions2.js')");
        }
        
        // パッケージのインポート
        if (!version7) {
            // importPackageを使うために必要
            engine.eval("load('nashorn:mozilla_compat.js')");
        }
        engine.eval("importPackage(Packages.test.js)");
        
        // スクリプトをコンパイル
        CompiledScript compiledScript = ((Compilable) engine).compile("print(sum(10,50))");
        
        // グローバル変数を定義
        engine.eval("var gVal = 0");

        JavaClass jClass = new JavaClass("scriptengine");
        Bindings gBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
        for (int i = 0; i < limit; i++) {
            // 評価用のコンテキストを用意
            ScriptContext cxLocal = new SimpleScriptContext();
            // グローバルオブジェクトを共有
            // graal に合わせて ENGINE_SCOPE を使用
            //cxLocal.setBindings(gBindings, ScriptContext.GLOBAL_SCOPE);
            cxLocal.setBindings(gBindings, ScriptContext.ENGINE_SCOPE);
            engine.setContext(cxLocal);
            
            // グローバル変数を出力後、カウントアップ
            engine.eval("print('グローバル変数 gVal=' + gVal++)");

            // Javaのクラスを使用するfunctionを定義
            engine.eval("function sum() { return JavaFunctions.sum(Array.prototype.slice.call(arguments)); }");
            
            // 内部でJavaのクラスを使用
            engine.eval("print(sum(1,5))");
            
            // Javaから受け渡されたデータ、オブジェクトを使用
            Bindings eBindings = cxLocal.getBindings(ScriptContext.ENGINE_SCOPE);
            eBindings.put("count", i * 10);
            eBindings.put("str", "creator");
            eBindings.put("jClass", jClass);
            // 受け渡されたデータをJavaのクラスに設定
            engine.eval("jClass.setCount(count)");
            engine.eval("jClass.println('str=' + str)");
            
            // ロードしたJSファイルの無名関数を使用
            engine.eval("print(joiner('Doc', 'Creator', 4))");
            
            // 事前にコンパイルしたスクリプトを実行
            compiledScript.eval();

            // グローバルスコープのオブジェクトを追加/削除
            Bindings bindings = new SimpleBindings();
            cxLocal.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
            bindings.put("obj1", new JavaClass("obj1"));
            bindings.put("obj2", new JavaClass("obj2"));
            bindings.put("obj3", new JavaClass("obj3"));
            engine.eval("obj1.getCount();obj2.getCount();obj3.getCount()");
            
            bindings.remove("obj1");
            bindings.remove("obj2");
            bindings.remove("obj3");
            bindings.put("obj4", new JavaClass("obj4"));
            bindings.put("obj5", new JavaClass("obj5"));
            bindings.put("obj6", new JavaClass("obj6"));
            engine.eval("obj4.getCount();obj5.getCount();obj6.getCount()");
        }
    } catch (ScriptException | FileNotFoundException e) {
        e.printStackTrace();
    }
    
    long time = System.currentTimeMillis() - start;
    System.out.println("scriptengine(" + limit + ") 実行速度: " + time);
}

 
ループ数を 1000、5000、10000 と変えて計測します。計測結果は以下の通りです。
 
計測結果
 
正直意外でした。Java7 が速いのは知っていたので Java17 + Rhino が一番速いと思ってました。
Graal 速いですね。スコープ周りがバグってるのが本当に惜しい。

Binding の put/remove を追加していない場合はどうなのかというと、
 
軽量版計測結果
 
Nashorn があからさまに速くなりました。
 
Java17 ではとりあえず Nashorn を選択し、GraalJS が修正されたら GraalJS を使用するのが良さそうです。