Search code examples
javascriptengine

Java code cannot invoke method from scriptengine with new context


I am trying to implement example invoking method from Javascript in Java.

private static final String JS = "function doit(p) { list.add(p); return true; }";

public static void main(String[] args) throws ScriptException, NoSuchMethodException {
    List<String> list = new ArrayList<>();


    ScriptEngineManager scriptManager = new ScriptEngineManager();
    ScriptEngine engine = scriptManager.getEngineByName("nashorn");

    ScriptContext context = new SimpleScriptContext();
    context.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
    Bindings scope = context.getBindings(ScriptContext.ENGINE_SCOPE);

    scope.put("list", list);
    engine.eval(JS, context);

    Invocable invocable = (Invocable) engine;
    invocable.invokeFunction("doit", "Hello!!!");

    System.out.println(list.size());
}
}

This code throws exception:

Exception in thread "main" java.lang.NoSuchMethodException: No such function doit
    at jdk.nashorn.api.scripting.ScriptObjectMirror.callMember(ScriptObjectMirror.java:204)
    at jdk.nashorn.api.scripting.NashornScriptEngine.invokeImpl(NashornScriptEngine.java:383)
    at jdk.nashorn.api.scripting.NashornScriptEngine.invokeFunction(NashornScriptEngine.java:190)
    at testjavascriptinteraction.TestJavaScript.main(TestJavaScript.java:32)

Version

java -version
openjdk version "1.8.0_91"
OpenJDK Runtime Environment (build 1.8.0_91-8u91-b14-3ubuntu1~16.04.1-b14)
OpenJDK 64-Bit Server VM (build 25.91-b14, mixed mode)

What is wrong?


Solution

  • I can reproduce this problem, and have found a way how to solve it.

    I had a hunch that after casting to Invocable, the engines own scripting context is still being used, and not the temporary one you have passed to eval.

    I can fix this by calling engine.setContext(...) with this context before casting to Invacable. i.e.:

    ...
    scope.put("list", list);
    engine.eval(JS, context); // 'context' now contains the 'doit' function.
    
    engine.setContext(context); // <-- Right here
    Invocable invocable = (Invocable) engine; // Now the Invocable will use 'context'.
    invocable.invokeFunction("doit", "Hello!!!"); //Runs fine
    
    System.out.println(list.size());
    

    The fact that this works seems to confirm my hunch.