読者です 読者をやめる 読者になる 読者になる

Java の上の JavaScript エンジン Nashorn の基本


f:id:Naotsugu:20170220190122p:plain

Nashorn とは

Java7 までは JavaScript スクリプティングエンジンとして Rino が同梱されていましたが、Java8 からは Nashorn が同梱されるようになりました。 ナースホルン(Nashorn)はドイツ語でサイを意味します。

Nashorn は ECMAScript-262 に準拠しており、JSR 292(Supporting Dynamically Typed Languages on the Java Platform) を利用することでランタイム性能が大きく改善されています(java.lang.invoke.CallSite を使い invokedynamic で処理される)。上の画像はベンチマーク結果です。初回は遅いですが、それ以降は大きく改善していることが分かります。

また、Java9 からは ES6 対応されるので、今後さらに使いやすくなるものと思われます。

Java から Nashorn を使う

Java6 で導入された ScriptEngin を使います。

ScriptEngineManager manager = new ScriptEngineManager()
ScriptEngine engine = manager.getEngineByName("nashorn");

Object result = engine.eval("'Nashorn'.length");
System.out.println(result); // 7

engine.eval() にスクリプトコードを渡すことでスクリプトが実行され、実行結果が取得できます。

Reader を渡して実行することもできます。

Path path = Paths.get("/path/to/script/script.js");
Object result = engine.eval(Files.newBufferedReader(path));

Java オブジェクトを渡す

Java オブジェクトは engine に put することで JavaScript 側で使うことができます。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

engine.put("path", Paths.get("example.txt"));
Object result = engine.eval("path.fileName");
System.out.println(result); // example.txt

put で名前を付けて登録することで、JavaScript 側で利用できます。 path.getFileName() の呼び出しをプロパティ構文を使い path.fileName で呼ぶことができます。

Bindings を使い、スコープを指定することもできます。

Bindings bindings = engine.getBindings(ScriptContext.GLOBAL_SCOPE);
bindings.put("path", Paths.get("example.txt"));

ScriptContext.GLOBAL_SCOPE を指定した場合は ScriptEngineManager (正確には ScriptEngineFactory) で共有され、GLOBAL_SCOPE ENGINE_SCOPE を指定した場合は ScriptEngine で共有されます。

最初の例の engine.put() では ScriptContext.ENGINE_SCOPE となります。

JavaScript の関数を Java から呼び出す

Invocable.invokeFunction() を使います。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

engine.eval("function hello(name) { print('Hello, ' + name) }");
Invocable inv = (Invocable) engine;
inv.invokeFunction("hello", "Nashorn");
// Hello, Nashorn

invokeFunction の第一引数には関数名、第二引数以降に可変長で関数のパラメータを指定します。

JavaScript のオブジェクトを Java から呼び出す

同じような例として、Invocable.invokeMethod() というものもあります。

engine.get() により JavaScript のオブジェクトを Java 側に公開して実行できます。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

engine.eval("var obj = new Object()");
engine.eval("obj.hello = function(name) { print('Hello, ' + name) }");

Object obj = engine.get("obj");

Invocable inv = (Invocable) engine;
inv.invokeMethod(obj, "hello", "Nashorn");
// Hello, Nashorn

engine.get() により jdk.nashorn.api.scripting.ScriptObjectMirror が取得されます。 これを inv.invokeMethod() の第一引数に指定して JavaScript オブジェクトのメソッドが呼べます。

JavaScript 内で Java クラスを利用する

JavaScript 内で Java オブジェクトを作成して利用することができます。

JavaScript 側では java, javax, com, org, edu で始まるJavaパッケージはそのまま使えます。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

engine.eval("var decimal = new java.math.BigDecimal(100)");
engine.eval("print(decimal.intValue())");
// 100

上記以外のパッケージの任意のクラスは Java.type() でアクセスできます。

engine.eval("var Employee = Java.type('jp.co.xx.Employee')");
engine.eval("var e1 = new Employee()");
engine.eval("print(e1.toString())");
// jp.co.xx.Employee@64c87930

