前回は、JDK 15で削除される予定のJavaScriptエンジン「Nashorn」に対して、その移行先の有力候補である「GraalJS(GraalVM JavaScript)」の使用方法を解説した。GraalJSはJava Scripting APIをサポートしているので、Nashornと同じように使うことができるが、一方で独自のJavaScriptコードの実行方法も用意されている。今回はそのようなGraalJS独自の機能や、Nashorn独自の機能との互換性を紹介する。

  • NashornはJDK 11で非推奨になり、JDK 15で削除される

    NashornはJDK 11で非推奨になり、JDK 15で削除される

polyglot APIを使ったJavaScriptコードの実行

Java Scripting APIはJavaの標準APIとして提供されているものなので、JavaScriptエンジンが変わったとしても、まったく同じように使用できるという利点がある。ただし、このAPIそのものも作られてから長い期間が経っているので、今となっては少し古い設計となっている。そこでGraalJSでは、よりモダンな実装によるJavaScriptコードの実行方法も提供されている。それがpolyglot APIである。

次のコードは、polyglot APIを使用したJavaScriptコードの実行例だ。実行するJavaScriptコードの内容は前回見せたJava Scripting APIのものと同じだが、Javaプログラムからの実行方法は大きく異なっている。

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Source;

public class Main {

    public static void main(String[] args) {
        try(Engine engine = Engine.create()) {
            Source source = Source.create(
                    "js",
                    "function sum(a, b) { return a + b }");
            Source source2 = Source.create(
                    "js",
                    "sum(21, 22)");
            try (Context context = Context.newBuilder().engine(engine).build()) {
                context.eval(source);
                int v = context.eval(source2).asInt();
                System.out.println(v);
            }
        }
    }
}

Engineクラス、Sourceクラス、Contextクラスは、それぞれGraalJSのpolyglot APIによって提供されている。EngineクラスはJavaScriptエンジンを表すクラスで、Engine.create()メソッドによって作成する。Sourceクラスは実行するJavaScriptのソースコードを表すクラスで、Source.create()メソッドに言語指定とコードを渡すことで作成できる。

Contextクラスは実際にコードを実行するコンテキストを表している。Contextのインスタンスは、次のようにまずビルダーを取得した上で、使用するJavaScriptエンジンのEngineインタンスをengine()メソッドに指定して作成する。

Context context = Context.newBuilder().engine(engine).build());

Contextを用意できたら、eval()メソッドを使用すればSourceインスタンスに格納されたコードが実行される。

int v = context.eval(source2).asInt();

eval()が返すのはValueインスタンスで、ここからasInt()やasString()などのメソッドを使って値を取り出す。Java Scripting APIではコード実行の戻り値として返される型はObjectだが、polyglot APIではValueによって対応する値の種類が限定されているためより安全に使用できる。

GraalJSでJavaScriptからJavaクラスを使用する

続いてJavaScrptからJavaクラスを使用する方法を見てみよう。GraalJSにはNashornのjjsコマンドの代わりにJavaScriptを実行できる「js」コマンドが付属している。次のJavaScriptは、前回紹介したJavaのLocalDateTimeクラスを使用するものと同じコードである。

var now = java.time.LocalDateTime.now();
print("now = " + now);
print("1 day later = " + now.plusDays(1));
print("Truncated To Hours = " + now.truncatedTo(java.time.temporal.ChronoUnit.HOURS));

このコードをjsコマンドで実行しようとすると、次のようにエラーになってしまう。

$ js javadate.js
TypeError: (intermediate value).time.LocalDateTime.now is not a function
    at <js> :program(javadate.js:1:10-38)

GraalJSの場合、JavaScriptでJavaクラスを利用したい場合は、次のようにjsコマンドに「--jvm」フラグを付けて実行する必要がある。

$ js --jvm javadate.js
now = 2020-08-25T11:52:13.175001
1 day later = 2020-08-26T11:52:13.175001
Truncated To Hours = 2020-08-25T11:00

GraalJSでNashornの拡張機能を利用する

前回紹介したように、GraalJSはECMAScript仕様に準拠しているので、標準的なJavaScriptコードはすべて問題なく実行することができる。しかしNashornからの移行という観点では、それだけではまだ十分とは言えない。Nashornには標準のECMAScriptにはない独自の機能がいくつか用意されているため、それらの機能を使用している場合は、GraalJSに移行する際に少し注意を払わなけれなならない。

