在 GraalVM Polyglot 上下文中从 JavaScript 访问 Java 对象

Access Java object from JavaScript in GraalVM Polyglot context

运行 在 GraalVM CE 上。

openjdk version "11.0.5" 2019-10-15
OpenJDK Runtime Environment (build 11.0.5+10-jvmci-19.3-b05-LTS)
OpenJDK 64-Bit GraalVM CE 19.3.0 (build 11.0.5+10-jvmci-19.3-b05-LTS, mixed mode, sharing)

案例一:

import org.graalvm.polyglot.Context;

public class Test {

    static class Data {
        public String name = "HelloWorld";
        public String getName() {
            return this.name;
        }
    }

    public static void main(String[] args) {
        Context context = Context.newBuilder("js").allowHostAccess(true).build();
        context.getBindings("js").putMember("d", new Data());

        context.eval("js", "var x = d.name");

        System.out.println(
                context.getBindings("js").getMember("x").asString()
        );
    }
}

结果:

null

为什么?

据我了解,d 正确通过:

((Data) context.getBindings("js").getMember("d").as(Data.class)).name

returns "HelloWorld".

案例二:

context.eval("js", "d.getName()");

异常

Exception in thread "main" TypeError: invokeMember (getName) 
on JavaObject[task.Test$Data@35a3d49f (task.Test$Data)] failed due to: 
Unknown identifier: getName

但是 getName 是 public... 怎么了?

GraalVM JavaScript 默认执行严格的沙盒规则,其中之一是 JavaScript 代码无法访问主机 Java 对象,除非用户明确允许。允许您的代码访问 context.eval("js", "d.getName()") 的最简单方法是传递选项 polyglot.js.allowAllAccess=true 如下所述 link:

GraalVM JavaScript ScriptEngine implementation

查看示例:

import org.graalvm.polyglot.Context;

public class Test {

    static class Data {
        public String name = "HelloWorld";
        public String getName() {
            return this.name;
        }
    }

    public static void main(String[] args) {
        Context context = Context.newBuilder("js").allowHostAccess(true).build();
        context.getBindings("js").putMember("d", new Data());

        context.eval("js", "var x = d.getName()");

        System.out.println(
                context.getBindings("js").getMember("d").as(Data.class)).name
        );
    }
}

您需要用 @HostAccess.Export

注释 class 字段和方法

By default only public classes, methods, and fields that are annotated with @HostAccess.Export are accessible to the guest language. This policy can be customized using Context.Builder.allowHostAccess(HostAccess) when constructing the context.

Example using a Java object from JavaScript:

 public class JavaRecord {
     @HostAccess.Export public int x;    
     @HostAccess.Export
     public String name() {
         return "foo";
     }
 }

或者,您可以使用 GraalVM JSR-223 ScriptEngine

GraalVM JavaScript provides a JSR-223 compliant javax.script.ScriptEngine implementation. Note that this is feature is provided for legacy reasons in order to allow easier migration for implementations currently based on a ScriptEngine. We strongly encourage users to use the org.graalvm.polyglot.Context interface

To set an option via Bindings, use Bindings.put(, true) before the engine's script context is initialized. Note that even a call to Bindings#get(String) may lead to context initialization. The following code shows how to enable polyglot.js.allowHostAccess via Bindings:

ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("polyglot.js.allowHostAccess", true);
bindings.put("polyglot.js.allowHostClassLookup", (Predicate<String>) s -> true);
bindings.put("javaObj", new Object());
engine.eval("(javaObj instanceof Java.type('java.lang.Object'));"); // would not work 

without allowHostAccess and allowHostClassLookup This example would not work if the user would call e.g. engine.eval("var x = 1;") before calling bindings.put("polyglot.js.allowHostAccess", true);, since any call to eval forces context initialization.

当您使用上下文并向其添加 Java 对象时,在幕后,TruffleApi 中的 IntropLibrary 会创建一个 HostObject 并将其与该对象相关联。这意味着您不使用对象本身,而是使用包装器对象。

当您调用 getMember() 方法时,IntropLibrary 只能访问 public 仅可用的托管对象的字段和方法。由于您的内部 class 具有默认访问权限(无访问修饰符),因此 API 无法找到其成员,即使它们是 public。 (class 的成员不能拥有比其 class 本身更广泛的访问权限。

要解决这个问题,你要做的就是让你内心classpublic

import org.graalvm.polyglot.Context;

public class Test {

  public static class Data {
    public String name = "HelloWorld";
    public String getName() {
        return this.name;
    }
  }

  public static void main(String[] args) {
    Context context = Context.newBuilder("js").allowHostAccess(true).build();
    context.getBindings("js").putMember("d", new Data());

    context.eval("js", "var x = d.name;");

    System.out.println(
        context.getBindings("js").getMember("x").asString()
    );
  }
}

要以通常的 js 方式完全访问 java 对象,您可以使用 sj4js 库。

这个例子取自文档...

public class TestObject extends JsProxyObject {
    
    // the property of the object
    private String name = "";
    
    // the constructor with the property 
    public TestObject (String name) {
        super ();
        
        this.name = name;
        
        // we hvae to initialize the proxy object
        // with all properties of this object
        init(this);
    }

    // this is a mandatory override, 
    // the proxy object needs this method 
    // to generate new objects if necessary
    @Override
    protected Object newInstance() {
        return new TestClass(this.name);
    }
    
    // the setter for the property name
    public void setName (String s) {
        this.name = s;
    }
    
    // the getter for the property name
    public String getName () {
        return this.name;
    }
}

您可以像访问 java 对象一样访问此对象。

try (JScriptEngine engine = new JScriptEngine()) {
    engine.addObject("test", new TestClass("123"));
            
    engine.exec("test.name");
    // returns "123"

    engine.exec("test['name']")
    // returns "123"

    engine.exec("test.name = '456'")   
    engine.exec("test.name");
    // returns "456"
}