Java.type() で内部クラスを利用する場合は ‘java.util.AbstractMap$SimpleEntry’ のように $ を使います。 Java.type() を使わない場合は単純に java.util.AbstractMap.SimpleEntry でアクセスできます。

JavaScript と Java の型

文字列

JavaScript の文字列と Java の文字列は透過的に変換されます。

JavaScript 文字列に対して Java 文字列のメソッド compareTo を呼べます。

'Hello'.compareTo('World');

数値

JavaScript の Number 型は Java の double 型に対応します。

JavaScript 内で、int や long を期待する Java オブジェクトのメソッドに数値を渡した場合、小数点以下が自動的に切り捨てられます。

配列

Java の配列を javaScript 側で作るには Java.type() を使います。

var javaIntArray = Java.type('int[]');
var javaNumbers = new javaIntArray(10);

var javaStringArray = Java.type('java.lang.String[]');
var javaItems = new javaStringArray(10);

JavaScript の配列をJavaの配列に変換するには Java.to() を使います。

int javaStringArray = Java.type('java.lang.String[]');
var javaItems = Java.to(jsItems, javaStringArray);

反対に Java の配列を JavaScript の配列にするには Java.from() を使います。

var jsItems = Java.from(javaItems);

リストとマップ

Java のリストは以下のようなシンタックスシュガーが使えます。

var javaItems = java.util.Arrays.asList('A', 'B', 'C');
var first = javaItems[0];
javaItems[0] = 'a';

マップに対しても同じように使えます。

var javaMap = new java.util.HashMap();
javaMap['A'] = 'a';
var first = javaMap['A'];

ラムダ

ラムダを受ける Java オブジェクトに JavaScript の無名関数を渡せます。

var javaStringArray = Java.type('java.lang.String[]');;
var words = new javaStringArray(2);;
words[0] = 'Apple';
words[1] = 'Orange';

java.util.Arrays.sort(words, function(a, b) { 
    return java.lang.Integer.compare(a.length, b.length)
});

無名関数が単一式の場合は { }return を省略できます。

Java クラス・パッケージのインポート

Java クラスにアクセスする場合は Java.type() を利用するのは前に書きました。

var ArrayList = Java.type("java.util.ArrayList");
var a = new ArrayList;

単純名でアクセスしたい場合には、組込の互換性スクリプト(mozilla_compat.js)の importPackage()importClass()関数でインポートすると単純名でアクセスできます。

load("nashorn:mozilla_compat.js");

importClass(java.awt.Frame);
var frame = new java.awt.Frame("hello");

Object、Boolean、Math など、JavaScriptオブジェクトと競合する場合 JavaImporterオブジェクトを定義して with で使用します。

var Gui = new JavaImporter(java.awt, javax.swing);

with (Gui) {
    var awtframe = new Frame("AWT Frame");
    var jframe = new JFrame("Swing JFrame");
};

JavaScript で Java インターフェースを実装する

JavaScript 側で Java インターフェースのを実装したオブジェクトを生成できます。

Iterator インターフェースの実装は以下のように書くことができます。

var iter = new java.util.Iterator {
    next: function() Math.random(),
    hasNext: function() true
}

FunctionalInterface の場合にはメソッド名を省略することもできます。

var task = new java.lang.Runnable(function() print('hello'));
new java.lang.Thread(task).start();

クラスオブジェクトを Java.extend() で定義することもできます。

var RandomIterator = Java.extend(java.util.Iterator, {
    next: function() Math.random(),
    hasNext: function() true
});

var itre = new RandomIterator();

Java.extend() を使うと複数インターフェースを実装したクラスを定義することもできます。

JavaScript 関数を Java インターフェースの実装として利用する

Invocable.getInterface() により、JavaScript 関数を Java側でインターフェースの実装として使うことができます。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

engine.eval("function run() { print('run() function called') }");

Invocable inv = (Invocable) engine;
Runnable r = inv.getInterface(Runnable.class);

Runnable インターフェースの持つ run() と一致するスクリプト関数が呼び出されます。

スクリプトがオブジェクトのメソッドの場合には以下のようにします。

