自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 (推薦)