您的位置:首页 > 编程语言 > Java开发

学习使用 Java 自带的 JS 引擎:Rhino

2011-05-06 23:01 1186 查看
Rhino 是 JRE 自带的 JavaScript 引擎。从 JRE 6 时代开始正式整合。正如 JavaScript 字面本意是“Java->Script = Java 的脚本”那般,于 JRE 内整合 Rhino 就显得“根正苗红”、“名正言顺”的。本人一直所关心的,是如何能够结合两者的特长,发挥 Java 强类型语言和 JS 的灵活性。就这个话题,我尝试了封装了 Rhino 某些接口,并略有心得。在这里借 CSDN 的宝地,向大家介绍介绍 Rhino。

JsEngine

该类是 JS 引擎的核心。
最新完整代码在:http://code.taobao.org/p/bigfoot_v2/src/java/com/ajaxjs/framework/javascript/JsEngine.java
单元测试:http://code.taobao.org/p/bigfoot_v2/src/java/com/ajaxjs/framework/javascript/TestJS.java
该类封装了一些 API 的方法,使其更健壮。主要的方法如下:


运算 JS 代码

第一件事情,创建 Rhino 实例:
import javax.script.*;
public class JsEngine {
	private ScriptEngine js_engine = new ScriptEngineManager().getEngineByExtension("js");
        ...
}
这是一个空的 JS 运行时。要输入 JS 语句,调用 js_engine.eval(String js); 即可,效果等同于 JS 中的 eval()。如下所示:
/**
 * 运行 js 代码
 * @param jsSource
 * @return
 */
public Object eval(String jsCode) throws ScriptException{
	Object result = null;
	
	if(Util.isEmptyString(jsCode))System.err.println("传入 jsCode 为空,请输入代码");
	else result = js_engine.eval(jsCode);
	
	return result;
}
例子:
@Test
public void testEval() throws ScriptException{
	js.eval("var foo ='Hello World!';");
	Object obj;
	obj = js.eval("foo='Hello World!';");
	obj = js.eval("foo;");
	
	assertNotNull(obj);
	assertEquals(obj.toString(), "Hello World!");
}
注意 js.eval("var foo ='Hello World!';"); 并没有返回值,而全局变量的方式 js.eval("foo='Hello World!';"); 和单独调用变量 js,eval("foo;") 则会有返回值 。

加载 JS 文件

如同网页加载 js <script src="foo.js"> 那样,我们提供 load() 方法来加载 JavaScript 文件。因为 Rhino 没有直接提供一个加载磁盘 js 文件的专门方法,所以 load() 方法的原理是读取磁盘文件之后把内容传给 eval() 执行。
/**
 * 加载 js 文件
 * @param fullFilePath
 * @return
 */
public String load(String fullFilePath){
	String code = null;
	System.out.println("加载 js 文件:" + fullFilePath);
	
	try {
		code = Fso.readFile(fullFilePath);
	} catch (FileNotFoundException e) {
		System.err.println("加载文件 " + fullFilePath + "的时候,磁盘找不到该文件!");
		e.printStackTrace();
	}
	
	try {
		if(code != null)eval(code);
	} catch (ScriptException e) {
		System.err.println("加载文件 " + fullFilePath + "的时候,js 引擎发现语法错误!请修正 js 里的问题!");
		e.printStackTrace();
	}
	
	return code;
}

public void load(String[] fullFilePaths){
	for(String fullFilePath : fullFilePaths)load(fullFilePath);
}

/**
 * 加载 js 文件
 * 从类相同目录的地方加载 js 文件。
 * @param cls
 * @param jsFileName
 * @return
 */
public String load(Class<?> cls, String jsFileName){
	return load(Util.getClassFolder_FilePath(cls, jsFileName));
}
为了更方便调用,于是对 load 方法进行重载,形成了另外两个方法(如上所示)。
例子:

