自JDK1.6开始,已经自带了一个ScriptEngine,可以用来执行如javascript何groovy脚本代码。在实际场景中基本上都是在多线程环境下使用的,比如在servlet中执行一个脚本对推荐结果列表做二次转换后再返回给前端结果。
可以通过执行一下代码可以查看你当前使用的jdk在支持的脚本的线程安全性:
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
public class ScriptEngineTest {
public static void main(String[] args) {
final ScriptEngineManager mgr = new ScriptEngineManager();
for(ScriptEngineFactory fac: mgr.getEngineFactories()) {
System.out.println(String.format("%s (%s), %s (%s), %s", fac.getEngineName(),
fac.getEngineVersion(), fac.getLanguageName(),
fac.getLanguageVersion(), fac.getParameter("THREADING")));
}
}
}
官方也提供了一些在多线程环境下使用ScriptEngine的建议:https://blogs.oracle.com/nashorn/nashorn-multithreading-and-mt-safety
o, our agenda(议事日程) is two fold. The first is to provide a "workers" library (timeline is not tied to JDK8) which uses an onevent model
that JavaScripters are familiar with. No synchronization/locking constructs to be added to the language. Communication between threads
(and potentially nodes/servers) is done using JSON (under the covers.) Going this route allows object isolation between threads, but also allows
maximal use of CPU capacity.
The second part is, we will not guarantee MT-safe structures, but we have to face reality. Developers will naturally be drawn to using threads.
Many of the Java APIs require use of threads. So, the best we can do is provide guidelines on how to not shoot yourself in the foot(搬起石头砸自己的脚
These guidelines will evolve(发展) and we'll post them 'somewhere' after we think them through. In the meantime, I follow some basic rules;
Avoid sharing script objects or script arrays across threads (this includes global.) Sharing script objects is asking for trouble. Share only
primitive data types, or Java objects.
If you want to pass objects or arrays across threads, use JSON.stringify(obj) andJSON.parse(string) to transport using strings.
If you really really feel you have to pass a script object, treat the object as a constant and only pass the object to new threads (coherency.)
Consider using Object.freeze(obj).
If you really really really feel you have to share a script object, make sure the object's properties are stabilized. No adding or removing
properties after sharing starts. Consider using Object.seal(obj).
Given enough time, any other use of a shared script object will eventually cause your app to fail.
里面说到了几点值得注意的,就是不要共享脚本中的对象,可以共享原生数据类型或者java对象,比如你在script里面定义一个var i=0,那么这个变量i是全局的变量,是所有线程共享的变量,当你在多线程情况下执行var i=0; i=i+1;时,每个线程得到的结果并不会都是1。当然如果你是在script的function中定义的变量,那么它不会被共享,例如你的script string是:function addone(){var i=0;i=i+1;return i;}那么多线程同时调用这个function时,返回的结果都是1。
这里要注意的一点是function中一定要用var 重新定义变量,否则还是全局的变量.
如果你确实想共享一个对象,可以用JSON.stringfy和JSON.parse通过json序列化和反序列化来共享,这样其实内部是生成java 的String对象,String对象都是新分配内存保存的,所以每个线程都持有的是不同的对象实例,改变互不影响。
尽量复用ScriptEngine,因为是将脚本语言编译成了java可执行的字节码来执行的,如果不复用的话,那么每次都要去编译脚本生成字节码,如果是一个固定的脚本的话,这样效率是很低的
如何去在servlet中复用ScriptEngine:
public class MyServlet extends HttpServlet { private static ThreadLocal<ScriptEngine> engineHolder; @Override public void init() throws ServletException { engineHolder = new ThreadLocal<ScriptEngine>() { @Override protected ScriptEngine initialValue() { return new ScriptEngineManager().getEngineByName("JavaScript"); } }; } @Override public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { try (PrintWriter writer = res.getWriter()) { ScriptContext newContext = new SimpleScriptContext(); newContext.setBindings(engineHolder.get().createBindings(), ScriptContext.ENGINE_SCOPE); Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE); engineScope.put("writer", writer); Object value = engineHolder.get().eval("writer.print('Hello, World!');", engineScope); writer.close(); } catch (IOException | ScriptException ex) { Logger.getLogger(MyServlet.class.getName()).log(Level.SEVERE, null, ex); } } }
使用一个ThreadLocal来共享ScriptEngine,这样在后来的请求可以复用这个ScriptEngine实例。即每个线程都有自己的ScriptEngine实例。由于线程池的存在,线程可以被复用,从而ThreadLocal里的ScriptEngine也被复用了。
给一个测试多线程使用的例子:
import jdk.nashorn.api.scripting.NashornScriptEngine; import jdk.nashorn.api.scripting.ScriptObjectMirror; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.*; public class JavaExecScriptMTDemo { public static void main(String[] args) throws Exception { ScriptEngineManager sem = new ScriptEngineManager(); NashornScriptEngine engine = (NashornScriptEngine) sem.getEngineByName("javascript"); String script = "function transform(arr){" + " var arr2=[]; for(var i=0;i<arr.size();i++){arr2[i]=arr[i]+1;} return arr2.reverse(); " + "}"; engine.eval(script); Callable<Collection> addition = new Callable<Collection>() { @Override public Collection call() { try { ScriptObjectMirror mirror= (ScriptObjectMirror)engine.invokeFunction("transform", Arrays.asList(1, 2, 3)); return mirror.values(); } catch (ScriptException | NoSuchMethodException e) { throw new RuntimeException(e); } } }; ExecutorService executor = Executors.newCachedThreadPool(); ArrayList<Future<Collection>> results = new ArrayList<>(); for (int i = 0; i < 50; i++) { results.add(executor.submit(addition)); } int miscalculations = 0; for (Future<Collection> result : results) { Collection jsResult = result.get(); System.out.println(jsResult); } executor.awaitTermination(1, TimeUnit.SECONDS); executor.shutdownNow(); }
脚本内容是一个函数,传入一个整型数组,然后将数组中的每个元素+1,返回一个新的数组。启动50个线程同时执行这个函数,会发现输出的结果都是[2,3,4]。说明这种方式的用法是线程安全的,因为我们没有共享脚本变量。
当我们改变一下脚本内容,改成这样:
String script = "function transform(arr){" + " var arr2=[]; for( i=0;i<arr.size();i++){arr2[i]=arr[i]+1;} return arr2.reverse(); " + "}";
就是把for循环上的var i=0改成了i=0.然后再执行测试代码。会发现抛出数组越界异常。说明这种情况下多线程不安全了,因为这个i是一个共享脚本变量,每个脚本下都可见。
script包下最主要的是ScriptEngineManager、ScriptEngine、CompiledScript和Bindings 4个类或接口。
ScriptEngineManager是一个工厂的集合,可以通过name或tag的方式获取某个脚本的工厂,并生成一个此脚本的ScriptEngine,
ScriptEngine engine=new ScriptEngineManager().getEngineByName("JavaScript");
通过工厂函数得到了ScriptEngine之后,就可以用这个对象来解析脚本字符串,直接调用Object obj = ScriptEngine.eval(String script)即可,返回的obj为表达式的值,比如true、false或int值。
ScriptEngine:是一个脚本引擎,包含一些操作方法,eval,createBindings,setBindings
engine.eval(option); //option:"10+((D-parseInt(D/28)*28)/7+1)*10";
option 可以是一段js代码,函数,函数传参需要调用 engine.createBindings获得bindings,bindings.put(key,value)来传入参数
CompiledScript可以将ScriptEngine解析一段脚本的结果存起来,方便多次调用。只要将ScriptEngine用Compilable接口强制转换后,调用compile(String script)就返回了一个CompiledScript对象,要用的时候每次调用一下CompiledScript.eval()即可,一般适合用于js函数的使用。
Compilable compilable=(Compilable) engine; CompiledScript JSFunction=compilable.compile(option); //解析编译脚本函数 Bindings bindings=engine.createBindings(); bindings.put(key,value); JSFunction.eval(bingdings);
Bindings有3个层级,为Global级、Engine级和Local级,前2者通过ScriptEngine.getBindings()获得,是唯一的对象,而Local Binding由ScriptEngine.createBindings()获得,很好理解,每次都产生一个新的。Global对应到工厂,Engine对应到ScriptEngine,向这2者里面加入任何数据或者编译后的脚本执行对象,在每一份新生成的Local Binding里面都会存在。
String option="function getNum(num){if(num%7==0){ return 5+5*(parseInt(5*(num-parseInt(num/29)*28)/28)+1)}else{return 5;}} getNum(num)" ------------------------------- try{ Compilable compilable = (Compilable) engine; Bindings bindings = engine.createBindings(); //Local级别的Binding CompiledScript JSFunction = compilable.compile(option); //解析编译脚本函数 for(Map.Entry<String,Object> entry:map.entrySet()){ bindings.put(entry.getKey(),entry.getValue()); } Object result=JSFunction.eval(bindings); System.out.println(result); return (int)Double.parseDouble(result.toString()); }catch (ScriptException e) { e.printStackTrace(); }
Script Variables 脚本变量
当您使用Java应用程序嵌入脚本引擎和脚本时,,可能希望将应用程序对象做为脚本的全局变量。此示例演示如何将应用程序对象公开为脚本的全局变量。我们在应用中创建了一个java.io.File对象,使用名称“file”显示与全局变量相同的内容。这个脚本可以读取这个变量。例如:它可以调用这个变量的权限修饰为public的方法。请注意,访问Java对象,方法和字段的语法取决于脚本语言。 JavaScript支持最“自然”的类似Java的语法。
public class ScriptVars { public static void main(String[] args) throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("Nashorn"); File f = new File("test.txt"); //将File对象f直接注入到js脚本中并可以作为全局变量使用 engine.put("file", f); // evaluate a script string. The script accesses "file" // variable and calls method on it engine.eval("print(file.getAbsolutePath())"); } }
Invoking Script Functions and Methods java中执行js函数
有时你需要重复调用一个特定的脚本函数,例如,您的应用程序菜单功能可能由脚本实现。在你的菜单的事件处理器中你可能想调用一个脚本的函数。以下示例演示了从Java代码调用特定的脚本函数。
import javax.script.*; public class InvokeScriptFunction { public static void main(String[] args) throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("Nashorn"); // String定义一个js函数 String script = "function hello(name) { print('Hello, ' + name); }"; // 直接执行上面的js函数(这个函数是全局的,在下面的js引擎中依然可以调用,不会执行完就消失) engine.eval(script); // javax.script.Invocable 是一个可选的接口 // 检查脚本引擎是否被实现! // 注意:JavaScript engine 实现了 Invocable 接口 Invocable inv = (Invocable) engine; // 执行这个名字为 "hello"的全局的函数 inv.invokeFunction("hello", "Scripting!!" ); } }
如果您的脚本语言是基于对象的(如JavaScript)或面向对象的,则你可以在脚本对象上调用脚本方法。
import javax.script.*; public class InvokeScriptMethod { public static void main(String[] args) throws Exception { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); // 用String定义了一段JavaScript代码这段代码定义了一个对象'obj' // 给对象增加了一个名为 hello 的方法(hello这个方法是属于对象的) String script = "var obj = new Object(); obj.hello = function(name) { print('Hello, ' + name); }"; //执行这段script脚本 engine.eval(script); // javax.script.Invocable 是一个可选的接口 // 检查你的script engine 接口是否已实现! // 注意:JavaScript engine实现了Invocable接口 Invocable inv = (Invocable) engine; // 获取我们想调用那个方法所属的js对象 Object obj = engine.get("obj"); // 执行obj对象的名为hello的方法 inv.invokeMethod(obj, "hello", "Script Method !!" ); } }
参考:
原文:https://blog.csdn.net/xiao_jun_0820/article/details/76498268
https://blog.csdn.net/u014792352/article/details/74644791
https://blog.csdn.net/menggudaoke/article/details/77869354 (推荐)