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

Copyright© 2011-2021 Shunsuke Otani All Right Reserved .