engine.eval("var obj = new Object()")
engine.eval("obj.run = function() { print('obj.run() method called') }");

Object obj = engine.get("obj");
Invocable inv = (Invocable) engine;
Runnable r = inv.getInterface(obj, Runnable.class);

スクリプトを事前コンパイルする

Compilable.compile() でスクリプトを事前にコンパイルしておける。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

Compilable compilingEngine = (Compilable) engine;
CompiledScript cscript = compilingEngine.compile("print('Hello')");

cscript.eval();

Nashorn で REPL する

jjs というコマンドラインツールで REPL(read-eval-print-loop) できます。

バージョンを出力してみましょう。

$ jjs -version
nashorn 1.8.0_102

スクリプトを簡単に試すことができます。

$ jjs
jjs> print('Hello!')
Hello!

スクリプトから Java の呼び出しも行えます。

NTPサービスにアクセスしてレスポンスを出力してみます。

jjs> var scanner = new java.util.Scanner(new java.net.URL('https://ntp-a1.nict.go.jp/cgi-bin/ntp').openStream()).useDelimiter('\\A')
print(scanner.hasNext() ? scanner.next() : "")

<HTML>
<HEAD><TITLE>NTP</TITLE></HEAD>
<BODY>
3695209073.027
</BODY>
</HTML>

${} で括れば文字列へ埋め込みができます。

jjs> print("current time : ${java.time.Instant.now()}")
current time : 2017-01-01T00:00:00.000Z

jjs でシェルコマンドを使う

-scripting オプション付きで起動するとバッククオートでコマンド実行できます。

$ jjs -scripting
jjs> var output = `ls`
jjs> print(output)

doc
work

コマンドの標準出力は $OUT 標準エラー出力は $ERR、終了コードは $EXIT に設定されます。

nashorn をシェルスクリプトとして使う

先程のNTPサービスから時刻取得する例をシェルスクリプトにしてみます。

以下のような script.js ファイルを作成します。

#!/usr/bin/jjs

var stream = new java.net.URL($ARG[0]).openStream();
var scanner = new java.util.Scanner(stream);
scanner.useDelimiter('\\A');
print(scanner.hasNext() ? scanner.next() : "");
exit(0);

1 行目にはシバンとして /usr/bin/jjs を指定しました。 引数は $ARG[0] で受けます。

実行権限を付けた後、以下のように実行できます。

$ ./script.js -- https://ntp-a1.nict.go.jp/cgi-bin/ntp
<HTML>
<HEAD><TITLE>NTP</TITLE></HEAD>
<BODY>
3695212906.683
</BODY>
</HTML>

jjs の場合は、引数の指定に -- が必要です。

例えば

60 秒毎に特定サイトにアクセスしてレスポンスとして200が得られるかを調べるスクリプト。

healthCheck.js などとして、

#!/usr/bin/jjs

// import from Java
var TimerTask = java.util.TimerTask;
var Timer = java.util.Timer;
var URL = java.net.URL;


var timerTask = new TimerTask(function() { 
    var connection = new URL('http://example.com/').openConnection();
    connection.requestMethod = 'GET';
    connection.connect();
    connection.disconnect();
    var code = connection.responseCode;
    
    if (code === 200) {
        print('healthy');
    } else {
        print('Oops..' + code);
    }
});

new Timer().scheduleAtFixedRate(timerTask, 1 * 1000, 60 * 1000);

Java の場合は URL.openConnection() は HttpURLConnection にキャストしないといけませんが、JavaScript は動的なので単に var に入れとけばよいです。

簡単にヘルスチェックスクリプトが書けます。そして Java8 が入っていればどこでも動きます。

まとめ

Nashorn で JavaScript と Java の相互利用について見てきました。 Java が入っていれば、実用的なランタイム性能で簡単に JavaScript が使え、Java の豊富なライブラリも使えるのは良いですね。 Win でも Mac でも Linux でも、慣れ浸しんだ言語で簡単にスクリプトを書くことができるので、シェルスクリプトとしての利用に真価を発揮します。

etc9.hatenablog.com