@Test
public void testLoad() throws ScriptException {
	Object obj;

	js.load("c:/project/bigfoot/java/com/ajaxjs/mvc/config.js");
	obj = js.eval("bf");
	assertNotNull(obj);
	
	js.load(Node.class, "config.js");
	obj = js.eval("bf");
	assertNotNull(obj);
}
如果不想依赖 Fso.readFile() 方法,可以使用下面封装的方法。
public void read_jsFile(String filePath){
    // System.out.println("Reading JS File:::::::::" + filePath);
    java.io.FileReader reader = null;
    
    try{
        if(!new java.io.File(filePath).exists())throw new java.io.FileNotFoundException("JS file not exist:" + filePath);

        reader  = new java.io.FileReader(filePath);    
        
        if(System.getProperty("java.version").startsWith("1.6")){
            //////////---------------JDK6的毛病-----------不能解析 utf-8编码
            BufferedReader br =null;
            try {
                br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath),"UTF-8"));
            } catch (UnsupportedEncodingException e1) {
                e1.printStackTrace();
            }   
            StringBuffer file = new StringBuffer();
            String line = null;   
            
            try {
                while ((line = br.readLine()) != null) {   
                    file.append("\n");   
                    file.append(line);   
                }
            } catch (IOException e) {
                e.printStackTrace();
            }finally{
                if(br!=null)
                    try {br.close();
                    } catch (IOException e) {e.printStackTrace();} 
            }
            js_engine.eval(file.toString());
            //////////---------------JDK6的毛病-----------不能解析 utf-8编码
            
        }else{
            // js_engine.eval(new InputStreamReader(Main.class.getResourceAsStream("scripting.js"))); // 从类路径中加载js文件并执行。
            js_engine.eval(reader);
        }
        
    }catch(javax.script.ScriptException e){
        e.printStackTrace();
        System.out.println("脚本" + filePath +" 解析错误!");
//        log(e.toString(), true);
    }catch(java.io.FileNotFoundException e){
        e.printStackTrace();
        System.out.println("没有 " + filePath +" 文件!");
    }finally{
        try{
            if(reader != null)reader.close();
        }catch(java.io.IOException e){}
    }
}
JDK 7 的话可以简单方法 engine.eval(new InputStreamReader(Main.class.getResourceAsStream("scripting.js"))); 就行了;JDK 6 用这个方法会有 UTF-8 的问题,所以多写了这么多的代码。

Java 向 JS 传递变量

在 Java 中向脚本引擎 (Script Engine) 传递变量,即脚本语言使用 Java 定义的变量,例如 js_engine.put("fileObj", obj);。当然用 eval() 也可以。注意可以直接赋值 Java 对象。
public void put(String varName, Object obj){
	// js_engine.put("fileObj", obj);
	js_engine.put(varName, obj);
}
例子:
@Test
public void testPut() throws ScriptException {
	js.put("a", 6);
	Object obj = js.eval("a");
	
	assertNotNull(obj);
	assertEquals(obj, 6);
}
对于上面 put 的变量,它作用于自身 engine 范围内,也就是 ScriptContext.ENGINE_SCOPE,put 的变量放到一个叫 Bindings 的 Map 中,可以通过 engine.getBindings(ScriptContext.ENGINE_SCOPE).get(“a”); 得到 put 的内容。和 ENGINE_SCOPE 相对,还有个 ScriptContext.GLOBAL_SCOPE 作用域,其作用的变量是由同一 ScriptEngineFactory 创建的所有 ScriptEngine 共享的全局作用域。

执行方法且传参

前面提到 eval() 可以执行 JS 的方法。然而 eval() 不能传参数。要传参数应使用 call()。我装的方法如下所示。
/**
 * @param methodName
 * @param arg Object result1 = jsInvoke.invokeFunction(methodName, new Object[] { 10, 5 });
 * @return
 * @throws ScriptException 
 */
public Object call(String methodName, Object...arg) throws ScriptException{
	Object result = null;
	
	Invocable inv = (Invocable)js_engine; // Invocable 接口是 ScriptEngine可选实现的接口。(多态)
	
	try { 
	    result = inv.invokeFunction(methodName, arg);  
	}catch(NoSuchMethodException e) {  
		System.err.println("没有 " + methodName + "() 这个方法");
		e.printStackTrace();
	}catch(ScriptException e) {  
		System.err.println("脚本执行 " + methodName + "() 异常!");
		throw e;
	}

	return result;
}
call() 方法的第一个参数必须是方法名,后面是参数列表。还有一个问题在下仍未想通,就是不知道如何调用 “obj.foo.bar.mehtod()” 这样非全局对象的方法。
例子:
@Test
public void testCall() throws ScriptException {
	js.eval("function max_num(a, b){return (a > b) ? a : b;}");
	Object obj = js.call("max_num", 6, 4);
	
	assertNotNull(obj);
	assertEquals(obj, 6);
}
调用 obj.foo.bar.mehtod() 的方式可以要使用 invokeMethod() 方法。顾名思义,Method 是对应面向对象的,invokeFunction 中的 Function 是对应过程式的函数。
invokeMethod() 必须指定对象是哪个。

inv.invokeMethod(obj, methodName, arg);
脚本语言实现 Java 的接口。Invocable 还可以动态实现接口,它可以从脚本引擎中得到 Java Interface 的实例;也就是说可以定义个一个 Java 接口,其实现是由脚本完成。
interface Adder {  
	int add(int a, int b);  
} 
 
Adder adder = inv.getInterface(Adder.class); // 通过 inv.getIntegerface()方法转化为java的接口  

