在Nashorn中的特定上下文中执行函数

Viv*_*ath 6 javascript java concurrency scope nashorn

我有一个自定义Nashorn运行时,我设置了一些全局函数和对象 - 其中一些是无状态的,其中一些是有状态的.针对此运行时,我正在运行一些自定义脚本.

对于每次执行,我计划创建一个由全局上下文支持的新上下文:

myContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(myScript, myContext);
Run Code Online (Sandbox Code Playgroud)

根据我读到的内容,对全局范围的任何修改(从脚本的角度来看)将仅限于我创建的新上下文.

这些脚本在评估时会公开一些对象(具有明确定义的名称和方法名称).我可以通过强制转换engine来调用对象上的方法Invocable.但是,我如何知道函数运行的上下文?这甚至是一个问题,还是该功能的执行上下文是根据评估它的上下文设置的?

在多线程情况下,我可以期待什么行为,其中所有线程共享相同的脚本引擎实例,并且它们都尝试运行相同的脚本(公开全局对象).当我然后在对象上调用方法时,函数会在哪个上下文中运行?它将如何知道要使用的对象实例?

我期待看到一个invoke方法,我可以指定上下文,但似乎并非如此.有没有办法做到这一点,还是我完全错了?

我知道解决这个问题的一个简单方法是每次执行创建一个新的脚本引擎实例,但据我所知,我会失去优化(特别是在共享代码上).话虽如此,这里会预先编译帮助吗?

Viv*_*ath 11

我想通了.我遇到的问题是invokeFunction抛出一个NoSuchMethodException因为自定义脚本公开的函数在引擎默认范围的绑定中不存在:

ScriptContext context = new SimpleScriptContext();
context.setBindings(nashorn.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(customScriptSource, context);
((Invocable) engine).invokeFunction(name, args); //<- NoSuchMethodException thrown
Run Code Online (Sandbox Code Playgroud)

所以我要做的就是按名称从上下文中提取函数并像这样明确地调用它:

JSObject function = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE);
function.call(null, args); //call to JSObject#isFunction omitted brevity 
Run Code Online (Sandbox Code Playgroud)

这将调用新创建的上下文中存在的函数.您还可以通过以下方式调用对象上的方法:

JSObject object = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE);
JSObject method = (JSObject) object.getMember(name);
method.call(object, args);
Run Code Online (Sandbox Code Playgroud)

call抛出一个未经检查的异常(Throwable包含在一个RuntimeExceptionNashornException已用JavaScript堆栈框架信息初始化的异常),因此如果您想提供有用的反馈,则可能必须明确处理该异常.

这样线程就不会互相跳过,因为每个线程都有一个单独的上下文.我还能够在线程之间共享自定义运行时代码,并确保自定义运行时公开的可变对象的状态更改被上下文隔离.

为此,我创建了一个CompiledScript包含自定义运行时库的编译表示的实例:

public class Runtime {

    private ScriptEngine engine;
    private CompiledScript compiledRuntime;

    public Runtime() {
        engine = new NashornScriptEngineFactory().getScriptEngine("-strict");
        String source = new Scanner(
            this.getClass().getClassLoader().getResourceAsStream("runtime/runtime.js")
        ).useDelimiter("\\Z").next();

        try {
            compiledRuntime = ((Compilable) engine).compile(source);
        } catch(ScriptException e) {
            ...
        }
    }

    ...
}
Run Code Online (Sandbox Code Playgroud)

然后当我需要执行脚本时,我会评估编译后的源代码,然后根据该上下文评估脚本:

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

//Exception handling omitted for brevity

//Evaluate the compiled runtime in our new context
compiledRuntime.eval(context); 

//Evaluate the source in the same context
engine.eval(source, context);

//Call a function
JSObject jsObject = (JSObject) context.getAttribute(function, ScriptContext.ENGINE_SCOPE);
jsObject.call(null, args);
Run Code Online (Sandbox Code Playgroud)

我用多个线程测试了这个,我能够确保状态更改仅限于属于单个线程的上下文.这是因为编译的表示在特定的上下文中执行,这意味着由它公开的任何事件的实例都限定在该上下文中.

这里的一个小缺点是,您可能不必要地重新评估不需要具有线程特定状态的对象的对象定义.要解决这个问题,请直接在引擎上进行评估,这会将这些对象的绑定添加到引擎中ENGINE_SCOPE:

public Runtime() {
    ...
    String shared = new Scanner(
        this.getClass().getClassLoader().getResourceAsStream("runtime/shared.js")
    ).useDelimiter("\\Z").next();

    try {
        ...        
        nashorn.eval(shared);
        ...
    } catch(ScriptException e) {
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以从引擎中填充特定于线程的上下文ENGINE_SCOPE:

context.getBindings(ScriptContext.ENGINE_SCOPE).putAll(engine.getBindings(ScriptContext.ENGINE_SCOPE));
Run Code Online (Sandbox Code Playgroud)

您需要做的一件事是确保您公开的任何此类对象都已冻结.否则,可以重新定义或添加属性.

  • 感谢您抽出宝贵时间回答自己的问题.很有帮助! (2认同)