例えば、Nashornでは拡張機能として次のような構文がサポートされている。

  • 条件付きcatch節 (Conditional catch clauses)
  • 関数式
  • for-each式
  • 匿名クラス風の新しい文法
  • 匿名関数宣言
  • 複数行の文字列リテラル(-scriptingモード時のみ)
  • 文字列リテラル中の${式}の利用(-scriptingモード時のみ)
  • バッククォート(`)を使ったコマンドの実行(-scriptingモード時のみ)
  • #を使ったコメントおよびshebangの記述(-scriptingモード時のみ)

これらの機能の詳細は、下記のページに詳しくまとめられているので、本稿と合わせて参考にしていただきたい。

GraalJSでも、Nashornからの移行に配慮して、これらの拡張構文がサポートされている。ただし、使用するにはjsコマンドに「--js.syntax-extensions」と「--experimental-options」という2つのフラグを付けて実行する必要がある。

次のコードは、Nashornの拡張構文である関数式を使用した例である。

function sqr(x) x*x

print(sqr(2));

これをフラグなしで実行した場合、次のように文法エラーになる。

$ js closure.js
SyntaxError: closure.js:1:16 Expected { but found x
function sqr(x) x*x
                ^

互換性のための2つのフラグを付ければ、Nashornと同じように実行できる。

$ js --js.syntax-extensions --experimental-options closure.js
4

上の拡張リストにおいて、括弧書きで「-scriptingモード時のみ」と書かれている構文は、jjsコマンドをスクリプトモードと呼ばれるモードで使用した場合にのみ有効な拡張構文である。このスクリプトモード用の拡張構文をGraalJSで使用したい場合は、jsコマンドに「--js.scripting」フラグを付ける必要がある。次のコードは、文字列リテラル中で式を記述できる${}構文の使用例である。

var x = "World"
var str = "Hello, ${x}"

print(str)

これを「--js.scripting」フラグなしで実行すると、次のように${}の部分が単に文字列として表示されるだけだ。

$ js --js.syntax-extensions --experimental-options string.js
Hello, ${x}

「--js.scripting」フラグを付ければ、${}内が式として解釈されるようになる。

$ js --js.scripting --js.syntax-extensions --experimental-options string.js
Hello, World

Nashorn独自の機能は拡張構文だけではない。例えば前回も登場した、JavaのパッケージをimportするためのimportPackage()関数も独自機能のひとつである。importPackage()に代表される一部の関数では、Nashornとの互換性を表す「--js.nashorn-compat」フラグが必要になる。

次のコードは、前回も使用したimportPackage()関数を含むスクリプトである。

load("nashorn:mozilla_compat.js");

importPackage("java.time");
var now = LocalDateTime.now();
print("now = " + now);
print("1 day later = " + now.plusDays(1));

importPackage("java.time.temporal");
print("Truncated To Hours = " + now.truncatedTo(ChronoUnit.HOURS));

これをjsコマンドで実行したい場合は、次のように「--js.nashorn-compat」と「--experimental-options」の2つのフラグが必要となる。

$ js --jvm --js.nashorn-compat --experimental-options javadate_import.js
now = 2020-08-25T11:57:29.842625
1 day later = 2020-08-26T11:57:29.842625
Truncated To Hours = 2020-08-25T11:00

List/Mapの要素へのアクセス

Nashornには、java.util.Listオブジェクトの各要素に対して、次の例のように配列の要素と同様にアクセスできる機能がある。

var ArrayList = Java.type("java.util.ArrayList")
var list = new ArrayList()
list.add("Rhino")
list.add("Nashorn")
list.add("GraalJS")
// get()メソッドで要素を取得
print(list.get(0))
print(list.get(1))
print(list.get(2))
// 配列のように要素を取得
print(list[0])
print(list[1])
print(list[2])
// list.lengthで長さを取得
print(list.length); // list.size()

GraalJSでもこの機能は有効で、ほかの拡張機能と同様に「--js.nashorn-compat」と「--experimental-options」の2つのフラグを付けることで実行できる。

$ js --jvm --js.nashorn-compat --experimental-options list.js
Rhino
Nashorn
GraalJS
Rhino
Nashorn
GraalJS
3

java.util.Mapに対しても同様に、各要素にプロパティのようにアクセスできる機能がある。次のコードでは、Mapオブジェクトの要素に対して、「map.js」や「map['js']」のように、キーjsの値にアクセスしている。

var HashMap = Java.type("java.util.HashMap")
var map = new HashMap()
// get()/put()メソッドで要素にアクセス
map.put('js', 'GraalJS')
print(map.get('js'))
// プロパティのように要素にアクセス
print(map['js'])
print(map.js)
// プロパティのように要素を追加
map['language'] = 'java'
print(map.get("language"))
print(map.language)
print(map['language'])

Nashornでは有効なこの機能だが、現時点ではGraalVMではサポートされておらず、次のようにエラーになってしまう。

$ js --jvm --js.nashorn-compat --experimental-options map.js
GraalJS
undefined
undefined
null
undefined
undefined

Javaグローバル・オブジェクトの互換性

上の例で「Java.type」というプロパティを使用したが、これはJavaの型を表すグローバル・オブジェクトである。NashornおよびGraalJSには、このようなJavaとの互換性をサポートするためのグローバル・オブジェクトがいくつか用意されている。ただし、NashornとGraalJSでは、若干だがそのサポート範囲が異なるので注意が必要となる。以下に、NashornとGraalJSそれぞれで用意されているグローバル・オブジェクトを示す。なお、GraalJSで使用する場合にはjsコマンドに「--jvm」フラグが必要となる。

プロパティ Nashorn GraalJS
Java.type
Java.from
Java.to
Java.extend
Java.super
Java.synchronized
Java.asJSONComparible ×
Java.isJavaObject
Java.isType
Java.typeName
Java.isJavaFunction
Java.isScriptObject
Java.isScriptFunction
Java.addToClasspath ×

ここで紹介したように、GraalJSはNashornとの互換性について十分に配慮されており、現行のNashorn用のプロフラムもほぼ問題なく移行できる。ただし、一部の機能はサポートされていなかったり、挙動が異なったりするので注意が必要だ。