使用 Rhino 作为 Java 的 JSON 解析/转换包

前端开发者是幸福的,源自于浏览器对 JSON 天然的支持(JSON 本身脱胎于 JavaScript),JSON 字符串一下子 eval() 或者 JSON.parse() 就可以直接使用了;输出 JSON 字符反之亦然。

如果是 JS 的老大哥 Java 呢?这个问题大家应该都会不约而同地回答:一般从接口转换 Java 对象为 JSON 输出的时候都会选择相关的 JSON-lib,有的是 JSON.org 的,有的是 Jackson JSON,有的是 FastJSON 的,有的是 GSON 的,很多很多,反之亦然。而今天我要跟大家说的是,其实只要使用自带 Rhino 也可以做到 JSON 转换——就地取材。

在先前封装过 Rhino 的基础上,进而实现了 JSON 转换——为 Mapper “映射器”。

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

JsEngine

该类是 JS 引擎的核心。

该类封装了一些 API 的方法,使其更健壮。主要的方法如下:

使用 Rhino 作为 Java 的 JSON 解析/转换包

运算 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(JAVA_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 很好的参考资源:

下面介绍一下 Mapper。

Mapper 作用

Mapper 为映射器。如同在前端里面的 JSON.stringify() 和 JSON.parse() 那样,我们都是围绕序列化和反序列化两大命题来提供相关的方法,只不过当前的环境换成了 Java。请见下面的脑图。

使用 Rhino 作为 Java 的 JSON 解析/转换包

上图只是呈现大体思路。下面展开详述之。

Mapper 没有直接继承 JsEngine 类,而是作为一个 JsEngine 的单例,提供了若干静态的方法调用。

必须指出,不是什么类型的 Java 对象都可以转换为 JSON 的。当前我们只能够为 Map 和 Object 提供支持。一方面来说我们也不希望太复杂的转换。

Map 转换为 JSON Str

JSON 无非是键队值的结构,Java 的 Map 也键队值的结构,因此转换起来毫不费力。具体过程如下。

/**
 * 输入一个 Map,将其转换为 JSON Str
 * @param hash
 * @return
 */
public static String stringify(Map<String, Object> hash){
	StringBuilder buff = new StringBuilder();
	buff.append("{");
	
	int size = hash.size();
	if(size > 0){
		int i = 0;
		for(String key : hash.keySet()){
			buff.append("\"" + key + "\":" + "\"" + hash.get(key) + "\"");
			if(++i != size)buff.append(",");
		}
	}
	
	buff.append("}");
	return buff.toString();
}

十分抱歉的是,这个方法非常粗糙,不管 Map 里面值类型如何,通通输出 JSON 的字符串。理想的情况下,应该可以根据类型作正确的转换。不过由于 Map 转换 JSON 的情况不多,所以就懒了一下。

例子:

Map<String, Object> map = new HashMap<String, Object>();
map.put("foo", "11");
map.put("bar", 2222);

String jsonStr = Mapper.stringify(map);
// 输出 {"foo":"11","bar":"2222"}

实际应用中使用更多的并不是 HashMap——简单的 Object 即可,而且可以解决上面方法没有解析 Number/Boolean 的情况,请接着看看。

Object 转换为 JSON Str

 将 Simple Object 对象转换成JSON格式的字符串:JAVA-->JSON。

Object obj = new Object() {  
    public Boolean isOk = false;   
    public String msg = "Hello world";
};

String jsonStr = Mapper.stringify(obj);
// 输出 {"foo":"11","bar":"2222"}
assertNotNull(jsonStr);
System.out.println(jsonStr);
assertEquals(jsonStr, "{\"isOk\":false,\"msg\":\"Hello world\"}");

如上所示,声明 Object 的时候利用了匿名类的构造函数定义了两个字段 isOk 和 msg。经过 Mapper.stringify 转换为 JSON 字符串。

下面为一个应用的例子,是我类库中封装的方法。

public void printErr(Exception error, String customErrMsg)throws IOException{
    resp.setContentType("application/json");
    
    final String _msg = String.format("发生异常。异常信息:%s %s %s", 
        error == null ? "" : error.getMessage(), 
        error == null ? "" : error.toString(), 
        customErrMsg == null ? "" : customErrMsg
    );

    respWriter.println(parseObj(new Object() {  
        public Boolean isOk = false;   
        public String msg = _msg;
    }));
    respWriter.flush();
}

有兴趣看看转换的原理吗?想必聪明的你已然想到会用 Java 反射去处理的——是的,正是如此,请见下面的源码。

public static String stringify(Object obj){
    if (obj == null)return null;
    
    // 检查是否可以交由 JS 转换的类型,否则使用该方法进行处理
    if(obj instanceof NativeArray || obj instanceof NativeObject)return navtiveStringify(obj);
    
    Class<?> clazz = obj.getClass();
    java.lang.reflect.Field[] fields = clazz.getDeclaredFields();
    
    StringBuilder buff = new StringBuilder();
   
    buff.append("{");

    for (java.lang.reflect.Field f : fields) {
        String fieldName; Object value;
        
        try{
            fieldName = f.getName();
            f.setAccessible(true);
            if(fieldName.indexOf("this$") != -1)continue;
            value = f.get(obj);
            
//	            System.out.println(fieldName + "--------" + value);
            buff.append("\"");
            buff.append(fieldName);
            buff.append("\":");

            if(value == null){
                buff.append("\"\",");
                continue;
            }
            if(value instanceof Boolean){
                buff.append((Boolean)value);
                buff.append(",");
            }else if(value instanceof Number){
                buff.append((Number)value);
                buff.append(",");
            }else if(value instanceof java.util.Date){
                buff.append("\"");
                buff.append(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((java.util.Date)value));
                buff.append("\",");
            }else if(value instanceof Object[]){
                Object[] arr = (Object[])value;
                String str = "";
                
                for(int i = 0; i < arr.length; i++){
                    if(arr[i] instanceof String){
                        str += ("\"" + arr[i] + "\"").replace("\\", "\\\\");
                    }else{
                        str += stringify(arr[i]);
                    }
                    
                    if(i != arr.length - 1)str += ",";
                }

                buff.append("[");
                buff.append(str);
                buff.append("]o");// ??????????????//
            }else{
                buff.append("\"");
                buff.append(value.toString().replace("\\", "\\\\").replace("\"", "\\\""));
                buff.append("\",");
            }
            
        }catch(IllegalArgumentException e){
        	e.printStackTrace();
        }catch (IllegalAccessException e) {
        	e.printStackTrace();
        //}catch(java.lang.reflect.InvocationTargetException e) {
            //e.printStackTrace();
        }
    }

    if (buff.length() > 1)buff = buff.deleteCharAt(buff.length() - 1);
 
    buff.append("}");

    return buff.toString();
}

需要指出的是,该方法不支持多层的的、嵌套的 Object。有兴趣深入反射的朋友可以看看下面两篇文章,我就是跟他们学习而来的。

输入 JSON 字符串,返回  Java 对象

JSON 字符串一般是从接口返回而来的,然后在 Java 环境中使用。那 JSON 转换为什么 Java 对象?我觉得 Map 就可以了。于是我们 API 提供这个两个方法:

/**
 * 输入 JSON 字符串,返回 Map
 * @param jsonStr
 * @return
 */
public Map<String, Object> parse(String jsonStr){
	return NativeObject2Hash(jsonStr);
}

/**
 *  输入 JSON 字符串,返回 Map 数组
 * @param jsonStr
 * @return
 */
public Map<String, Object>[] parseArr(String jsonStr){
	return NativeArray2Map(jsonStr);
}

用户要知道是 JSON 对象还是 JSON 数组才选择哪个方法来调用。

背后的调用的方法是 Nativebject2Hash。顾名思义,是 NativeObject “To” HashMap。NativeObject 是 Rhino 里表示 Object 类型,NativeArray 则是数组。Nativebject2Hash 安排有若干重载的方法:

  • public static Map<String, Object> NativeObject2Hash(String jsCode);
  • public static Map<String, Object> NativeObject2Hash(Object obj);
  • public static Map<String, Object> NativeObject2Hash(NativeObject obj);

Object obj 实际是 String/Nativeobject 类型中的一种。NativeObject2Hash(String jsCode) 对送入的 JSON 字符串经过 JsEngine.eval() 得到 NativeObject;最后归根到底处理的方法仍是 NativeObject2Hash(NativeObject obj)。源码如下。

/**
 * JS Object 对象转换到 Java Hash 对象
 * @param obj
 * @return
 */
public static Map<String, Object> NativeObject2Hash(NativeObject obj){
    Map<String, Object> hash = new HashMap<String, Object>();
    
    for (Object id : obj.getAllIds()) {// 遍历对象
    	String newId = (String)id;
    	Object value = obj.get(newId, obj);
        
       if(value instanceof Boolean){
    	   hash.put(newId, value);
	   }else if(value instanceof String){
    	   hash.put(newId, value);
       }else if(value instanceof Double){
    	   hash.put(newId, ((Double)value).intValue());// js number 转换为 int
       }else if(value instanceof NativeObject){
    	   hash.put(newId, NativeObject2Hash(value));
       }else if(value instanceof NativeArray){
    	   hash.put(newId, NativeArray2Map(value)); // 这是规则的情况,数组中每个都是对象,而非 string/int/boolean @todo
       }else{
    	   if(value == null)
    		   hash.put(newId, null); // js 为 null,所以 java hash 也为null
    	   else 
    		   System.out.println("未知 JS 类型:" + value.getClass().getName());
       }
    }
    
    return hash;
}

如上所述,仍是反射的原理。

数组类型为 NativeArray,返回 Map<String, OBject>[]。NativeArray2Map 无非只是一个遍历过程。

/**
 * JS Array 对象转换到 Java Array 对象
 * @param nativeArr
 * @return
 */
public static Map<String, Object>[] NativeArray2Map(NativeArray nativeArr){
    Long len = nativeArr.getLength();
    
    @SuppressWarnings("unchecked")
	Map<String, Object>[] arr = new HashMap[len.intValue()];
    
    int i = 0;
	for(Object obj : nativeArr){ // JDK7 supports directly
		if(obj instanceof NativeObject){
			arr[i++] = NativeObject2Hash((NativeObject)obj);
		}else if(obj instanceof NativeArray){
			System.out.println("TODO NativeArray");
		}
	}
    
    return arr;
}

JDK 6 的时代不能直接遍历 NativeArray,需要 nativeArr.getIds()。

public static Map<String, Object>[] NativeArray2Map(NativeArray nativeArr){
	    Long len = nativeArr.getLength();
	    
	    @SuppressWarnings("unchecked")
		Map<String, Object>[] arr = new HashMap[len.intValue()];
	    
	    // JDK7 supprts directly
//		for(Object item : arr){
//			Map<String, Object> hash = NativeObject2Hash((NativeObject)item);
//			list.add(hash);
//		}
	    
	    // JDK6
	    for (Object o : nativeArr.getIds()) {
	        Integer index = (Integer)o;
	        // what about arr. not nativeObj
	        // either obj or arr here
	        if(nativeArr.get(index, nativeArr) instanceof NativeObject){
	        	NativeObject nativeObj = (NativeObject)nativeArr.get(index, nativeArr);
	        	arr[index.intValue()] = NativeObject2Hash(nativeObj);
	        }else if(nativeArr.get(index, nativeArr) instanceof NativeArray){
	        	System.out.println("NativeArray");
	        }
	    }

	    return arr;
}

最后我们看看例子。

@Test
public void testNativeObject2Hash(){
	Map<String, Object> map = Mapper.NativeObject2Hash("{'a':0, 'b':1}");
	assertNotNull(map);
	assertEquals(map.get("a"), 0);
	
	Object obj = "{'a':0, 'b':1};"; 
	map = Mapper.NativeObject2Hash(obj);
	assertNotNull(map);
	assertEquals(map.get("b"), 1);
	
	JsEngine js = new JsEngine();
	obj = js.eval_return_Object("json = " + obj.toString());
	map = Mapper.NativeObject2Hash((NativeObject)obj);
	assertNotNull(map);
	assertEquals(map.get("b"), 1);
	
	map = Mapper.NativeObject2Hash("var json = {'a':0, 'b':1};", "json");
	assertNotNull(map);
	assertEquals(map.get("b"), 1);
}

@Test
public void testNativeArray2Map(){
	String arrStr = "[{'a':0, 'b':1}, {'c':2}]";
	Map<String, Object>[] map = Mapper.NativeArray2Map(arrStr);
	assertNotNull(map[0]);
	assertEquals(map[0].get("a"), 0);
	
	JsEngine js = new JsEngine();
	Object obj = js.eval_return_Object("json = " + arrStr);
	map = Mapper.NativeArray2Map(obj);
	assertNotNull(map[0]);
	assertEquals(map[0].get("b"), 1);
	
	map = Mapper.NativeArray2Map((NativeArray)obj);
	assertNotNull(map[1]);
	assertEquals(map[1].get("c"), 2);
}

最后,欢迎大家提出意见!

2015-6-16 edit:

JDK 8 问题:

新的 JDK 带来了新的 JS 引擎,但是 NactiveObject、NactiveArray 这些对象的 API 均发生变化,应该说没有这些包了。所以,当前不准备贸然升级 8。

2016-1-13 edit:

终于让 Java 7 更新到 Narshorn!参见我的博文《Java 7 可运行的 Nashorn,代替 Rhino》


前端和后端同用一套 js

import javax.script.ScriptException;
import javax.servlet.jsp.PageContext;

import com.ajaxjs.app.App;
import com.ajaxjs.json.IEngine;

public class Cross{
	public static IEngine runtime; // 单例
	
	static{
		runtime = App.jsRuntime;
		runtime.load(Cross.class, "Js.js");

		try {
			runtime.call(View, "setObj", "sss", "sdsd");
			runtime.eval("self = this;");
			
			runtime.eval("function write(str){" +
						"pageContext.getOut().println(str);" +
					"}");
			runtime.eval("function setObj(globalVarName, obj){" +
				"self[globalVarName] = obj;" +
			"}");
		} catch (ScriptException e) {
			e.printStackTrace();
		}
	}

	public static void init(PageContext pageContext){
		Object View = runtime.get("View");
		
		Cross.runtime.call("setObj", Object.class, View, "pageContext", pageContext);
	}
}

js

View = {
	init : function(){
		View.setObj('request',  pageContext.getRequest());
		View.setObj('response', pageContext.getResponse());
		View.setObj('out', pageContext.getOut()());
	}
};
self = this;

View.setObj = function (globalVarName, obj){
	self[globalVarName] = obj;
};

View.write = function (str){
	pageContext.getOut().println(str);
};


上一篇:云原生生态周报 Vol. 4 | Twitter 走向 K8s


下一篇:JavaScriptSerializer 对json数据转换