Wednesday, October 11, 2017 / JavaScript, java

Nashorn, AbstractJSObject を使って JS Array っぽいオブジェクトをつくって使う

Java8 からは Rhino に変わって Nashorn を使って JavaScript を実行することができます。 Binding 機能を使えば、JavaScript から Java 側で作成した自前のオブジェクト利用することも簡単です。

そこで、この機能を使っていろいろと実験をしているのですが、少し困った問題が出てきました。 Java の ArrayList オブジェクトを Nashorn に Binding して使う場合、 こちらとしては、JavaScript の Array オブジェクトと同じように振る舞ってほしいのですが、 それとは微妙に作動が違うのです。

これを解決するためにいろいろ方法を探したのですが、どうやらAbstractJSObject を使って、 JavaScript の Array オブジェクトと同じように振る舞うオブジェクトを実装すればよいようです。

そのあたりで得たことを整理したので、このエントリーでシェアします。

Hello World!

ここで使用している言語は Groovy です。

Groovy Version: 2.4.6 JVM: 1.8.0_131 Vendor: Azul Systems, Inc. OS: Mac OS X

手始めに、一番簡単な Nashorn による JavaScript 実行をしてみます。

import javax.script.ScriptEngineManager

def script = '''\
print('Hello World!');
'''

def engine = new ScriptEngineManager().getEngineByName("nashorn")
engine.eval(script);

ArrayList を試す

次に java.util.ArrayList を使ってみます。

import javax.script.ScriptEngineManager

def script = '''\
var arrayList = new java.util.ArrayList();
arrayList.add('a');
arrayList.add('b');
arrayList.add('c');
print( arrayList );
'''

def engine = new ScriptEngineManager().getEngineByName("nashorn")
engine.eval(script);

実行すると結果は

[a, b, c]

となります。

同じことを Binding 機能を使って記述すると以下のようになります。

import javax.script.ScriptEngineManager

def script = '''\
arrayList.add('a');
arrayList.add('b');
arrayList.add('c');
print( arrayList );

for(var i=0; i<arrayList.length; i++){
    print( arrayList[i] );
}
'''

def engine = new ScriptEngineManager().getEngineByName("nashorn")
engine.put('arrayList', new java.util.ArrayList())
engine.eval(script);

不思議なのは、java.util.ArrayList には length プロパティはないはずですが、それが使えたり、

arrayList[i]

のように配列の要素にアクセスできることです。

こうなると push メソッドなども使いたくなりますが、このままではエラーになり使えません。 そこで、 java.util.ArrayList 拡張した自前の MyArrayList クラスをつくり push メソッドを実装してみました。

import javax.script.ScriptEngineManager

def script = '''\
arrayList.push('a');
arrayList.push('b');
arrayList.push('c');
print( arrayList );

for(var i=0; i<arrayList.length; i++){
    print( arrayList[i] );
}
'''

def engine = new ScriptEngineManager().getEngineByName("nashorn")

class MyArrayList extends ArrayList {
    void push(Object item){
        add(item)
    }
}

engine.put('arrayList', new MyArrayList())
engine.eval(script);

実行結果

[a, b, c]
a
b
c

うまくいきました。

JavaScript の Array の場合 存在しない要素にアクセスしてもエラーにはならないが…

なかなか良い感じなのですが、困るのは 存在しない要素にアクセスしたときの振る舞いの違い です。 たとえば…

var list = ['a','b','c'];
print(list[3]);

このような JavaScript を実行しても undefined が返るだけでエラーにはなりません。

しかし、これと同じコード、ただし使う配列は java.util.ArrayList の場合は…

import javax.script.ScriptEngineManager

def script = '''\
print( arrayList[3] );
'''

def engine = new ScriptEngineManager().getEngineByName("nashorn")
engine.put('arrayList', new java.util.ArrayList(['a','b','c']))
engine.eval(script);

実行すると次のようにエラーになってしまう。

Caught: java.lang.IndexOutOfBoundsException: Index: 3, Size: 3
java.lang.IndexOutOfBoundsException: Index: 3, Size: 3

存在しない配列にアクセスしてもエラーにならない Java由来の配列オブジェクトをつくりたい

これを実現する方法を いろいろ調べてみると、どうやら AbstractJSObject を使えばよいようです。

AbstractJSObject はマップにも配列(Array)にもなれる万能オブジェクトのようです。 そのあたりの詳しいことは AbstractJSObject などでググって調べていただくこととして、 ここでは、JavaScript の Array っぽいオブジェクトを Java 側で実装して使うことだけにフォーカスしてみます。

MyArrayList オブジェクトを AbstractJSObject のサブクラスとしてこのように実装することで…

  • 存在しない要素にアクセスしても null を返すだけでエラーにはならない
  • push メソッドが使用可能

そんなオブジェクトを実装してみます。

import javax.script.ScriptEngineManager
import jdk.nashorn.api.scripting.AbstractJSObject

def script = '''\
arrayList.push('a')
arrayList.push('b')
arrayList.push('c')

for(var i=0; i<arrayList.length; i++){
    print( arrayList[i] );
}

print( arrayList[3] );
'''

def engine = new ScriptEngineManager().getEngineByName("nashorn")

class MyArrayList extends AbstractJSObject {
    private List list = []

    @Override
    Object getMember(String name) { 
        if ("push".equals(name)){
            return new AbstractJSObject(){
                @Override Object call(Object thiz, Object... args) { 
                    return MyArrayList.this.list.add(args[0])
                }
                @Override boolean isFunction() {
                    return true
                } 
            }
        }
        if ("length".equals(name)){
            return list.size(); 
        }
        if ("toString".equals(name)){
            return new AbstractJSObject(){
                @Override Object call(Object thiz, Object... args) { 
                    return MyArrayList.this.toString()
                }
                @Override boolean isFunction() {
                    return true
                } 
            }
        }
        return null; 
    }
    @Override Object getSlot(int index) {
        return hasSlot(index) ? list[index] : null
    }
    @Override boolean hasSlot(int slot) {
        return (slot >= 0 && slot < list.size())
    }
    @Override boolean isArray() {
        return true
    }
    @Override String toString() { 
        return list.toString()
    }
}

engine.put('arrayList', new MyArrayList())
engine.eval(script);

ポイントは getMember の実装です。

  • そのオブジェクトのファンクションがコールされると getMember が呼ばれる、ファンクション名を getMember の最初の引数で受け取ることができる
  • getMember が返すオブジェクトは 必要なら AbstractJSObject を使う

まとめ

AbstractJSObject があれば、どんなオブジェクトでも表現できそうですが、自由すぎて私にはメンテナンスできそうにありません。