优化Nashorn引擎调用性能,解决部分JDK版本的ScriptException:ReferenceError

4,576 阅读4分钟

前言

由于业务需要,在项目里大量使用了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提交中

链接bugs.java.com/bugdatabase…

由此可见在JDK8的部分版本中存在此问题,升级版本即可解决

总结

  1. 使用预编译提高Nashorn执行JS的性能
  2. 使用--global-per-engine参数,共享上下文提高性能
  3. 使用function定义JS代码,做到上下文无关,解决并发问题
  4. 注意不同版本,高并发下执行导致的ScriptException:ReferenceError

~~以上为笔者个人理解,由于水平有限,如有疏漏,望多多指教 ~~