Android JS解析引擎 Rhino 使用笔记(不借助webview)

来源:互联网 发布:python游戏开发pdf 编辑:程序博客网 时间:2024/05/22 09:02

在使用过程中有个需求是在不大改动移动端现有处理逻辑的基础上,通过后期配置来灵活更改本地的逻辑联系。最终选定的方案是借助Js,一开始想到用webview,但webview开销大。经查找,最终使用了 Rhino。
注:本文主要参考自【Android】不使用WebView来执行Javascript脚本(Rhino)


Rhino 简介

(摘自:https://www.ibm.com/developerworks/cn/java/j-lo-rhino/)

Rhino 是开源的 JavaScript 引擎,是完全基于 Java 实现,几乎可以使用 JavaScript 完成 Java 所有的工作。它可以提供强大的计算能力,没有 I/O 的限制,可以将 JavaScript 编译成 Java 字节码,具有良好的速度和性能。在 Rhino 环境中既可以使用 JavaScript 脚本语言,同时也可以非常简单的使用 Java 语言的某些工具。Rhino 为我们提供了如下功能:
- 对 JavaScript 1.5 的完全支持
- 直接在 Java 中使用 JavaScript 的功能
- 一个 JavaScript shell 用于运行 JavaScript 脚本
- 一个 JavaScript 的编译器,用于将 JavaScript 编译成 Java 二进制文件

github 地址:https://github.com/mozilla/rhino
Rhino 官网地址 : https://developer.mozilla.org/zh-CN/docs/Mozilla/Projects/Rhino


AndroidStudo 中导入Rhino

  1. 下载Rhino jar 包。 Rhino jar 包下载
  2. 将jar包放入libs文件夹下。右击该jar包,选择 add as library,选择Model导入

基本使用

基本使用参考 【Android】不使用WebView来执行Javascript脚本(Rhino)
或者官方的示例


封装

在参考的博客中,Rhino嵌在Activity中。由于项目中多个地方需要使用到,所以需要将其封装起来。
另外,由于主要功能是运行js语句来调用本地的java方法,故封装也主要是实现js调用java

/** *  JS解析封装 */public class JSEngine{    private Class clazz;    private String allFunctions ="";//js方法语句    public JSEngine(){        this.clazz = JSEngine.class;        initJSStr();//初始化js语句    }    private void initJSStr(){        /**         * 在此处可以看到 javaContext、javaLoader的应用,         * 基本使用原理应该是利用类名、类加载器和上下文去获取JSEngine的类和方法         * 注意method的输入参数类型与本地方法的对应         */        allFunctions =                 " var ScriptAPI = java.lang.Class.forName(\"" + JSEngine.class.getName() + "\", true, javaLoader);\n" +                " var methodGetValue=  ScriptAPI.getMethod(\"getValue\", [java.lang.String]);\n" +                " function getValue(key) {\n" +                "       return  methodGetValue.invoke(javaContext,key);\n" +                " }\n" +                " var methodSetValue=ScriptAPI.getMethod(\"setValue\",[java.lang.Object,java.lang.Object]);\n" +                " function setValue(key,value) {\n" +                "       methodSetValue.invoke(javaContext,key,value);\n" +                " }\n";    }    //本地java方法    public void setValue(Object keyStr, Object o) {        System.out.println("JSEngine output - setValue : " + keyStr.toString() + " ------> " + o.toString());    }    //本地java方法    public String getValue(String keyStr) {        System.out.println("JSEngine output - getValue : " + keyStr.toString() );        return "获取到值了";    }    /**     * 执行JS     * @param js  js执行代码 eg: "var v1 = getValue('Ta');setValue(‘key’,v1);"     */    public void runScript(String js){        String runJSStr = allFunctions + "\n" + js;//运行js = allFunctions + js        org.mozilla.javascript.Context rhino = org.mozilla.javascript.Context.enter();        rhino.setOptimizationLevel()-1;        try {            Scriptable scope = rhino.initStandardObjects();            ScriptableObject.putProperty(scope, "javaContext", org.mozilla.javascript.Context.javaToJS(this, scope));//配置属性 javaContext:当前类JSEngine的上下文            ScriptableObject.putProperty(scope, "javaLoader", org.mozilla.javascript.Context.javaToJS(clazz.getClassLoader(), scope));//配置属性 javaLoader:当前类的JSEngine的类加载器            rhino.evaluateString(scope, runJSStr, clazz.getSimpleName(), 1, null);        } finally {            org.mozilla.javascript.Context.exit();        }    }}

使用测试

public class JSActivity extends Activity{    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_js);        JSEngine jsEngine = new JSEngine();        jsEngine.runScript(testjs);    }    private String testjs ="var val = getValue('testKey');" +                           "setValue('setKey',val)";  }                           

