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

在使用过程中有个需求是在不大改动移动端现有处理逻辑的基础上,通过后期配置来灵活更改本地的逻辑联系。最终选定的方案是借助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 : testKey
System.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 : objectKey
System.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方法,声明注解
@JSAnnotation
public void setValue(Object keyStr, Object o) {
    System.out.println("JSEngine output - setValue : " + keyStr.toString() + " ------> " + o.toString());
}

//本地java方法,声明注解
@JSAnnotation
public 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;
    }

参考资料

猜你喜欢

转载自blog.csdn.net/haha_zhan/article/details/75967225
今日推荐