从 Nashorn 迁移到 GraalJS 的指南

本指南作为先前针对 Nashorn 引擎的代码的迁移指南。有关支持的 Java 互操作性功能概述,请参见 Java 互操作性指南

Nashorn 引擎已在 JDK 11 中作为 JEP 335 的一部分被弃用,并在 JDK15 中作为 JEP 372 的一部分被移除。

GraalJS 可以作为先前在 Nashorn 引擎上执行的 JavaScript 代码的替代品。GraalJS 提供了 Nashorn 先前提供的 JavaScript 的所有功能。许多功能默认可用,一些功能需要启用选项,另一些功能需要对源代码进行少量修改。

Nashorn 和 GraalJS 都支持 Java 互操作性类似的语法和语义集。一个显著的差异是 GraalJS 采用“默认安全”方法,这意味着一些功能需要显式启用,而这些功能在 Nashorn 上默认可用。与迁移相关的最重要的差异列于此处。

默认情况下可用的 Nashorn 功能(取决于 安全设置

  • Java.type, Java.typeName
  • Java.from, Java.to
  • Java.extend, Java.super
  • Java 包全局变量:Packages, java, javafx, javax, com, org, edu

Nashorn 兼容模式 #

GraalJS 提供 Nashorn 兼容模式。部分实现 Nashorn 兼容性所必需的功能仅在启用 js.nashorn-compat 选项时才可用。对于 GraalJS 不想默认公开的 Nashorn 特定扩展,情况就是如此。

请注意,您必须解锁 实验性功能 才能使用此选项。此外,请注意,在某些情况下,设置此选项会破坏 GraalJS 的“默认安全”方法,例如,在旧版 ScriptEngine 上操作时。

当您使用 Nashorn 兼容模式时,默认情况下,ECMAScript 5 被设置为兼容性级别。您可以使用 js.ecmascript-version 选项指定不同的 ECMAScript 版本。请注意,这可能与完全的 Nashorn 兼容性冲突。本节末尾提供了如何设置选项的代码示例。

js.nashorn-compat 选项可以设置

  • 通过使用命令行选项
      js --experimental-options --js.nashorn-compat=true
    
  • 通过使用 Polyglot API
      import org.graalvm.polyglot.Context;
    
      try (Context context = Context.newBuilder().allowExperimentalOptions(true).option("js.nashorn-compat", "true").build()) {
          context.eval("js", "print(__LINE__)");
      }
    
  • 通过在启动 Java 应用程序时使用系统属性(请记住,还要在应用程序中的 Context.Builder 上启用 allowExperimentalOptions
      java -Dpolyglot.js.nashorn-compat=true MyApplication
    

仅在 nashorn-compat 选项下可用的功能包括

  • Java.isJavaFunction, Java.isJavaMethod, Java.isScriptObject, Java.isScriptFunction
  • new Interface|AbstractClass(fn|obj)
  • JavaImporter
  • JSAdapter
  • 字符串值上的 java.lang.String 方法
  • load("nashorn:parser.js"), load("nashorn:mozilla_compat.js")
  • exit, quit

js.ecmascript-version 选项可以用类似的方式设置。由于这是一个支持的选项,因此不需要只为了设置 ecmascript-version 就提供 experimental-options 选项。

js --js.ecmascript-version=2020

Nashorn 语法扩展 #

Nashorn 语法扩展 可以使用 js.syntax-extensions 实验性选项启用。它们也在 Nashorn 兼容模式(js.nashorn-compat)中默认启用。

GraalJS 与 Nashorn 的区别 #

GraalJS 在某些方面与 Nashorn 不同,这些方面是刻意设计的。

默认安全 #

GraalJS 采用“默认安全”方法。除非嵌入器明确允许,否则 JavaScript 代码无法访问 Java 类或访问文件系统,以及其他限制。GraalJS 的几个功能,包括 Nashorn 兼容性功能,仅在相关安全设置足够宽松时才可用。

请确保您了解对应用程序和主机系统解除默认安全限制的任何更改的安全隐患。

有关可用设置的完整列表,请参见 Context.Builder。这些选项可以在使用 Polyglot API 构建上下文时定义。

启用 GraalJS 功能时经常需要的选项是

  • allowHostAccess():配置访客应用程序可以访问的公共构造函数、方法或公共类的字段。使用 HostAccess.EXPLICIT 或自定义 HostAccess 策略来选择性地启用访问。设置为 HostAccess.ALL 以允许无限制访问。
  • allowHostClassLookup():设置一个过滤器,指定访客应用程序可以查找的 Java 主机类。设置为谓词 className -> true 以允许查找所有类。
  • allowIO():允许访客语言在主机系统上执行无限制的 IO 操作,例如,需要从文件系统 load()。设置为 true 以启用 IO。

如果您在旧版 ScriptEngine 上运行代码,请参见 通过 Bindings 设置选项,了解如何在旧版 ScriptEngine 上设置选项。

最后,请注意,nashorn-compat 模式在 ScriptEngine 上执行代码时(但在 Context 上不执行)会启用相关选项,以便在该设置中提供与 Nashorn 更好的兼容性。

启动器名称 js #

GraalJS 带有一个名为 js 的二进制启动器。请注意,根据构建环境,GraalJS 可能仍然会附带 Nashorn 及其 jjs 启动器。

ScriptEngine 名称 graal.js #

GraalJS 附带对 ScriptEngine 的支持。它在几个名称下注册,包括“graal.js”、“JavaScript”和“js”。如果您需要完全的 Nashorn 兼容性,请务必按照上述说明激活 Nashorn 兼容模式。根据构建设置,GraalJS 可能仍然会附带 Nashorn 并通过 ScriptEngine 提供它。有关更多详细信息,请参见 ScriptEngine 实现

ClassFilter #

GraalJS 在使用 polyglot Context 启动时提供一个类过滤器。请参见 Context.Builder.hostClassFilter

完全限定名称 #

GraalJS 要求使用 Java.type(typename)。它默认不支持仅通过完全限定类名访问类。Java.type 带来更多清晰度,并避免在 JavaScript 代码中意外使用 Java 类。例如,请查看此模式

var bd = new java.math.BigDecimal('10');

它应该表示为

var BigDecimal = Java.type('java.math.BigDecimal');
var bd = new BigDecimal('10');

有损转换 #

GraalJS 在调用 Java 方法时不允许对参数进行有损转换。这会导致数值上的错误,难以检测。

GraalJS 始终选择具有最窄可能的参数类型(可以在不丢失的情况下转换为这些参数类型)的重载方法。如果没有这样的重载方法,GraalJS 会抛出 TypeError,而不是进行有损转换。通常,这会影响执行的重载方法。

可以使用自定义 targetTypeMapping 来定制行为。请参见 HostAccess.Builder#targetTypeMapping

ScriptObjectMirror 对象 #

GraalJS 不提供 ScriptObjectMirror 类的对象。相反,JavaScript 对象对 Java 代码公开为实现 Java 的 Map 接口的对象。

引用 ScriptObjectMirror 实例的代码可以通过将类型更改为接口(MapList)或 polyglot Value 类(提供类似的功能)来重写。

多线程 #

在 GraalVM 上运行 JavaScript 通过从 Java 代码创建多个 Context 对象来支持多线程。上下文可以在线程之间共享,但每个上下文一次只能被一个线程访问。可以从一个 Java 应用程序创建多个 JavaScript 引擎,并且可以安全地在多个线程上并行执行。

Context polyglot = Context.create();
Value array = polyglot.eval("js", "[1,2,42,4]");

GraalJS 不允许从 JavaScript 中创建访问当前 Context 的线程。此外,GraalJS 不允许并发线程同时访问同一个 Context。这会导致难以管理的同步问题,例如在未准备进行多线程的语言中出现数据竞争。例如

new Thread(function() {
    print('printed from another thread'); // throws Exception due to potential synchronization problems
}).start();

JavaScript 代码可以使用在 Java 中实现的 Runnable 来创建和启动线程。子线程可能无法访问父线程或任何其他 polyglot 线程的 Context。如果违反,将抛出 IllegalStateException。子线程可以创建一个新的 Context 实例。

new Thread(aJavaRunnable).start(); // allowed on GraalJS

通过适当的同步,多个上下文可以在不同的线程之间共享。可以在 此处 找到使用多个线程中的 JavaScript Context 的示例 Java 应用程序。

仅在 Nashorn 兼容模式下可用的扩展 #

以下在 Nashorn 中可用的 JavaScript 扩展在 GraalJS 中默认情况下被停用。它们在 Nashorn 兼容模式下提供。强烈建议不要基于这些功能实现新的应用程序,而应将其作为将现有应用程序迁移到 GraalVM 的一种手段。

字符串 length 属性 #

GraalJS 不对字符串的 length 属性进行特殊处理。访问字符串长度的规范方法是读取 length 属性

myJavaString.length;

Nashorn 允许用户以属性和函数两种方式访问 length。现有的函数调用 length() 应该表示为属性访问。Nashorn 兼容模式下会模拟 Nashorn 的行为。

JavaScript 全局对象中的 Java 包 #

GraalJS 要求使用 Java.type 而不是完全限定名称。在 Nashorn 兼容模式下,以下 Java 包将添加到 JavaScript 全局对象:java, javafx, javax, com, orgedu

JavaImporter #

JavaImporter 功能仅在 Nashorn 兼容模式下可用。

JSAdapter #

不建议使用非标准的 JSAdapter 功能,应将其替换为等效的标准 Proxy 功能。为了兼容性,JSAdapter 在 Nashorn 兼容模式下仍然可用。

Java.* 方法 #

Nashorn 在 Java 全局对象上提供的一些方法仅在 Nashorn 兼容模式下可用,或者目前 GraalJS 不支持。在 Nashorn 兼容模式下可用的是:Java.isJavaFunctionJava.isJavaMethodJava.isScriptObjectJava.isScriptFunction。目前不支持 Java.asJSONCompatible

访问器 #

在 Nashorn 兼容模式下,GraalJS 允许用户通过使用名称作为属性来访问 getter 和 setter,同时省略 getsetis

var Date = Java.type('java.util.Date');
var date = new Date();

var myYear = date.year; // calls date.getYear()
date.year = myYear + 1; // calls date.setYear(myYear + 1);

GraalJS 模仿了 Nashorn 在访问顺序方面的行为。

  • 在读取操作的情况下,GraalJS 将首先尝试调用一个名为 get 的 getter,并在其中使用驼峰式大小写指定属性名称。如果该方法不可用,则会调用一个名为 is 的 getter,并在其中使用驼峰式大小写指定属性名称。在第二种情况下,与 Nashorn 不同的是,即使返回值不是布尔类型,也会返回该值。只有当两种方法都不可用时,才会读取属性本身。
  • 在写入操作的情况下,GraalJS 将尝试调用一个名为 set 的 setter,并在其中使用驼峰式大小写指定属性名称,并将值作为参数提供给该函数。如果 setter 不可用,则会写入属性本身。

请注意,Nashorn(以及 GraalJS)在属性读写和函数调用之间有明确的区分。当 Java 类同时具有公开可用的相同名称的字段和方法时,obj.property 将始终读取字段(或如上所述的 getter),而 obj.property() 将始终调用相应的方法。

其他需要考虑的方面 #

GraalJS 的功能 #

GraalJS 支持最新 ECMAScript 规范的功能以及一些扩展。请参见 JavaScript 兼容性。请注意,此示例将对象添加到全局范围,这些对象可能会干扰现有代码,而这些代码不知道这些扩展。

控制台输出 #

GraalJS 提供了一个与 Nashorn 兼容的内置函数 print

请注意,GraalJS 还提供了一个 console.log 函数。在纯 JavaScript 模式下,这是一个 print 的别名,但在 Node 模式下运行时,它使用 Node.js 提供的实现。在 Node 模式下,console.log 周围的 Java 对象的行为有所不同,因为 Node.js 没有实现对这些对象的特殊处理。

联系我们