一、被淘汰的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脚本中先解析,再使用!