结果输出如下:

System.out: JSEngine output - getValue : testKeySystem.out: JSEngine output - setValue : setKey ------> 获取到值了

方法测试 - 返回本地类

  1. 添加本地测试类
class TestObject{    private String name;    private String address;    TestObject(String name ,String address){        this.name = name;        this.address = address;    }}
  1. 为JSEngine添加方法 getObjectValue
public Object getObjectValue(Object keyStr){    System.out.println("JSEngine output - getObjectValue : " + keyStr.toString());    return new TestObject("小明","广州");}
  1. allFunctions 添加 getObjectValue 声明
allFunctions =                " var ScriptAPI = java.lang.Class.forName(\"" + JSEngine.class.getName() + "\", true, javaLoader);\n" +                " var methodGetValue=  ScriptAPI.getMethod(\"getValue\", [java.lang.String]);\n" +                " function getValue(key) {\n" +                "       return  methodGetValue.invoke(javaContext,key);\n" +                " }\n" +                " var methodSetValue=ScriptAPI.getMethod(\"setValue\",[java.lang.Object,java.lang.Object]);\n" +                " function setValue(key,value) {\n" +                "       methodSetValue.invoke(javaContext,key,value);\n" +                " }\n"+                " var methodGetObjectValue=ScriptAPI.getMethod(\"getObjectValue\",[java.lang.Object]);\n" +                " function getObjectValue(key) {\n" +                "       return methodGetObjectValue.invoke(javaContext,key);\n" +                " }\n";
  1. 修改执行语句,尝试获取 name属性的值
private String testjs =                         "var test = getObjectValue('objectKey');" +                        "setValue('testvalue',test.name);";
  1. 运行,发现无法获取到属性name的值
System.out: JSEngine output - setValue : testvalue ------> undefined

解决方案
思路:先将返回的对象转成字符串,再利用 javascript 的 eval 函数将字符串转成符合要求的对象
此时,需要修改 JSEngine 中的getObjectValue方法和 allFunctions 中的 getObjectValue 方法

//修改 JSEngine 中的getObjectValue方法public String getObjectValue(Object keyStr){    System.out.println("JSEngine output - getObjectValue : " + keyStr.toString());    return new Gson().toJson(new TestObject("小明","广州"));//利用Gson 将 TestObject对象先转成String}
//修改 allFunctions 中的getObjectValue方法  " var methodGetObjectValue=ScriptAPI.getMethod(\"getObjectValue\",[java.lang.Object]);\n" +" function getObjectValue(key) {\n" +"       var retStr = methodGetObjectValue.invoke(javaContext,key);\n" +"       var ret = {};" +"       eval('ret='+retStr);" +"       return ret;" +" }\n";

仍执行以下语句

private String testjs =                         "var test = getObjectValue('objectKey');" +                        "setValue('testvalue',test.name);";

输出如下:可以看到能使用 .name 的形式获取到 name属性的值

System.out: JSEngine output - getObjectValue : objectKeySystem.out: JSEngine output - setValue : testvalue ------> 小明

反射构建js语句

