Neige
本帖最后由 Neige 于 2022-7-30 11:46 编辑

如果你在插件中使用过js,你应该对预编译提升性能有些许认知。
一段js文本经过nashorn引擎编译,成为CompiledScript。

你可以对它做很多事情,比如通过CompiledScript#eval()直接调用这段脚本,比如通过CompiledScript#eval(ScriptContext context)在新的上下文调用这段脚本,比如通过CompiledScript#eval(Bindings bindings)在新的绑定关系中调用这段脚本。

CompiledScript#eval()
CompiledScript#eval(ScriptContext context)
CompiledScript#eval(Bindings bindings)
这三种方法都应该在什么情况下使用呢?
以一段实际脚本为例:
  1. function main() {
  2.     return 1
  3. }
  4. main()
复制代码
这段脚本调用后返回值是1,只需要用CompiledScript#eval()调用即可。
那么再看另一段脚本:
  1. function main() {
  2.     return num
  3. }
  4. main()
复制代码
这段脚本将返回num变量,可是我们从来没有定义过num变量。因此,直接使用CompiledScript#eval()调用是会报错的。
这时候我们有三种解决方案:
  1. // 你编译脚本时使用的那个ScriptEngine
  2. scriptEngine.put("num", 1)
  3. compiledScript.eval()
复制代码
  1. SimpleScriptContext context = new SimpleScriptContext()
  2. context.setAttribute("num", 1, ScriptContext.ENGINE_SCOPE)
  3. compiledScript.eval(context)
复制代码
  1. SimpleBindings bindings = new SimpleBindings()
  2. bindings.put("num", 1)
  3. compiledScript.eval(bindings)
复制代码
这三种方法都可以让脚本解析值正常返回1。
那么效率呢?
  1. // 万次平均100ms
  2. compiledScript.eval()
复制代码
  1. // 万次平均85ms
  2. compiledScript.eval(context)
复制代码
  1. // 万次平均65ms
  2. compiledScript.eval(bindings)
复制代码
另外,值得一提的是,第一种方法是非线程安全的。
ScriptEngine#put是在脚本引擎的默认上下文定义变量。
如果你有多个线程同时进行这个操作,就可能先后覆盖,导致数据出错。
由此得出结论:单纯需要传入变量后执行脚本,使用bindings是一个不错的选择。

但实际情况往往更加复杂。
以如下脚本为例:
  1. function sum() {
  2.     return num1 + num2
  3. }
  4. function shit() {
  5.     return num1 * 1000
  6. }
复制代码
这一段脚本中有两个函数:sum和shit
在实际应用中,我们肯定希望能够自由调用指定函数。
幸好nashorn了解我们的需求,已经给了对应的方法。
  1. compiledScript.eval()
  2. (scriptEngine as Invocable).invokeFunction("函数名")
复制代码
每个CompiledScript都由ScriptEngine编译,你eval一个CompiledScript实际上是在对应的ScriptEngine中执行这段脚本。
首先输入compiledScript.eval()是为了让这段脚本载入引擎。
然后,将ScriptEngine转为Invocable,即可通过invokeFunction调用相关方法。
你也可以通过invokeFunction("函数名", 参数1, 参数2, ...)向调用的函数传入参数。
不知道你是怎么想的,我当初看到这个方法,心里充满了疑惑。
这种方式,需要先将脚本载入引擎。这就说明,内容全存在引擎里。
而引擎此时没有任何同步锁,所以在这时使用该引擎解析其他脚本,其他脚本中的重名变量/函数就会进行覆盖。
因此,对于可以预编译的脚本来说,一个脚本一个引擎是相对合理的做法
这可以有效防止脚本间同名变量的相互覆盖。

但此时新的问题出现了。对于这种执行指定方法的情况,如果你要传入顶级变量,该如何操作?
invokeFunction执行了脚本中的某个方法,而你无法确保这个方法是上下文无关的。
比如你调用了a方法,而a方法内部调用了b方法,你想要正确调用a方法,就只能在当前环境调用。
因此你无法在invokeFunction时传入新的context或bindings。
你只能在invokeFunction前将变量put进ScriptEngine,再进行函数调用。
于是不可避免的,将导致线程安全问题。
以刚才的那段脚本为例:
  1. function sum() {
  2.     return num1 + num2
  3. }
  4. function shit() {
  5.     return num1 * 1000
  6. }
复制代码
你在多个线程传入不同的num1和num2,调用sum函数并获取返回值,最后会发现结果并不准确。
因为不断传入的num1和num2会进行前后覆盖,导致你实际调用的与你传入的内容并不一致。
这种情况该如何解决呢?

用户@Glom_ 尝试通过“引擎池”解决。
多个引擎编译同一个脚本,调用前向引擎池发送请求,获取引擎后进行变量设置与函数调用。
经过两天的努力,白给了()
这种方式会消耗较多的资源,且实际应用过程中会产生各种奇葩问题(比如顶级变量不通用,手动同步存在线程安全问题)。

最后我以js原型链的思想解决了该问题。
具体代码如下:
  1. // 载入这段脚本
  2. scriptEngine.eval("""
  3.     function sum() {
  4.         // 这种情况下只能使用this.XXX调用传入的变量
  5.         return this.num1 + this.num2
  6.     }
  7.     function shit() {
  8.         return this.num1 * 1000
  9.     }
  10. """)
  11. // 进行特殊处理
  12. scriptEngine.eval("""
  13.     // 建立一个类
  14.     function NeigeItemsNumberOne() {}
  15.     // 让这个类的原型指向当前全局对象
  16.     NeigeItemsNumberOne.prototype = this
  17.     // 调用这个函数可以返回一个新的对象
  18.     function newObject() { return new NeigeItemsNumberOne() }
  19. """)
复制代码
然后具体调用过程可以为
  1. // ScriptObjectMirror就是js对象在nashorn中的实现
  2. val newObject: ScriptObjectMirror = (compiledScript.scriptEngine as Invocable).invokeFunction("newObject") as ScriptObjectMirror
  3. // 设置对象属性
  4. newObject["num1"] = 1
  5. newObject["num2"] = 2
  6. // 调用对象方法
  7. return newObject.callMember("sum")
复制代码
这种方法,在你每次调用函数前,基于原型链新建了一个对象,将你要传入的变量设置为对象属性。
对象中的方法自然可以调用对象属性,只要通过this.XXX就可以调用了。因为每次都新建了一个新的对象,因此每次的操作都是互相隔离,绝对安全的。
而且,这种方式拥有极高的性能。
还记得一开始的性能测试吗?
  1. // 万次平均100ms
  2. compiledScript.eval()
复制代码
  1. // 万次平均85ms
  2. compiledScript.eval(context)
复制代码
  1. // 万次平均65ms
  2. compiledScript.eval(bindings)
复制代码
而执行同样的函数,这种方式的耗时是:
万次8ms

Glom_
引擎池 寄

第一页 上一页 下一页 最后一页