RhinoでJavaの中からJavaScriptを利用する 2021年版

JavaVM で JavaScript エンジンを動かして、DSLとして JavaScript を利用したいようなケースは結構あったはずだが、JavaVM に標準で搭載されているJavaScriptエンジン Nashorn はすでに Java11 で[非推奨](http://openjdk.java.net/jeps/335)となっていて、プロダクションで使っている勢にとっては不安の種となっている。

Nashorn 非推奨の理由は、最新の ECMAScript 仕様に追随しきれないというもの。

確かにモダンなESを使いたいモチベーションはあるが、それが足枷となってJVMそのものの進化のスピードが律速されてしまうのであれば、非推奨としてJVM本体の進化のペースとは切り離すという判断は合理的。

ではどうしたらいいのか。Alt JVM として開発されている他言語対応JVM GraalVM を使うというのが第一の選択肢である。GraalVM に関しては色々なところで触れられているのでここでは触れない。

実運用上は VM を変更して運用することはないとはいえ、Javaのプログラムが特定のJVMでないと動かないというのは、なんとなく残念な感じがしてしまう。ここで試してみるのは、Nashorn 以前の JS on JVM を実現するスタンダードである Rhino である。

https://github.com/mozilla/rhino

久しぶりに Rhino のリポジトリを見てみると、最新の 1.7.13 が2020年9月にリリースされており、今でもちゃんとメンテナンスされていることがわかる。

ES2015 との[互換性テーブル](http://mozilla.github.io/rhino/compat/engines.html)を見てみると、なかなか苦労している様子ではあるが、互換性のレベルも徐々に向上しているので今後にも期待が持てそうだ。

## ScriptEngine で Rhino を使う

Rhino 1.7.13 で、JVM の ScriptEngine インタフェースに対応している。なるほど Nashorn をそのまま置き換えられるようになったのだ。

Rhino で ScriptEngine インタフェースを利用するには、従来の rhino.jar だけではなく rhino-engine.jar が必要となる。

そのため pom.xml に以下の依存関係を記述する。(Mavenの場合)

```xml
<!-- 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`を指定するだけでそのまま利用可能だ。

```java
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 のスコープにオブジェクトをセットして呼び出す必要がある。

```java
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` は自分で作成した以下のようなクラスだ。

```java
public class MyConsole {

    public void log(Object arg) {
        System.out.println(String.valueOf(arg));
    }
}
```

結果は

```text
a=30, b=20, c=30
```

となり期待するとおりとなった。

## 生で Rhino を使う

1.7.12 以前の Rhino と同様に、ScriptEngine インタフェースを経由せずにそのまま生で Rhino を使うことも可能だ。

この場合のコードは以下のようになる。

```java
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 のオブジェクトを操作するプログラムを書いてみたところ以下のケースに遭遇した。

```javascript
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 を採用することにおいて不安はない。