通过上面可以看到,在JSEngine中每添加一个方法,在JS语句中也要对应多添加一个方法。而在js语句的编写过程中则需要注意多处细节,比较容易书写错误,所以能否自动生成js语句而不用每次都手写呢?
有的,利用注解和反射。
首先观察前面的js语句
每个本地方法在js中的定义主要包括两部分:
1、通过本地方法的方法名来获得该方法

var methodGetValue=  ScriptAPI.getMethod(\"getValue\", [java.lang.String]);\n

2、自定义js方法(可重新命名),在js方法中调用本地方法的引用

function getValue(key) {    return  methodGetValue.invoke(javaContext,key);}

其他有差异的话则在于返回值类型为本地类对象时候的js方法的不同,如

function getObjectValue(key) {       var retStr = methodGetObjectValue.invoke(javaContext,key);       var ret = {};       eval('ret='+retStr);       return ret; }

下面开始反射构建js语句
1、创建注解 JSAnnotation ,设定参数 returnObject ,用于区分上面所述的方法是否返回本地类对象。

/** * 注解 */@Target(value = ElementType.METHOD)@Retention(value = RetentionPolicy.RUNTIME)public @interface JSAnnotation {    boolean returnObject() default false;//是否返回对象,默认为false 不返回}

2、为方法添加注解

//本地java方法,声明注解@JSAnnotationpublic void setValue(Object keyStr, Object o) {    System.out.println("JSEngine output - setValue : " + keyStr.toString() + " ------> " + o.toString());}//本地java方法,声明注解@JSAnnotationpublic String getValue(String keyStr) {    System.out.println("JSEngine output - getValue : " + keyStr.toString() );    return "获取到值了";}//有返回本地类对象,则returnObject 设置为true    @JSAnnotation(returnObject = true)    function getObjectValue(key) {   var retStr = methodGetObjectValue.invoke(javaContext,key);   var ret = {};   eval('ret='+retStr);   return ret;}

3、利用注解生成js语句

/** * 通过注解自动生成js方法语句 */private String getAllFunctions(){    String funcStr =  " var ScriptAPI = java.lang.Class.forName(\"%s\", true, javaLoader);\n" ;    Class cls = this.getClass();    for (Method method: cls.getDeclaredMethods()){        JSAnnotation an = method.getAnnotation(JSAnnotation.class);        if (an == null ) continue;        String functionName = method.getName();        String paramsTypeString  ="";//获取function的参数类型        String paramsNameString = "";//获取function的参数名称        String paramsNameInvokeString = "";        Class [] parmTypeArray = method.getParameterTypes();        if (parmTypeArray != null && parmTypeArray.length > 0){            String[] parmStrArray = new String[parmTypeArray.length];            String[] parmNameArray = new String[parmTypeArray.length];            for (int i=0;i < parmTypeArray.length; i++){                parmStrArray[i] = parmTypeArray[i].getName();                parmNameArray[i] = "param" + i ;            }            paramsTypeString = String.format(",[%s]",TextUtils.join(",",parmStrArray));            paramsNameString = TextUtils.join(",",parmNameArray);            paramsNameInvokeString = "," + paramsNameString;        }        Class returnType = method.getReturnType();        String returnStr = returnType.getSimpleName().equals("void") ? "" : "return";//是否有返回值        String methodStr = String.format(" var method_%s = ScriptAPI.getMethod(\"%s\"%s);\n",functionName,functionName,paramsTypeString);        String functionStr = "";        if (an.returnObject()){//返回对象            functionStr = String.format(                    " function %s(%s){\n" +                    "    var retStr = method_%s.invoke(javaContext%s);\n" +                    "    var ret = {} ;\n" +                    "    eval('ret='+retStr);\n" +                    "    return ret;\n" +                    " }\n"  ,functionName,paramsNameString,functionName,paramsNameInvokeString );        }else {//非返回对象            functionStr = String.format(                    " function %s(%s){\n" +                    "    %s method_%s.invoke(javaContext%s);\n" +                    " }\n",functionName,paramsNameString,returnStr,functionName,paramsNameInvokeString );        }        funcStr = funcStr + methodStr + functionStr;    }    return funcStr;}

js自动生成的完整封装

public class JSEngine {    private Class clazz;    private String allFunctions ="";//js方法语句    public JSEngine(){        this.clazz = JSEngine.class;        allFunctions = String.format(getAllFunctions(), clazz.getName());//生成js语法    }    class TestObject{        private String name;        private String address;        TestObject(String name ,String address){            this.name = name;            this.address = address;        }    }    /**     * 本地方法 - 返回本地类对象     * @param keyStr     * @return     */    @JSAnnotation(returnObject = true)    public String getObjectValue(Object keyStr){        System.out.println("JSEngine output - getObjectValue : " + keyStr.toString());        return new Gson().toJson(new TestObject("小明","广州"));    }    /**     * 本地java方法     * @param keyStr     * @param o     */    @JSAnnotation    public void setValue(Object keyStr, Object o) {        System.out.println("JSEngine output - setValue : " + keyStr.toString() + " ------> " + o.toString());    }    /**     * 本地java     * @param keyStr     * @return     */    @JSAnnotation    public String getValue(String keyStr) {        System.out.println("JSEngine output - getValue : " + keyStr.toString() );        return "获取到值了";    }    /**     * 执行JS     * @param js  js执行代码 eg: "var v1 = getValue('Ta');setValue(‘key’,v1);"     */    public void runScript(String js){        String runJSStr = allFunctions + "\n" + js;//运行js = allFunctions + js        org.mozilla.javascript.Context rhino = org.mozilla.javascript.Context.enter();        rhino.setOptimizationLevel(-1);        try {            Scriptable scope = rhino.initStandardObjects();            ScriptableObject.putProperty(scope, "javaContext", org.mozilla.javascript.Context.javaToJS(this, scope));//配置属性 javaContext:当前类JSEngine的上下文            ScriptableObject.putProperty(scope, "javaLoader", org.mozilla.javascript.Context.javaToJS(clazz.getClassLoader(), scope));//配置属性 javaLoader:当前类的JSEngine的类加载器            rhino.evaluateString(scope, runJSStr, clazz.getSimpleName(), 1, null);        } finally {            org.mozilla.javascript.Context.exit();        }    }    /**     * 通过注解自动生成js方法语句     */    private String getAllFunctions(){        String funcStr =  " var ScriptAPI = java.lang.Class.forName(\"%s\", true, javaLoader);\n" ;        Class cls = this.getClass();        for (Method method: cls.getDeclaredMethods()){            JSAnnotation an = method.getAnnotation(JSAnnotation.class);            if (an == null ) continue;            String functionName = method.getName();            String paramsTypeString  ="";//获取function的参数类型            String paramsNameString = "";//获取function的参数名称            String paramsNameInvokeString = "";            Class [] parmTypeArray = method.getParameterTypes();            if (parmTypeArray != null && parmTypeArray.length > 0){                String[] parmStrArray = new String[parmTypeArray.length];                String[] parmNameArray = new String[parmTypeArray.length];                for (int i=0;i < parmTypeArray.length; i++){                    parmStrArray[i] = parmTypeArray[i].getName();                    parmNameArray[i] = "param" + i ;                }                paramsTypeString = String.format(",[%s]", TextUtils.join(",",parmStrArray));                paramsNameString = TextUtils.join(",",parmNameArray);                paramsNameInvokeString = "," + paramsNameString;            }            Class returnType = method.getReturnType();            String returnStr = returnType.getSimpleName().equals("void") ? "" : "return";//是否有返回值            String methodStr = String.format(" var method_%s = ScriptAPI.getMethod(\"%s\"%s);\n",functionName,functionName,paramsTypeString);            String functionStr = "";            if (an.returnObject()){//返回对象                functionStr = String.format(                        " function %s(%s){\n" +                        "    var retStr = method_%s.invoke(javaContext%s);\n" +                        "    var ret = {} ;\n" +                        "    eval('ret='+retStr);\n" +                        "    return ret;\n" +                        " }\n"  ,functionName,paramsNameString,functionName,paramsNameInvokeString );            }else {//非返回对象                functionStr = String.format(                        " function %s(%s){\n" +                        "    %s method_%s.invoke(javaContext%s);\n" +                        " }\n",functionName,paramsNameString,returnStr,functionName,paramsNameInvokeString );            }            funcStr = funcStr + methodStr + functionStr;        }        return funcStr;    }    /**     * 注解     */    @Target(value = ElementType.METHOD)    @Retention(value = RetentionPolicy.RUNTIME)    public @interface JSAnnotation {        boolean returnObject() default false;//是否返回对象,默认为false 不返回    }}

  • 补充:运行在子线程
AsyncTask task = new AsyncTask() {    @Override    protected Object doInBackground(Object[] params) {        if (ukjsEngine == null) ukjsEngine = new UKJSEngine(ukjsEngineListener);        ukjsEngine.runScript(jsStr);        return null;    }};task.execute();

则引擎初始化与执行 runScript 需要都在子线程中;如果引擎初始化在主线程,而runScript在子线程,则会报错

  • ConsString 问题
var data = {          "entityId": "2c63b681-1de9-41b7-9f98-4cf26fd37ef1",          "recId": id,          "needPower": 0      };var result = app.request('api/dyxxxxxity/dxxxxl', 'post', data, {});

在本地的request() 方法中,对data进行转化处理

mArgs = new Gson().toJson(data );

如下图,可以看到数据是正常的
这里写图片描述

但在下面实例中:

 var productseries = app.getValue('xilie'); var data = {   "ProductsetId":productseries,   "Direction":"DOWNER" }; var result = app.request('api/prxxxxts/gexxxxes', 'post', data, {});

这里写图片描述

可以看到本地获取到 ProductsetId 参数类型为 ConsString,从而导致
在进行 Gson().toJson(data ) 操作获得的结果有问题

    /**     * 参数调整:     * 存在问题:从js传入的JSON 对象,类型变为 NativeObject;而NativeObject 中的String类型可能被js转为     * ConsString 类型;用 Gson.toJson(xxx) 处理带有ConsString 类型的数据会出现异常。其中的ConsString     * 类型的数据转化出来并不是 String 类型,而是一个特殊对象。     * 解决方案:遍历 NativeObject 对象,将其中的 ConsString 类型的数据转为 String 类型     * @param input     * @return     */    public static Object argsNativeObjectAdjust(Object input) {        if (input instanceof NativeObject){            JSONObject bodyJson = new JSONObject();            NativeObject nativeBody = (NativeObject) input;            for (Object key : nativeBody.keySet()){                Object value = nativeBody.get(key);                value = argsNativeObjectAdjust(value);                try {                    bodyJson.put((String) key,value);                } catch (JSONException e) {                    e.printStackTrace();                }            }            return bodyJson;        }        if (input instanceof NativeArray){            JSONArray jsonArray = new JSONArray();            NativeArray nativeArray = (NativeArray) input;            for (int i = 0; i < nativeArray.size() ; i++){                Object value = nativeArray.get(i);                value = argsNativeObjectAdjust(value);                jsonArray.put(value);            }            return jsonArray;        }        if (input instanceof ConsString){            return input.toString();        }        return input;    }

参考资料

  • Rhino官网
    https://developer.mozilla.org/zh-CN/docs/Mozilla/Projects/Rhino#Rhino_downloads
  • Rhino 使 JavaScript 应用程序更灵动
    https://www.ibm.com/developerworks/cn/java/j-lo-rhino/
  • 【Android】不使用WebView来执行Javascript脚本(Rhino)本文也主要是参考该文章
    http://www.cnblogs.com/over140/p/3389974.html

  • Rhino 文档
    https://developer.mozilla.org/zh-CN/docs/Mozilla/Projects/Rhino/Documentation

  • Rhino API
    http://mozilla.github.io/rhino/javadoc/index.html

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