跳到主要內容

Java - 使用 JavaScript Engine 在後端執行 JavaScript

這篇是想寫好一段時間了
最近剛好有需求做了滿完整的應用,剛好可以拿出來當示範
Java的JavaScript engine在現在前端開發的需求越來越多的情況下,活躍的場合更多了
通常在以下狀況,你可以考慮使用JavaScript Engine
1.你想在前端使用Java的物件、類別或方法
2.前端複雜的邏輯需要在後端實現

當然你也可以將前端的運算結果用JavaScript engine傳送給後端
不過我想這個需求,使用browser request傳遞前端運算成果到後端做起來會簡單很多
這次我的實作目的是需要將一段前端邏輯拉到後端實作
目的程式最初是為了前端的功能實作,儲存資料的方式也是JSON
最後會compile成一段在JavaScript執行的code
不過有時候變化就是那麼突然(咦),因為客戶的需求及安全性考慮需要在後端使用這個功能
這次乾脆讓他在後端以JavaScript的方式省掉重新設計邏輯的麻煩
程式碼如下:

import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import sun.org.mozilla.javascript.internal.NativeArray;
import sun.org.mozilla.javascript.internal.NativeObject;
import sun.org.mozilla.javascript.internal.Scriptable;


 // create a script engine manager
 ScriptEngineManager factory = new ScriptEngineManager();
 // create a JavaScript engine
 ScriptEngine engine = factory.getEngineByName("JavaScript");
 // read json.js file for Rhino under Java 7
 try {
     engine.eval(new FileReader("/js/json2.js"));
 } catch (Exception e) {
     System.out.println(e);
 }

 String storedData = ....;
 String executeCodeGenerator = "for(var _rules=JSON.parse(unescape('" + storedData+ "')...";

 try {
     engine.eval(executeCodeGenerator);
 } catch (ScriptException e) {
     e.printStackTrace();
 }
 String executeCode = engine.get("code").toString();

 //bind front-end Js function
 String generalFunction = "Array.prototype.map||(Array.prototype.map=function(d,f){var g,e,a;if(null===this)throw new TypeError(' this is null or not defined');var b=Object(this),h=b.length>>>0;if('function'!==typeof d)throw new TypeError(d+' is not a function');f&&(g=f);e=Array(h);for(a=0;a<h;){var c;a in b&&(c=b[a],c=d.call(g,c,a,b),e[a]=c);a++}return e});" +
     "var finalIgnoreData =[];....";

 StringBuilder bindValueStr = new StringBuilder();
 // bind back-end data
 for (....) {
     ....
     bindValueStr.append("dataArray[" + id+ "]=['" + value+ "'];");
 }

 try {
     engine.eval(generalFunction + bindValueStr.toString() + executeCode);
 } catch (ScriptException e) {
     System.out.println(e);
 }

 NativeArray ignoreData = (NativeArray) engine.get("finalIgnoreData ");
 for (Object idIndex : ignoreData .getIds()) {
     Object info = ignoreData .get((Integer) idIndex, null);
     int id= (int) Math.round(Double.parseDouble(NativeObject.getProperty((Scriptable) info, "id").toString()));
     int y = (int) Math.round(Double.parseDouble(NativeObject.getProperty((Scriptable) info, "y").toString()));
     ignoreList.put(id, y);
 }


首先會需要先取得JavaScript Engine的instance
Java 7 開始的JavaScript engine才會實作JSON相關的函式
所以為了相容性我加了這句 engine.eval(new FileReader("/js/json2.js"));
讓他先執行我放在特定位置的JSON相容程式
如果你需要在後端執行特定的JavaScript 檔案,也可以用這種方式進行
engine可以接收 String 或 input stream ,並在內部用JavaScript的邏輯方式運作

接著的 String executeCodeGenerator = "for(var _rules=JSON.parse(unescape('" + storedData+ "')...";
則是我將儲存的JSON格式資料交給engine處理了
這樣也不用在後端處理將String轉為Java的JSONObject等等地瑣碎片段
這一端字串中我放入"var code"來儲存運算後的結果
用 String executeCode = engine.get("code").toString(); 把資料從engine裡拿出來
這一段在加工後還會再放入engine裡用JavaScript的方式處理
最後的結果再從 JavaScript engine 裡拿出來
處理邏輯全都沿用之前在前端寫的,完全沒有再重作

最後面其實有些偷懶
sun.org.mozilla.javascript.internal的幾個類別
讓我們可以取得內部的JavaScript Object跟JavaScript Array在Java中的instance
不過雖然這幾個類別在Oracle發行的正規版裡有,但是OpenJDK則沒有實作
所以如果要追求相容性的話還需要再處理成 String 等可以在兩邊通用的原生型別

我覺得這樣運用就已經滿完整的
如果有更進階的需求,可以再多參考官方文件
http://docs.oracle.com/javase/7/docs/technotes/guides/scripting/programmer_guide/
http://www.java2s.com/Tutorial/Java/0120__Development/0720__Script-Engines.htm
Java 8還有再多加入一些功能,可以參考下面的連結
http://www.codedata.com.tw/java/jdk8-nashorn-jjs/

留言