Java中JS引擎的介绍与使用(nashorn/delight-nashorn-sandbox)

一、被淘汰的Rhino JavaScript引擎

        从JDK6开始,java就引入了对脚本的支持,这里的脚本指的是但非局限于JS这样的非java语言,当时使用的脚本执行引擎是基于Mozilla 的Rhino。该引擎的特性允许开发人员将 JavaScript 代码嵌入到 Java 中,甚至从嵌入的 JavaScript 中调用 Java。此外,它还提供了使用jrunscript从命令行运行 JavaScript 的能力。

Java ScriptEngine优缺点:

  • 优点:可以执行完整的JS方法,并且获取返回值;在虚拟的Context中执行,无法调用系统操作和IO操作,非常安全;可以有多种优化方式,可以预编译,编译后可以复用,效率接近原生Java;所有实现ScriptEngine接口的语言都可以使用,并不仅限于JS,如Groovy,Ruby等语言都可以动态执行。

  • 缺点:无法调用系统和IO操作 ,也不能使用相关js库,只能使用js的标准语法。更新:可以使用scriptengine.put()将Java原生Object传入Context,从而拓展实现调用系统和IO等操作。


二、Nashorn JavaScript 引擎

        从JDK 8开始,Nashorn取代Rhino成为Java的嵌入式JavaScript引擎。Nashorn完全支持ECMAScript 5.1规范以及一些扩展。它使用基于JSR 292的新语言特性,其中包含在JDK 7中引入的invokedynamic,将JavaScript编译成Java字节码。

        nashorn首先编译javascript代码为java字节码,然后运行在jvm上,底层也是使用invokedynamic命令来执行,所以运行速度很给力。

        Nashorn是一个纯编译的JavaScript引擎。它没有用Java实现的JavaScript解释器,而只有把JavaScript编译为Java字节码再交由JVM执行这一种流程,跟Rhino的编译流程类似。

[ JavaScript源码 ] -> ( 语法分析器 Parser ) -> [ 抽象语法树(AST) ir ] -> ( 编译优化 Compiler ) -> [ 优化后的AST + Java Class文件(包含Java字节码) ] -> JVM加载和执行生成的字节码 -> [ 运行结果 ]

1、初识:在java中如何使用 nashorn 引擎

@Test
protected void useNashorn() throws ScriptException {
    ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
    System.out.println("------列出Java 中有哪些脚本引擎-------");
    scriptEngineManager.getEngineFactories().forEach(f -> System.out.println(f.getNames()));

    System.out.println("-------执行一个简单的js脚本--------");
    ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");
    nashorn.eval("print('hello world')");
}

