您的位置:首页 > 移动开发 > Android开发

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

2017-07-24 09:35 537 查看
在使用过程中有个需求是在不大改动移动端现有处理逻辑的基础上,通过后期配置来灵活更改本地的逻辑联系。最终选定的方案是借助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

下载Rhino jar 包。 Rhino jar 包下载

将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 ------> 获取到值了


方法测试 - 返回本地类

添加本地测试类

class TestObject{
private String name;
private String address;

TestObject(String name ,String address){
this.name = name;
this.address = address;
}
}


为JSEngine添加方法 getObjectValue

public Object getObjectValue(Object keyStr){
System.out.println("JSEngine output - getObjectValue : " + keyStr.toString());
return new TestObject("小明","广州");
}


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";


修改执行语句,尝试获取 name属性的值

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


运行,发现无法获取到属性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(
ede8
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 解析/转换包
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android js解析