前言
由于业务需要,在项目里大量使用了Nashorn引擎,目的是将动态计算公式放到JavaScript里面,方便业务动态添加、修改,在使用过程中遇到的不少坑
Nashorn JavaScript 引擎是 Java 8 的一部分, Nashorn 扩展了 Java 在 JVM 上运行动态 JavaScript 脚本的能力。
编译效率问题
JDK版本为1.8.0_144
在实际应用中,我们是js代码基本不变,传入调用的参数,可以采用预编译来加速
// Nashorn JavaScript是Java 8的一部分,扩展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);";
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);
这样的结果是完全正确的
本地与生产的不同表现
相关逻辑上线后,在生产环境初始表现稳定,但在后期跟踪发现,偶尔会发生异常,重试后问题解决,频率为一天两三次,错误占比大约百分之一,在验证很多猜想后,想到了不同的JDK版本,经查生产JDK版本为1.8.0_45,而我本地JDK版本为1.8.0_144,于是下载JDK1.8.0_45验证
异常为
javax.script.ScriptException: ReferenceError: "$1007" is not defined in at line number 4
at jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:455)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:439)
at jdk.nashorn.api.scripting.NashornScriptEngine.access$200(NashornScriptEngine.java:70)
at jdk.nashorn.api.scripting.NashornScriptEngine$3.eval(NashornScriptEngine.java:495)
at javax.script.CompiledScript.eval(CompiledScript.java:92)
模拟实际应用场景
JDK版本为1.8.0_144
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
System.out.println(factory.getEngineVersion());
String[] params = new String[]{"--global-per-engine"};
ScriptEngine scriptEngine = factory.getScriptEngine(params);
CompiledScript compile = ((Compilable) scriptEngine).compile("function eval1024(a, b, c) {return a + b + c;} eval1024($1001, $1002, $1003);");
CompiledScript compile1 = ((Compilable) scriptEngine).compile("function eval1033(a, b, c) {return a + b + c;} eval1033($1001, $1002, $1004);");
System.out.println(factory.getParameter("THREADING"));
System.out.println(factory.getEngineVersion());
System.out.println(factory.getLanguageName());
System.out.println(factory.getLanguageVersion());
ExecutorService pool = Executors.newFixedThreadPool(10);
AtomicInteger atomicInteger = new AtomicInteger();
AtomicInteger atomicInteger1 = new AtomicInteger();
for (int i = 0; i < 2048; i++) {
final int v = i;
pool.submit(() -> {
if (v % 2 == 0) {
Bindings bindings = new SimpleBindings();
bindings.put("$1003", v + 2);
bindings.put("$1001", v);
bindings.put("$1002", v + 1);
try {
Double eval = (Double) compile.eval(bindings);
if (eval == (3 * v + 3)) {
atomicInteger.incrementAndGet();
} else {
System.out.println("FAILED 1");
}
} catch (ScriptException e) {
e.printStackTrace();
}
} else {
Bindings bindings1 = new SimpleBindings();
bindings1.put("$1004", v + 2);
bindings1.put("$1001", v);
bindings1.put("$1002", v + 1);
try {
Double eval = (Double) compile1.eval(bindings1);
if (eval == (3 * v + 3)) {
atomicInteger1.incrementAndGet();
} else {
System.out.println("FAILED 2");
}
} catch (ScriptException e) {
e.printStackTrace();
}
}
});
}
pool.shutdown();
try {
pool.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("total 1 true : " + atomicInteger.get());
System.out.println("total 2 true : " + atomicInteger1.get());
输出结果为
1.8.0_144
null
1.8.0_144
ECMAScript
ECMA - 262 Edition 5.1
total 1 true : 1024
total 2 true : 1024
结果一切正常
相同的代码,JDK版本为1.8.0_45
输出结果为
null
1.8.0_45
ECMAScript
ECMA - 262 Edition 5.1
FAILED 1
FAILED 2
FAILED 1
javax.script.ScriptException: ReferenceError: "$1003" is not defined in <eval> at line number 1
at jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:455)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:439)
at jdk.nashorn.api.scripting.NashornScriptEngine.access$200(NashornScriptEngine.java:70)
at jdk.nashorn.api.scripting.NashornScriptEngine$3.eval(NashornScriptEngine.java:495)
at javax.script.CompiledScript.eval(CompiledScript.java:92)
.......
total 1 true : 1003
total 2 true : 1013
不仅有ScriptException的ReferenceError错误,还有并发问题,即使使用function函数,在此版本下依然有并发问题存在。
在Java的bug提交中

由此可见在JDK8的部分版本中存在此问题,升级版本即可解决
总结
- 使用预编译提高Nashorn执行JS的性能
- 使用--global-per-engine参数,共享上下文提高性能
- 使用function定义JS代码,做到上下文无关,解决并发问题
- 注意不同版本,高并发下执行导致的ScriptException:ReferenceError
~~以上为笔者个人理解,由于水平有限,如有疏漏,望多多指教 ~~