Java8 からは Rhino に変わって Nashorn を使って JavaScript を実行することができます。 Binding 機能を使えば、JavaScript から Java 側で作成した自前のオブジェクト利用することも簡単です。
そこで、この機能を使っていろいろと実験をしているのですが、少し困った問題が出てきました。 Java の ArrayList オブジェクトを Nashorn に Binding して使う場合、 こちらとしては、JavaScript の Array オブジェクトと同じように振る舞ってほしいのですが、 それとは微妙に作動が違うのです。
これを解決するためにいろいろ方法を探したのですが、どうやらAbstractJSObject を使って、 JavaScript の Array オブジェクトと同じように振る舞うオブジェクトを実装すればよいようです。
そのあたりで得たことを整理したので、このエントリーでシェアします。
ここで使用している言語は 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);
次に 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
うまくいきました。
なかなか良い感じなのですが、困るのは 存在しない要素にアクセスしたときの振る舞いの違い です。 たとえば…
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
これを実現する方法を いろいろ調べてみると、どうやら AbstractJSObject を使えばよいようです。
AbstractJSObject はマップにも配列(Array)にもなれる万能オブジェクトのようです。 そのあたりの詳しいことは AbstractJSObject などでググって調べていただくこととして、 ここでは、JavaScript の Array っぽいオブジェクトを Java 側で実装して使うことだけにフォーカスしてみます。
MyArrayList オブジェクトを AbstractJSObject のサブクラスとしてこのように実装することで…
そんなオブジェクトを実装してみます。
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 の実装です。
AbstractJSObject があれば、どんなオブジェクトでも表現できそうですが、自由すぎて私にはメンテナンスできそうにありません。