Javascript 在Nashorn中的特定上下文中执行函数
我有一个自定义的Nashorn运行时,我用一些全局函数和对象设置了它——其中一些是无状态的,一些是有状态的。在这个运行时,我运行一些自定义脚本 对于每次执行,我都计划创建一个由全局上下文支持的新上下文:Javascript 在Nashorn中的特定上下文中执行函数,javascript,java,concurrency,scope,nashorn,Javascript,Java,Concurrency,Scope,Nashorn,我有一个自定义的Nashorn运行时,我用一些全局函数和对象设置了它——其中一些是无状态的,一些是有状态的。在这个运行时,我运行一些自定义脚本 对于每次执行,我都计划创建一个由全局上下文支持的新上下文: myContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE); engine.eval(myScript, myContext); 根据我读到的内容,对全局范围的任何修改(从脚本的角度来看)都将限于我创建的
myContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(myScript, myContext);
根据我读到的内容,对全局范围的任何修改(从脚本的角度来看)都将限于我创建的新上下文
这些脚本在计算时会公开一些对象(具有定义良好的名称和方法名称)。我可以通过将engine
强制转换为Invocable
来调用对象上的方法。但是我如何知道函数将运行的上下文呢?这甚至是一个问题,还是该函数的执行上下文是基于其评估的上下文设置的
在多线程情况下,所有线程共享同一个脚本引擎实例,并且它们都尝试运行同一个脚本(这会公开一个全局对象),我可以期待什么行为。当我调用对象上的方法时,函数将在哪个上下文中运行?它如何知道要使用哪个对象实例
我希望看到一个invoke
方法,在这里我可以指定上下文,但事实似乎并非如此。有没有办法做到这一点,或者说我完全错了
我知道解决这个问题的一个简单方法是每次执行创建一个新的脚本引擎实例,但据我所知,我将失去优化(尤其是在共享代码上)。也就是说,预编译在这里会有帮助吗?我发现了这一点。我遇到的问题是,
invokeFunction
会抛出一个NoSuchMethodException
,因为自定义脚本公开的函数在引擎默认作用域的绑定中不存在:
ScriptContext context = new SimpleScriptContext();
context.setBindings(nashorn.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(customScriptSource, context);
((Invocable) engine).invokeFunction(name, args); //<- NoSuchMethodException thrown
这将调用新创建的上下文中存在的函数。您还可以通过以下方式调用对象上的方法:
JSObject object = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE);
JSObject method = (JSObject) object.getMember(name);
method.call(object, args);
call
抛出未经检查的异常(包装在RuntimeException
中的Throwable
或已使用JavaScript stackframe信息初始化的NashornException
),因此如果您想提供有用的反馈,可能必须显式处理该异常
这样,线程就不能相互重叠,因为每个线程都有一个单独的上下文。我还能够在线程之间共享自定义运行时代码,并确保自定义运行时公开的可变对象的状态更改通过上下文隔离
为此,我创建了一个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) {
...
}
}
...
}
然后,当我需要执行脚本时,我会评估编译后的源代码,然后根据该上下文评估脚本:
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);
我用多个线程对此进行了测试,我能够确保状态更改仅限于属于各个线程的上下文。这是因为编译后的表示是在特定的上下文中执行的,这意味着它所公开的任何内容的实例都限定在该上下文的范围内
这里的一个小缺点是,对于不需要特定于线程的状态的对象,您可能会不必要地重新评估对象定义。要解决这个问题,请直接在引擎上对它们进行评估,这将为这些对象添加绑定到引擎的引擎范围中
:
public Runtime() {
...
String shared = new Scanner(
this.getClass().getClassLoader().getResourceAsStream("runtime/shared.js")
).useDelimiter("\\Z").next();
try {
...
nashorn.eval(shared);
...
} catch(ScriptException e) {
...
}
}
context.getBindings(ScriptContext.ENGINE_SCOPE).putAll(engine.getBindings(ScriptContext.ENGINE_SCOPE));
随后,您可以从引擎的引擎\u范围
填充特定于线程的上下文:
public Runtime() {
...
String shared = new Scanner(
this.getClass().getClassLoader().getResourceAsStream("runtime/shared.js")
).useDelimiter("\\Z").next();
try {
...
nashorn.eval(shared);
...
} catch(ScriptException e) {
...
}
}
context.getBindings(ScriptContext.ENGINE_SCOPE).putAll(engine.getBindings(ScriptContext.ENGINE_SCOPE));
您需要做的一件事是确保您暴露的任何此类对象都已冻结。否则,可以重新定义或向其添加属性。感谢您抽出时间回答您的问题。非常有帮助!问题是。计算编译后的脚本和源代码有什么区别?在我看来,它是双重的?我是基于此实现的,但忽略了
engine.eval(source,context)
,它似乎工作得很好。@StijndeWitt区别在于预编译的源代码适用于任何维护特定自定义脚本执行状态的库。这并不是真的必要,我认为正确的方法可能是只编写JS库,以便仅在每个实例的基础上维护这种共享状态(即,库本身返回一个实例)。但是如果你只是在运行预编译的自定义脚本,那么第二次评估就没有必要了。就重用具有不同用户脚本执行的共享编译脚本而言,这是迄今为止最好的解决方案。我相信compiledRuntime.eval(context)
只会执行任何预编译的共享代码(如javadoc中所述)。这就是编译脚本的好处所在。如果有人能证实这一点,我将不胜感激。我读到预编译在Nashorn中实际上是不可行的,所以它没有帮助。但是找不到源代码。我可能错了。