String script = "var obj= new Object();obj.run=function(){println('run() was called');}";    
engine.eval(script);    
Object obj = engine.get("obj");    
javax.script.Invocable inv = (Invocable)engine;    
Runnable r = inv.getInterface(obj, Runnable.class);    
Thread t = new Thread(r);  
engine.eval("function run() {print('www.java2s.com');}");  
Invocable invokeEngine = (Invocable)engine;  
Runnable runner = invokeEngine.getInterface(Runnable.class);  
Thread t = new Thread(runner);  
t.start();  
t.join();
以上面的例子为例,定义接口 JSLib,该接口中的函数和 JavaScript 中的函数签名保持一致。

工具函数,返回特定类型

因为 eval() 总是返回 Object,所以每次类型都感觉比较麻烦。于是我封装了若干函数返回常见的值。这里没有使用泛型,而是使用方法定义。
private Object eval_return_Object(String jsSource){
	Object jsHash = null;
	
	try {
		jsHash = eval(jsSource);
	} catch (ScriptException e) {
		System.err.println("【error when eval jsSource】:" + jsSource);
		e.printStackTrace();
	}
	
	return jsHash;
}

public Map<String, String> eval_return_Map_String(String jsSource) {
	Map<String, Object> jsHash = eval_return_Map(jsSource);
	Map<String, String> hash = new HashMap<String, String>();
	for(String key : jsHash.keySet()){
		hash.put(key, jsHash.get(key).toString());
	}
	
	return hash;
}

public Map<String, Object> eval_return_Map(String jsSource) {
	Object jsHash = eval_return_Object(jsSource);
	return jsHash != null ? Mapper.NativeObject2Hash(jsHash) : null;
}

public Map<String, Object>[] eval_return_MapArray(String jsSource) {
	Object jsHash = eval_return_Object(jsSource);
	return jsHash != null ? Mapper.NativeArray2Map(jsHash) : null;
}

public String eval_return_String(String jsSource) {
	Object jsHash = eval_return_Object(jsSource);
	return jsHash != null ? jsHash.toString() : null;
}
同时也捕获了异常,自动处理异常。
例子:
@Test
public void testEval_return_String() throws ScriptException {
	String str = js.eval_return_String("'Hello';");
	
	assertNotNull(str);
	assertEquals(str, "Hello");
}

@Test
public void testEval_return_Map() throws ScriptException {
	Map<String, Object> map = js.eval_return_Map("json = {\"foo\" : \"88888\", \"bar\":99999};");
	

	assertNotNull(map);
	assertEquals(map.get("foo"), "88888");
	assertEquals(map.get("bar"), 99999);
}

@Test
public void testEval_return_Map_String() throws ScriptException {
	Map<String, String> map = js.eval_return_Map_String("json = {\"foo\" : \"88888\"};");
	
	assertNotNull(map);
	assertEquals(map.get("foo"), "88888");
}

@Test
public void testEval_return_MapArray() throws ScriptException {
	Map<String, Object>[] map = 
		js.eval_return_MapArray("json = [{\"foo\" : \"88888\"}, {\"bar\" : \"99999\"}];");
	
	assertNotNull(map);
	assertEquals(map.length, 2);
	assertEquals(map[0].get("foo"), "88888");
	assertEquals(map[1].get("bar"), "99999");
}

脚本预编译

脚本引擎默认是解释执行的,如果需要反复执行脚本,可以使用它的可选接口 Compilable 来编译执行脚本,以获得更好的性能。
public void compile(){
	try{
		javax.script.Compilable compEngine = (javax.script.Compilable) js_engine;
		javax.script.CompiledScript script = compEngine.compile("function max_num(a,b){return (a>b)?a:b;}");
		script.eval();
		javax.script.Invocable invoke = (javax.script.Invocable) compEngine;
		System.out.println(invoke.invokeFunction("max_num",4,6));
	}catch(javax.script.ScriptException e){
	}catch(NoSuchMethodException e){}
}
这个用得比较少。

内联 JS(inline-js)

为了方便与 Java 代码上下文结合,按照“就近原则”,我们就把 JS 代码嵌入到 Java 类里面去,不另外调用 js 文件了。为此,我们提出一个“内联 JS(inline-js)”的概念。这样的好处是可读性强,不必为了看懂逻辑而需要在 IDE 里面跳来跳去。例如:
class {
        ...
	static{
		// call 不能调用 JSON.stringify,所以用一个全局函数包裹着
		try {
			js.eval("function toSingleJSON(hash){return JSON.stringify(hash);}");
		} catch (ScriptException e) {
			e.printStackTrace();
		}
	};  

	/**
	 * 返回 JSON 字符串
	 * @param jsonObj(NativeObject|NativeArray)
	 * @return
	 */
	private static String navtiveStringify(Object jsonObj){
		Object jsonStr = null; 

		try {
			jsonStr = js.call("toSingleJSON", jsonObj);
		} catch (ScriptException e) {
			System.err.println("Can not make jsonObj as jsonStr in Rhino");
			e.printStackTrace();
		}
		
		return jsonStr == null ? null : jsonStr.toString();
	}
}
如果 JS 代码比较多,则要考虑把 JS 代码放进 *.js 文件中。

