javax.script.ScriptEngine でこういう使い方もできたのか、というメモ。
まずは普通の(?)使い方として、 JavaScript のトップレベル関数を呼ぶにはこうする。
(当たり前だけど)引数や戻り値は全て java.lang.Object でやりとりする。
そこで、今回の話。
Java 側でインターフェースを切って、それをスクリプト言語側で実装するということができる。
まずは普通の(?)使い方として、 JavaScript のトップレベル関数を呼ぶにはこうする。
ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); engine.eval("function increase(num) { return num + 1; }"); System.out.println(((Invocable) engine).invokeFunction("increase", 10));javax.script.Invocable#invokeFunction(String, Object...) は、
(当たり前だけど)引数や戻り値は全て java.lang.Object でやりとりする。
そこで、今回の話。
Java 側でインターフェースを切って、それをスクリプト言語側で実装するということができる。
まず、インターフェースを作ろう。
次に、実装側。JavaScript エンジンは Java6 に標準搭載されている。
Python でもやってみる。Jython 2.5.2 を使用した。
Ruby でもやってみる。JRuby 1.6.7 を使用した。
準備できたので呼んでみよう。
getName() で以下のエラーが発生する。
org.python.core.Py クラスをデコンパイルして見てみると、以下の箇所が原因であるようだ。
その length を取ろうとして NullPointerException が発生しているようだ。
何か回避策は無いかと考えてみたけれど、呼び出し側で try-catch して
javax.script.Invocable.invokeFunction(String) を代わりに呼ぶとか
com.zaneli.script.ScriptService#getName() に引数ありのものを追加してそちらを呼ぶとか
あまり綺麗じゃない方法しか思いつかなかった。
javax.script.Invocable.getInterface(Class) の引数にインターフェースだけじゃなくて抽象クラスも渡せれば
Java 側で何とかできたかもしれないけど。
バグレポートらしきものも見つかった。
[Jython-bugs] [issue1642] Proxy jsr223 Nullpointer no arguments
こちらの対応待ち、ということになるのかな…。
ちなみに Java インターフェースに定義されたメソッドがスクリプト言語側に存在しないと、
javax.script.Invocable#getInterface(Class) でのキャストには成功するものの、
該当メソッド呼び出し時に NoSuchMethodException が発生する。
次に、実装側。JavaScript エンジンは Java6 に標準搭載されている。
Python でもやってみる。Jython 2.5.2 を使用した。
Ruby でもやってみる。JRuby 1.6.7 を使用した。
準備できたので呼んでみよう。
public static void main(String[] args) throws Throwable { ScriptExecutor executor = new ScriptExecutor(); executor.execute("javascript", "serviceImpl.js"); executor.execute("python", "serviceImpl.py"); executor.execute("ruby", "serviceImpl.rb"); } public void execute(String shortName, String filePath) throws Throwable { try { ScriptService service = getService(shortName, filePath); service.echo("hello!"); System.out.println(service.getName()); System.out.println(service.calculate(10)); } catch (UndeclaredThrowableException e) { if (e.getUndeclaredThrowable() != null) { throw e.getUndeclaredThrowable(); } throw e; } } private ScriptService getService(String shortName, String filePath) throws FileNotFoundException, ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName(shortName); engine.eval(new FileReader(filePath)); return ((Invocable) engine).getInterface(ScriptService.class); }それぞれ、
hello! JavaScript Service 11
hello! Python Service 9
hello! Ruby Service 20が出力されれば成功…なんだけど、Jython だけ上手くいかなかった。
getName() で以下のエラーが発生する。
Exception in thread "main" java.lang.NullPointerException at org.python.core.Py.javas2pys(Py.java:1559) at org.python.jsr223.PyScriptEngine$1.invoke(PyScriptEngine.java:154)引数の無いメソッドを呼んでいるのが発生条件っぽい。
org.python.core.Py クラスをデコンパイルして見てみると、以下の箇所が原因であるようだ。
public static transient PyObject[] javas2pys(Object objects[]) { PyObject objs[] = new PyObject[objects.length]; for(int i = 0; i < objs.length; i++) objs[i] = java2py(objects[i]); return objs; }引数が無い場合、どうやら objects[] が null で、
その length を取ろうとして NullPointerException が発生しているようだ。
何か回避策は無いかと考えてみたけれど、呼び出し側で try-catch して
javax.script.Invocable.invokeFunction(String) を代わりに呼ぶとか
com.zaneli.script.ScriptService#getName() に引数ありのものを追加してそちらを呼ぶとか
あまり綺麗じゃない方法しか思いつかなかった。
javax.script.Invocable.getInterface(Class) の引数にインターフェースだけじゃなくて抽象クラスも渡せれば
Java 側で何とかできたかもしれないけど。
バグレポートらしきものも見つかった。
[Jython-bugs] [issue1642] Proxy jsr223 Nullpointer no arguments
こちらの対応待ち、ということになるのかな…。
ちなみに Java インターフェースに定義されたメソッドがスクリプト言語側に存在しないと、
javax.script.Invocable#getInterface(Class) でのキャストには成功するものの、
該当メソッド呼び出し時に NoSuchMethodException が発生する。