How do I call a method of a Java instance from Jav

2020-02-09 10:39发布

问题:

I'm using the Mozilla Rhino JavaScript emulator. It allows me to add Java methods to a context and then call them as if they were JavaScript functions. But I can't get it to work except when I use a static method.

The problem is this part of the documentation:

If the method is not static, the Java 'this' value will correspond to the JavaScript 'this' value. Any attempt to call the function with a 'this' value that is not of the right Java type will result in an error.

Apparently, my Java "this" value doesn't correspond with the one in JavaScript and I have no idea how to make them correspond. In the end, I'd like to create an instance in Java, and install a couple of methods from it in the global scope, so I can initialize the instance from Java but use it in my scripts.

Does anyone have some example code for this?

回答1:

When a java method (whether static or non-static) is to be made available as a global function within a scope we use the following logic:

FunctionObject javascriptFunction = new FunctionObject(/* String*/ javascriptFunctionName, /* Method */ javaMethod, /*Scriptable */ parentScope);
boundScope.put(javascriptFunctionName, boundScope, javascriptFunction);

Here the boundScope should always be the scope in which the function is to be made available.

However the value of the parent scope depends on whether we are binding an instance method or static method. In case of a static method, it can be any scope that makes sense. It can even be the same as the boundScope.

But in case of instance method, the parentScope should be the instance whose method is being bound.

The above was just background info. Now I will explain what the issue is and give a natural solution for it i.e. one that allows invoking the instance method directly as a global function rather than explicitly creating an instance of the object and then invoking the method using that instance.

When a function is called, Rhino invokes the FunctionObject.call() method that is passed a reference to this. In case the function is a global function, it is called without a reference to this (i.e. xxx() instead of this.xxx()), the value of the this variable that gets passed to the FunctionObject.call() method is the scope in which the call was made (i.e. in this case the value of the this parameter will be same as the value of the scope parameter).

This becomes a problem in case the java method being invoked is an instance method because as per the JavaDocs of constructor of FunctionObject class:

If the method is not static, the Java this value will correspond to the JavaScript this value. Any attempt to call the function with a this value that is not of the right Java type will result in an error.

And in the scenario described above that is exactly the case. The javascript this value does NOT correspond to the java this value and results in an incompatible object error.

The solution is to subclass FunctionObject, override the call() method, forcefully 'fix' the this reference, and then let the call proceed normally.

So something like:

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);
    }
  }

I think it would be best understood with a self-contained/complete example pasted below. In this example, we are exposing the instance method: myJavaInstanceMethod(Double number) as a global function within a javascript scope ('scriptExecutionScope'). So in this case the value of the 'parentScope' parameter must be an instance of the class that contains this method (i.e. 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);
    }
  }
}

If you want to see the behavior WITH the fix then uncomment line 78 and comment line 79:

return super.call(cx, scope, getParentScope(), args);
//return super.call(cx, scope, thisObj, args);

If you want to see the behavior WITHOUT the fix then comment line 78 and uncomment line 79:

//return super.call(cx, scope, getParentScope(), args);
return super.call(cx, scope, thisObj, args);

Hope this helps.



回答2:

What you can do is to bind a Java instance to the Javascript context, and then from Javascript that identifier will be a reference to the "real" Java object. You can then use it to make method calls from Javascript to Java.

Java side:

    final Bindings bindings = engine.createBindings();
    bindings.put("javaObject", new YourJavaClass());
    engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);

Javascript:

    javaObject.methodName("something", "something");

Now that example assumes you're using the JDK 6 java.util.script APIs to get between Java and Rhino. From "plain" Rhino, it's a little different but the basic idea is the same.

Alternatively, you can import Java classes into the Javascript environment, and Rhino gives you Javascript-domain references to Java objects when you use Javascript "new" on references to Java classes.