ザネリは列車を見送った

ブログという名の備忘録

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 が発生する。