// 执行结果:
------列出Java 中有哪些脚本引擎-------
[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

-------执行一个简单的js脚本--------
hello world

2、如何通过JAVA传参调用JS,再由JS传参调用JAVA函数:

定义一个用来被调用的JAVA方法:

package com.jiguiquan.www.controller;

public class ForJsCall {
    public static String jsCall(String name) {
        return "成功从JS中调用了JAVA函数哦:" + name;
    }
}

编写测试方法:

public static final String JS_STR1 =
        "var MyJavaClass = Java.type('com.jiguiquan.www.controller.ForJsCall');\n" +
        "var result = MyJavaClass.jsCall(name);\n" +
        "print(result);\n";

@Test
protected void callJavaFromJs() throws ScriptException {
    ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
    SimpleBindings simpleBindings = new SimpleBindings();
    simpleBindings.put("name", "吉桂权");
    nashorn.eval(JS_STR1, simpleBindings);
}

// 执行结果如下(过程很容易理解,不需要解释):
成功从JS中调用了JAVA函数哦:吉桂权

3、如果用 nashorn 执行一个纯函数,会直接调用他吗?——不会

@Test
public void test1() throws ScriptException {
    String jsStr1 = "function test(){return 'Hello World'};";
    ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
    System.out.println(nashorn.eval(jsStr1));
}

// 执行结果:
null

那我们应该如何让它正常返回结果呢?

// 方法一,在js脚本中,显式地执行一次test()方法
@Test
public void test1() throws ScriptException {
    String jsStr2 = "function test(){return 'Hello World'}; test();";
    ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
    System.out.println(nashorn.eval(jsStr2));
}

// 方法二:eval()后,将nashorn转为Invocable,以执行它的invokeFunction指定方法接口
@Test
public void test2() throws ScriptException, NoSuchMethodException {
    String jsStr1 = "function test(){return 'Hello World'};";
    ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");

    nashorn.eval(jsStr1);
    System.out.println(((Invocable) nashorn).invokeFunction("test"));
}

// 指定结果相同:
Hello World

其实也容易理解:在我们运行js脚本时候,如果只是一个函数,它只会被编译加载到执行器中,而并不会主动去调用这个函数;

想要函数执行,就必须主动去调用一次!


三、nashorn的sandbox(沙箱)的使用

Java提供脚本支持,这给我们业务提供了便利的同时,也给我们的服务带来了更大的风险,因为如果我们的业务需求是提供执行脚本的接口,那么JS脚本就是由客户端输入,这存在很多的不确定性,存在安全隐患,比如:

  • js代码存在死循环

  • js代码可以操作宿主机上面的功能,删除机器上的文件

  • js执行占用过多的java资源

这个时候sandbox就应运而生了,sandbox的作用就是将JS脚本执行的环境独立出来,达到对java类的访问限制以及对Nashorn引擎的资源限制的目的。

1、要想使用 nashorn 的 sandbox 需要导入对应的依赖:

<dependency>
    <groupId>org.javadelight</groupId>
    <artifactId>delight-nashorn-sandbox</artifactId>
    <version>0.2.5</version>
</dependency>

2、使用时很简单,创建沙箱,配置沙箱参数:

@Test
public void testSandbox() throws ScriptException {
    NashornSandbox sandbox = NashornSandboxes.create();
    sandbox.setMaxCPUTime(100);// 设置脚本执行允许的最大CPU时间(以毫秒为单位),超过则会报异常,防止死循环脚本
    sandbox.setMaxMemory(1024 * 1024); //设置JS执行程序线程可以分配的最大内存(以字节为单位),超过会报ScriptMemoryAbuseException错误
    sandbox.allowNoBraces(false); // 是否不允许使用大括号
    sandbox.allowLoadFunctions(true); // 是否允许nashorn加载全局函数
    sandbox.setMaxPreparedStatements(30); // because preparing scripts for execution is expensive // LRU初缓存的初始化大小,默认为0
    sandbox.setExecutor(Executors.newSingleThreadExecutor());// 指定执行程序服务,该服务用于在CPU时间运行脚本

    String jsStr = "function test(){return 'Hello SandBox'}; ";
    System.out.println(sandbox.eval(jsStr));
}

// 执行结果:
true

可以看到,使用sandbox和直接使用nashorn一样,如果只是eval一个纯方法函数,是不会调用并返回结果的!

但是不同点,在于,如果执行成功,sandbox会返回一个true,而nashorn返回的是null;

3、同样的,如果我们想执行定义的方法,也是有有个办法:

  • 在JS脚本中,显式地调用一次方法:

@Test
public void testSandbox() throws ScriptException {
    NashornSandbox sandbox = NashornSandboxes.create();
    sandbox.setMaxCPUTime(100);// 设置脚本执行允许的最大CPU时间(以毫秒为单位),超过则会报异常,防止死循环脚本
    sandbox.setMaxMemory(1024 * 1024); //设置JS执行程序线程可以分配的最大内存(以字节为单位),超过会报ScriptMemoryAbuseException错误
    sandbox.allowNoBraces(false); // 是否不允许使用大括号
    sandbox.allowLoadFunctions(true); // 是否允许nashorn加载全局函数
    sandbox.setMaxPreparedStatements(30); // because preparing scripts for execution is expensive // LRU初缓存的初始化大小,默认为0
    sandbox.setExecutor(Executors.newSingleThreadExecutor());// 指定执行程序服务,该服务用于在CPU时间运行脚本

    String jsStr = "function test(){return 'Hello SandBox'}; test()";
    System.out.println(sandbox.eval(jsStr));
}

// 执行结果:
Hello SandBox
  • 使用sandbox的Invocable使用invokeFunction执行方法:

@Test
public void testSandbox2() throws ScriptException, NoSuchMethodException {
    NashornSandbox sandbox = NashornSandboxes.create();
    sandbox.setMaxCPUTime(100);// 设置脚本执行允许的最大CPU时间(以毫秒为单位),超过则会报异常,防止死循环脚本
    sandbox.setMaxMemory(1024 * 1024); //设置JS执行程序线程可以分配的最大内存(以字节为单位),超过会报ScriptMemoryAbuseException错误
    sandbox.allowNoBraces(false); // 是否不允许使用大括号
    sandbox.allowLoadFunctions(true); // 是否允许nashorn加载全局函数
    sandbox.setMaxPreparedStatements(30); // because preparing scripts for execution is expensive // LRU初缓存的初始化大小,默认为0
    sandbox.setExecutor(Executors.newSingleThreadExecutor());// 指定执行程序服务,该服务用于在CPU时间运行脚本

    String jsStr = "function test(){return 'Hello SandBox'};";
    sandbox.eval(jsStr);
    System.out.println(sandbox.getSandboxedInvocable().invokeFunction("test"));
}

// 执行结果:
Hello SandBox

4、可以说,工作中我们使用的最多的就是使用invokeFunction这种方式,这种方式还有个很大的便处就是,很方便传入参数:

无论是 nashorn 方式,还是 sandbox 方式!

public static final String invokeJs = "function test(a, b, c){return a + b + c}";

// 使用nashorn执行js函数
@Test
public void testInvoke1() throws ScriptException, NoSuchMethodException {
    ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
    nashorn.eval(invokeJs);
    System.out.println(((Invocable) nashorn).invokeFunction("test", 1, 2, 3));
}

// 使用sandbox执行js函数
@Test
public void testInvoke2() throws ScriptException, NoSuchMethodException {
    NashornSandbox sandbox = NashornSandboxes.create();
    sandbox.setMaxCPUTime(100);
    sandbox.setMaxMemory(1024 * 1024);
    sandbox.allowNoBraces(false);
    sandbox.allowLoadFunctions(true);
    sandbox.setMaxPreparedStatements(30);
    sandbox.setExecutor(Executors.newSingleThreadExecutor());
    sandbox.eval(invokeJs);
    System.out.println((sandbox.getSandboxedInvocable().invokeFunction("test", 1, 2, 3)));
}

// 执行结果相同:
6.0


四、实战(项目初始化时动态初始化多个js脚本,后期按需求调用)

在支持用户自己动态配置js脚本的场景中很适用,如规则引擎!

如有需要,可在这里查看:https://gitee.com/jiguiquan/zidan-script-engine

代码看懂也就没什么,重要的是思路,权当抛砖引玉!

在dev分支中又做了一些调整,适配脚本入参不固定场景,可以将入参都封装在一个固定的请求体msg中;在js脚本中先解析,再使用!








jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