JS 使用 Java 包、对象

默认下 Rhino 只会导入 java.*/com.* 的 Java 包,如果你的项目是其他名字的包名,是要在 Rhino 里面的 js 导入的,如下:
// import package java.* / com/* by default
importPackage(Packages.ajaxjs);// Import our all packages.Beware of names conficts!
在 JS 里面可以直接使用 Java 对象:
String jsCode = "importPackage(java.util);var list2 = Arrays.asList(['A', 'B', 'C']); ";
js_engine.eval(jsCode);
java.util.List<String> list2 = (java.util.List<String>) js_engine.get("list2");
for (String val : list2) {
 	System.out.println(val);
}
JS 对于传入的 Java 字符串不能直接使用,必须包装一下转换为 JavaScript String 才可以用,例如 String(J***A_String);。
下面说说 JS 对象向 Java 的转换。

JS 向 Java 的转换

Rhino 里面转换 Js 数组 到 Java ArrayList

/**
 * JS Array 2 Java Array
 * @param arr
 * @returns {java.util.ArrayList}
 */
function toArrayList(arr, isHashElemet){
	var java_arr = new java.util.ArrayList();
	for(var i = 0, j = arr.length; i < j; i++){
		if(isHashElemet){
			var java_hash = new java.util.HashMap();
			for(var js_hash in arr[i]){
				java_hash.put(js_hash, arr[i][js_hash]);
			}
			java_arr.add(java_hash);
		}else java_arr.add(arr[i]);
	}
	
	return java_arr;
}

Rhino 里面转换 Js Obj 到 Java HashMap<String, String>

function toHashMap(keys, values){
	var java_hash = new java.util.HashMap();
	if(arguments.length == 1)
		for(var i in keys){
			java_hash.put(i, keys[i]);
		}
	else if(arguments.length > 1)
		for(var i = 0, j = keys.length; i < j; i++){
			java_hash.put(keys[i], values[i]);
		}
	
	return java_hash;
}
相关类型的转换方法。
public String double2String(Object obj){  
    int intVlalue = ((Double)obj).intValue();  
    return intVlalue + "";  
}  
  
public String dateParser(Double dateTime, java.text.SimpleDateFormat dateFormater){  
    if(dateFormater == null)  
        dateFormater = new java.text.SimpleDateFormat("MM-dd HH:mm");  
      
    Long dt = dateTime.longValue();  
    return dateFormater.format(new java.util.Date(dt));  
}

扩展 Rhino

在设计这个包的时候,我特意弄复杂一些。Why?因为大家知道,Java 开发的过程中,有时候看起来非常直接的实现却非要用设计模式转若干个弯去实现他。这似乎显的很多余,但是采用一些成熟的设计模式,会使程序更加的健壮、松耦合以及好维护和扩展。
Function.prototype.bind 的实现。好像不能直接扩展 Function.prototype,于是写成一个普通的函数,作用一样。
/**
 * 函数委托 参见 http://blog.csdn.net/zhangxin09/article/details/8508128  * @return {Function}
*/
Function_delegate = function () {
    var self = this, scope = this.scope, args = arguments, aLength = arguments.length, fnToken = 'function';

    return function(){
        var bLength = arguments.length, Length = (aLength > bLength) ? aLength : bLength;

        // mission one:
        for (var i = 0; i < Length; i++)
            if (arguments[i])args[i] = arguments[i]; // 拷贝参数

        args.length = Length; // 在 MS jscript下面,arguments作为数字来使用还是有问题,就是length不能自动更新。修正如左:

        // mission two:
        for (var i = 0, j = args.length; i < j; i++) {
            var _arg = args[i];
            if (_arg && typeof _arg == fnToken && _arg.late == true)
                args[i] = _arg.apply(scope || this, args);
        }

        return self.apply(scope || this, args);
    };
};
甚至还可以考虑引入更多的函数式特性!

最后要说的是 JsEngine 还带有一个格式化的内部类,方便美观 JSON。

IBM 很好的参考资源:
http://www.cnblogs.com/MMLoveMeMM/articles/3219998.html Java 自带的命令行工具文档:http://docs.oracle.com/javase/6/docs/technotes/tools/share/jrunscript.html
Java SE 6 新特性: 对脚本语言的支持
动态调用动态语言
给 Java SE 注入脚本语言的活力
JavaScript EE,第 1 部分: 在服务器端运行 JavaScript 文件
JavaScript EE,第 2 部分: 用 Ajax 调用远程 JavaScript 函数
JavaScript EE,第 3 部分: 结合使用 Java Scripting API 和 JSP
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: