Java インターフェースをスクリプト言語で実装する
javax.script.ScriptEngine でこういう使い方もできたのか、というメモ。
まずは普通の(?)使い方として、 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を使用した。
準備できたので呼んでみよう。
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 が発生する。