JavaVM で JavaScript エンジンを動かして、DSLとして JavaScript を利用したいようなケースは結構あったはずだが、JavaVM に標準で搭載されているJavaScriptエンジン Nashorn はすでに Java11 で非推奨となっていて、プロダクションで使っている勢にとっては不安の種となっている。
Nashorn 非推奨の理由は、最新の ECMAScript 仕様に追随しきれないというもの。
確かにモダンなESを使いたいモチベーションはあるが、それが足枷となってJVMそのものの進化のスピードが律速されてしまうのであれば、非推奨としてJVM本体の進化のペースとは切り離すという判断は合理的。
ではどうしたらいいのか。Alt JVM として開発されている他言語対応JVM GraalVM を使うというのが第一の選択肢である。GraalVM に関しては色々なところで触れられているのでここでは触れない。
実運用上は VM を変更して運用することはないとはいえ、Javaのプログラムが特定のJVMでないと動かないというのは、なんとなく残念な感じがしてしまう。ここで試してみるのは、Nashorn 以前の JS on JVM を実現するスタンダードである Rhino である。
久しぶりに Rhino のリポジトリを見てみると、最新の 1.7.13 が2020年9月にリリースされており、今でもちゃんとメンテナンスされていることがわかる。
ES2015 との互換性テーブルを見てみると、なかなか苦労している様子ではあるが、互換性のレベルも徐々に向上しているので今後にも期待が持てそうだ。
ScriptEngine で Rhino を使う
Rhino 1.7.13 で、JVM の ScriptEngine インタフェースに対応している。なるほど Nashorn をそのまま置き換えられるようになったのだ。
Rhino で ScriptEngine インタフェースを利用するには、従来の rhino.jar だけではなく rhino-engine.jar が必要となる。
そのため pom.xml に以下の依存関係を記述する。(Mavenの場合)
<!-- https://mvnrepository.com/artifact/org.mozilla/rhino -->
<dependency>
<groupId>org.mozilla</groupId>
<artifactId>rhino</artifactId>
<version>1.7.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mozilla/rhino-engine -->
<dependency>
<groupId>org.mozilla</groupId>
<artifactId>rhino-engine</artifactId>
<version>1.7.13</version>
</dependency>
最も単純に JavaScript を実行するには以下のようなコードになる。エンジン名にrhino
を指定するだけでそのまま利用可能だ。
ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("rhino");
try {
scriptEngine.eval("function add(a, b) { return a + b }");
Number v = (Number) scriptEngine.eval("add(13, 17)");
System.out.println(v);
} catch (ScriptException e) {
e.printStackTrace();
}
ただ Nashorn の場合は、上記コードの戻り値のNumber
の実際の型はDouble
だったが、Rhinoの場合はLong
に変わっている。細かいところではいろいろと差異がありそうだ。
Javaオブジェクトと相互運用してみる
JavaScript の中から Java のオブジェクトを利用するためには、JavaScript のスコープにオブジェクトをセットして呼び出す必要がある。
Map<String, Object> map = new HashMap<>();
map.put("a", 10);
map.put("b", 20);
map.put("console", new MyConsole());
SimpleBindings bindings = new SimpleBindings(map);
scriptEngine
.eval("c = a + b;"
+ "a += b;"
+ "console.log('a=' + a + ', b=' + b + ', c=' + c);",
bindings);
ここで MyConsole
は自分で作成した以下のようなクラスだ。
public class MyConsole {
public void log(Object arg) {
System.out.println(String.valueOf(arg));
}
}
結果は
a=30, b=20, c=30
となり期待するとおりとなった。
生で Rhino を使う
1.7.12 以前の Rhino と同様に、ScriptEngine インタフェースを経由せずにそのまま生で Rhino を使うことも可能だ。
この場合のコードは以下のようになる。
Context context = new ContextFactory().enterContext();
Scriptable globalScope = context.initStandardObjects();
Script script = null;
try (Reader reader = new FileReader(jsSourceFile)) {
script = context.compileReader(reader, jsSourceFile.getName(), 1, null);
} catch (IOException | EvaluatorException e) {
e.printStackTrace(System.err);
System.exit(1);
}
ScriptableObject.putProperty(globalScope, "a", 10);
ScriptableObject.putProperty(globalScope, "b", 20);
ScriptableObject.putProperty(globalScope, "console", new MyConsole());
// run global scope context
script.exec(context, globalScope);
文字列の比較に注意
いくつか JavaScript で Java のオブジェクトを操作するプログラムを書いてみたところ以下のケースに遭遇した。
var javaObject = someObject.getValue(); // javaObject は java.lang.String である
console.log(javaObject); // "OK"
if (javaObject === 'OK') { // false
// JavaScriptの文字列リテラルと同一とみなされない
console.log('===');
} else if (javaObject == 'OK') { // false
// == でもダメ
console.log('==');
} else if ('OK'.equals(javaObject)) { // true
console.log('equals');
}
この結果は equals
になる。Java の文字列と JavaScript の文字列は別のものなのである。
これは注意が必要だ。
追記
context.getWrapFactory().setJavaPrimitiveWrap(false);
を呼び出すことでこのあたりの挙動が変更できるとのこと。
まとめ
Rhino は着々と進化しつづけている。2021年においても Java アプリケーションにおける DSL として JavaScript を採用することにおいて不安はない。