Когда метод java (статический или нестатический) должен быть доступен как глобальная функция в области видимости, мы используем следующую логику:
FunctionObject javascriptFunction = new FunctionObject(/* String*/ javascriptFunctionName, /* Method */ javaMethod, /*Scriptable */ parentScope);
boundScope.put(javascriptFunctionName, boundScope, javascriptFunction);
Здесь boundScope
всегда должно быть областью, в которой должна быть доступна функция.
Однако значение родительской области зависит от того, связываем ли мы метод экземпляра или статический метод. В случае статического метода это может быть любая область, которая имеет смысл. Он может быть таким же, как boundScope
.
Но в случае метода экземпляра, parentScope
должен быть экземпляром, метод которого привязан.
Выше была просто справочная информация. Теперь я объясню, в чем заключается проблема, и дам для нее естественное решение, то есть такое, которое позволяет напрямую вызывать метод экземпляра как глобальную функцию, а не явно создавать экземпляр объекта, а затем вызывать метод с использованием этого экземпляра.
Когда вызывается функция, Rhino вызывает метод FunctionObject.call()
, которому передается ссылка на this
. В случае, если функция является глобальной функцией, она вызывается без ссылки на this
(т.е. xxx()
вместо this.xxx()
), значение переменной this
, которая передается методу FunctionObject.call()
, является область, в которой был сделан вызов (т. е. в этом случае значение параметра this
будет таким же, как значение параметра scope
).
Это становится проблемой в случае, если вызываемый java-метод является методом экземпляра, потому что согласно JavaDocs конструктора FunctionObject
класса:
Если метод не является статическим, значение Java this
будет соответствовать значению JavaScript this
. Любая попытка вызвать функцию со значением this
, которое имеет неправильный тип Java, приведет к ошибке.
И в описанном выше сценарии это именно так. Значение javascript this
НЕ соответствует значению java this
и приводит к ошибке несовместимого объекта.
Решение состоит в том, чтобы создать подкласс FunctionObject
, переопределить метод call()
, принудительно "исправить" ссылку this
, а затем позволить вызову продолжаться нормально.
Так что-то вроде:
FunctionObject javascriptFunction = new MyFunctionObject(javascriptFunctionName, javaMethod, parentScope);
boundScope.put(javascriptFunctionName, boundScope, javascriptFunction);
private static class MyFunctionObject extends FunctionObject {
private MyFunctionObject(String name, Member methodOrConstructor, Scriptable parentScope) {
super(name, methodOrConstructor, parentScope);
}
@Override
public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return super.call(cx, scope, getParentScope(), args);
}
}
Я думаю, что это было бы лучше всего понять с помощью отдельного / полного примера, вставленного ниже. В этом примере мы представляем метод экземпляра: myJavaInstanceMethod (Double number) как глобальную функцию в области видимости javascript ('scriptExecutionScope'). Поэтому в этом случае значение параметра parentScope должно быть экземпляром класса, который содержит этот метод (т. Е. MyScriptable).
package test;
import org.mozilla.javascript.*;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
//-- This is the class whose instance method will be made available in a JavaScript scope as a global function.
//-- It extends from ScriptableObject because instance methods of only scriptable objects can be directly exposed
//-- in a js scope as a global function.
public class MyScriptable extends ScriptableObject {
public static void main(String args[]) throws Exception {
Context.enter();
try {
//-- Create a top-level scope in which we will execute a simple test script to test if things are working or not.
Scriptable scriptExecutionScope = new ImporterTopLevel(Context.getCurrentContext());
//-- Create an instance of the class whose instance method is to be made available in javascript as a global function.
Scriptable myScriptable = new MyScriptable();
//-- This is not strictly required but it is a good practice to set the parent of all scriptable objects
//-- except in case of a top-level scriptable.
myScriptable.setParentScope(scriptExecutionScope);
//-- Get a reference to the instance method this is to be made available in javascript as a global function.
Method scriptableInstanceMethod = MyScriptable.class.getMethod("myJavaInstanceMethod", new Class[]{Double.class});
//-- Choose a name to be used for invoking the above instance method from within javascript.
String javascriptFunctionName = "myJavascriptGlobalFunction";
//-- Create the FunctionObject that binds the above function name to the instance method.
FunctionObject scriptableInstanceMethodBoundJavascriptFunction = new MyFunctionObject(javascriptFunctionName,
scriptableInstanceMethod, myScriptable);
//-- Make it accessible within the scriptExecutionScope.
scriptExecutionScope.put(javascriptFunctionName, scriptExecutionScope,
scriptableInstanceMethodBoundJavascriptFunction);
//-- Define a simple test script to test if things are working or not.
String testScript = "function simpleJavascriptFunction() {" +
" try {" +
" result = myJavascriptGlobalFunction(12.34);" +
" java.lang.System.out.println(result);" +
" }" +
" catch(e) {" +
" throw e;" +
" }" +
"}" +
"simpleJavascriptFunction();";
//-- Compile the test script.
Script compiledScript = Context.getCurrentContext().compileString(testScript, "My Test Script", 1, null);
//-- Execute the test script.
compiledScript.exec(Context.getCurrentContext(), scriptExecutionScope);
} catch (Exception e) {
throw e;
} finally {
Context.exit();
}
}
public Double myJavaInstanceMethod(Double number) {
return number * 2.0d;
}
@Override
public String getClassName() {
return getClass().getName();
}
private static class MyFunctionObject extends FunctionObject {
private MyFunctionObject(String name, Member methodOrConstructor, Scriptable parentScope) {
super(name, methodOrConstructor, parentScope);
}
@Override
public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return super.call(cx, scope, getParentScope(), args);
// return super.call(cx, scope, thisObj, args);
}
}
}
Если вы хотите увидеть поведение С исправлением, раскомментируйте строку 78 и строку комментария 79:
return super.call(cx, scope, getParentScope(), args);
//return super.call(cx, scope, thisObj, args);
Если вы хотите увидеть поведение без исправления, тогда прокомментируйте строку 78 и строку комментария 79:
//return super.call(cx, scope, getParentScope(), args);
return super.call(cx, scope, thisObj, args);
Надеюсь, это поможет.