项目里面涉及到根据具体值计算逻辑表达式或者计算表达式的结果,所以使用了Nashorn引擎,近期发现执行性能比较低,最后经过一段时间的查阅,最终解决了问题,形成了这篇文章。
Nashorn JavaScript 引擎是 Java 8 的一部分, Nashorn 扩展了 Java 在 JVM 上运行动态 JavaScript 脚本的能力。
在实际应用中,我们是表达式基本不变,传入调用的参数,可以采用预编译来加速。
实际操作中发现不使用预编译也可以,因为开启共享上下文之后会缓存最近的预编译模板。
ScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine scriptEngine = factory.getScriptEngine();
// 编写js代码
String script = "var a = x + 1; " +
" var b = y * 2 + 3; " +
" var c = a + b; " +
" c;" ;
//预编译
final CompiledScript compiled = ((Compilable)scriptEngine).compile(script);
调用
Bindings bindings = new SimpleBindings();
bindings.put("x", 1);
bindings.put("y", 2);
System.out.println(compiled.eval(bindings));
这种方法在服务器压力比较大的时候,CPU使用高,效率还是慢,经过追查,发现大量时间浪费在JDK初始化Nashorn的执行上下文,查看Nashorn的源码,里面提供了共享上下文的方法
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
String[] params = new String[]{"--global-per-engine"};
ScriptEngine scriptEngine = factory.getScriptEngine(params);
就是这个初始化参数--global-per-engine
见源码
//初始化参数由args传入
NashornScriptEngine(NashornScriptEngineFactory factory, String[] args, final ClassLoader appLoader, final ClassFilter classFilter) {
assert args != null : "null argument array";
this.factory = factory;
final Options options = new Options("nashorn");
options.process(args);
final ErrorManager errMgr = new ThrowErrorManager();
this.nashornContext = (Context)AccessController.doPrivileged(new PrivilegedAction<Context>() {
public Context run() {
try {
return new Context(options, errMgr, appLoader, classFilter);
} catch (RuntimeException var2) {
if (Context.DEBUG) {
var2.printStackTrace();
}
throw var2;
}
}
}, CREATE_CONTEXT_ACC_CTXT);
//初始化参数传入--global-per-engine,则此处为true,否则为false
this._global_per_engine = this.nashornContext.getEnv()._global_per_engine;
this.global = this.createNashornGlobal(this.context);
this.context.setBindings(new ScriptObjectMirror(this.global, this.global), 100);
使用之一为
private Global getNashornGlobalFrom(ScriptContext ctxt) {
// 为true,则直接返回共享的上下文
if (this._global_per_engine) {
return this.global;
} else {
Bindings bindings = ctxt.getBindings(100);
if (bindings instanceof ScriptObjectMirror) {
Global glob = this.globalFromMirror((ScriptObjectMirror)bindings);
if (glob != null) {
return glob;
}
}
Object scope = bindings.get("nashorn.global");
if (scope instanceof ScriptObjectMirror) {
Global glob = this.globalFromMirror((ScriptObjectMirror)scope);
if (glob != null) {
return glob;
}
}
ScriptObjectMirror mirror = this.createGlobalMirror(ctxt);
bindings.put("nashorn.global", mirror);
return mirror.getHomeGlobal();
}
}
带来的并发问题
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
String[] params = new String[]{"--global-per-engine"};
ScriptEngine scriptEngine = factory.getScriptEngine(params);
String script = " var a = x + 1; " +
" var b = y + 3; " +
" var c = a + b; " +
" c;";
final CompiledScript compiled = ((Compilable) scriptEngine).compile(script);
ExecutorService pool = Executors.newFixedThreadPool(10);
AtomicInteger integer = new AtomicInteger();
for (int i = 0; i < 1024; i++) {
final int v = i;
pool.submit(() -> {
Bindings bindings = new SimpleBindings();
bindings.put("x", v);
bindings.put("y", 2 * v);
try {
Double d = (Double) compiled.eval(bindings);
if (d == (3 * v + 4)) {
integer.incrementAndGet();
}
} catch (ScriptException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
try {
pool.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("total true : " + integer);
多线程模拟计算,发现部分结果(对比Java代码计算和JS代码计算)为错误的,这是因为上下文是相关的,JS中三个上下文相关变量为a、b、c,在多线程的执行下,会相互覆盖。
解决并发问题
使用function函数或者函数表达式来保证JS中的变量是上下文无关
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
System.out.println(factory.getEngineVersion());
String[] params = new String[]{"--global-per-engine"};
ScriptEngine scriptEngine = factory.getScriptEngine(params);
//使用function
String script = "function eval(a, b) {return a + b + 4;} eval(x, y);";
//或 String script = "x + y + 4";
final CompiledScript compiled = ((Compilable) scriptEngine).compile(script);
ExecutorService pool = Executors.newFixedThreadPool(10);
AtomicInteger integer = new AtomicInteger();
for (int i = 0; i < 1024; i++) {
final int v = i;
pool.submit(() -> {
Bindings bindings = new SimpleBindings();
bindings.put("x", v);
bindings.put("y", 2 * v);
try {
Double d = (Double) compiled.eval(bindings);
if (d == (3 * v + 4)) {
integer.incrementAndGet();
}
} catch (ScriptException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
try {
pool.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("total true : " + integer);
这样的结果是完全正确的。
在我的机器上执行效率是之前的写法(没有加--global-per-engine)的10倍左右。
总结
- 使用预编译提高Nashorn执行JS的性能
- 使用--global-per-engine参数,共享上下文提高性能
- 使用function定义JS代码,做到上下文无关,解决并发问题
- 注意不同版本,高并发下执行导致的ScriptException:ReferenceError