JSNI
(INFO: 对于新的实现,请使用面向未来的 JsInterop。JSNI 将在 GWT 3 中移除。)
通常,您需要将 GWT 与现有的手写 JavaScript 或第三方 JavaScript 库集成。有时,您可能需要访问 GWT 类 API 未公开的低级浏览器功能。GWT 的 JavaScript 本机接口 (JSNI) 功能可以解决这两个问题,它允许您将 JavaScript 直接集成到应用程序的 Java 源代码中。
GWT 编译器 将 Java 源代码转换为 JavaScript。有时,将手写 JavaScript 与 Java 源代码混合在一起非常有用。例如,某些核心 GWT 类的最低级功能是用 JavaScript 手写的。GWT 借鉴了 Java 本机接口 (JNI) 的概念来实现 JavaScript 本机接口 (JSNI)。编写 JSNI 方法是一种强大的技术,但应谨慎使用,因为编写防弹的 JavaScript 代码非常困难。JSNI 代码在不同浏览器之间可能不太可移植,更容易发生内存泄漏,不太适合 Java 工具,并且更难让编译器优化。
我们将 JSNI 视为 Web 等效于内联汇编代码。您可以通过多种方式使用它
- 在 JavaScript 中直接实现 Java 方法
- 在现有的 JavaScript 周围包装类型安全的 Java 方法签名
- 从 JavaScript 代码调用 Java 代码,反之亦然
- 跨 Java/JavaScript 边界抛出异常
- 从 JavaScript 读取和写入 Java 字段
- 使用开发模式调试 Java 源代码(使用 Java 调试器)和 JavaScript(使用脚本调试器)
- 编写本机 JavaScript 方法
- 从 JavaScript 访问 Java 方法和字段
- 从手写 JavaScript 调用 Java 方法
- 在 Java 源代码和 JavaScript 之间共享对象
- 将 Java 值传递到 JavaScript
- 将 JavaScript 值传递到 Java 代码
- 重要说明
- 异常和 JSNI
编写本机 JavaScript 方法
JSNI 方法声明为 native
,并在参数列表的末尾和尾随分号之间包含格式特殊的注释块中的 JavaScript 代码。JSNI 注释块以确切的标记 /*-{
开头,以确切的标记 }-*/
结束。JSNI 方法的调用方式与任何普通 Java 方法相同。它们可以是静态方法或实例方法。
JSNI 语法是对 Java 到 JavaScript 编译器的指令,以接受注释语句之间的任何文本作为有效的 JS 代码,并将其内联注入生成的 GWT 文件中。在编译时,GWT 编译器会对方法内部的 JavaScript 进行一些语法检查,然后生成接口代码以正确转换方法参数和返回值。
从 GWT 1.5 版本开始,支持 Java 可变参数结构。GWT 编译器会将两个 Java 代码之间的可变参数调用转换。但是,从 Java 调用可变参数 JavaScript 方法会导致被调用者在数组中接收参数。
示例
以下是如何编码 JSNI 方法的简单示例,该方法会弹出一个 JavaScript 警报对话框
public static native void alert(String msg) /*-{
$wnd.alert(msg);
}-*/;
请注意,代码没有直接在方法内部引用 JavaScript window
对象。从 JSNI 访问浏览器的窗口和文档对象时,必须分别将它们引用为 $wnd
和 $doc
。已编译的脚本在嵌套框架中运行,$wnd
和 $doc
会自动初始化以正确引用主机页面的窗口和文档。
以下是有问题的另一个示例
public static native int badExample() /*-{
return "Not A Number";
}-*/;
public void onClick () {
try {
int myValue = badExample();
GWT.log("Got value " + myValue, null);
} catch (Exception e) {
GWT.log("JSNI method badExample() threw an exception:", e);
}
}
此示例作为 Java 编译,其语法由 GWT 编译器验证为有效的 JavaScript。但是,当您在 开发模式 下运行示例代码时,它会返回异常。单击日志窗口中的行以在下面的消息区域中显示异常
com.google.gwt.dev.shell.HostedModeException: invokeNativeInteger(@com.example.client.GWTObjectNotifyTest::badExample()): JS value of type string, expected int
at com.google.gwt.dev.shell.JsValueGlue.getIntRange(JsValueGlue.java:343)
at com.google.gwt.dev.shell.JsValueGlue.get(JsValueGlue.java:179)
at com.google.gwt.dev.shell.ModuleSpace.invokeNativeInt(ModuleSpace.java:233)
at com.google.gwt.dev.shell.JavaScriptHost.invokeNativeInt(JavaScriptHost.java:97)
at com.example.client.GWTObjectNotifyTest.badExample(GWTObjectNotifyTest.java:29)
at com.example.client.GWTObjectNotifyTest$1.onClick(GWTObjectNotifyTest.java:52)
...
在本例中,Java IDE 和 GWT 编译器都无法判断 JSNI 方法内部的代码与 Java 声明之间存在类型不匹配。GWT 生成的接口代码在开发模式下的运行时捕获了该问题。在 生产模式 下运行时,您将看不到异常。JavaScript 的动态类型会掩盖这种问题。
提示:由于 JSNI 代码只是普通的 JavaScript,因此在开发模式下运行时,您将无法在 JSNI 方法内部使用 Java 调试工具。但是,您可以在包含 JSNI 方法的左大括号的源代码行上设置断点,从而允许您查看调用参数。此外,Java 编译器和 GWT 编译器不会对 JSNI 代码执行任何语法或语义检查,因此方法的 JavaScript 主体中的任何错误都将在运行时才能看到。
从 JavaScript 访问 Java 方法和字段
从 JSNI 方法的 JavaScript 实现内部操作 Java 对象非常有用。但是,由于 JavaScript 使用动态类型,而 Java 使用静态类型,因此您必须使用特殊语法。
编写 JSNI 代码时,偶尔在 生产模式 下运行会很有帮助。JavaScript 编译器 会检查您的 JSNI 代码,并在编译时标记您在 开发模式 下运行时无法捕获的错误。
从 JavaScript 调用 Java 方法
从 JavaScript 调用 Java 方法有点类似于从 C 代码调用 Java 方法,就像在 JNI 中一样。特别是,JSNI 借鉴了 JNI 损坏的方法签名方法来区分重载方法。从 JavaScript 调用 Java 方法的形式如下
[instance-expr.]@class-name::method-name(param-signature)(arguments)
- instance-expr. : 在调用实例方法时必须存在,在调用静态方法时必须不存在
- class-name : 是声明方法所在的类的完全限定名(或其子类)
- param-signature : 是内部 Java 方法签名,如 JNI 类型签名 中所指定,但没有方法返回值的尾随签名,因为它不需要选择重载
- arguments : 是要传递给被调用方法的实际参数列表
从 JavaScript 调用 Java 构造函数
从 JavaScript 调用 Java 构造函数与上述用例相同,只是方法名称始终为 new。
给出以下 Java 类
package pkg;
class TopLevel {
public TopLevel() { ... }
public TopLevel(int i) { ... }
static class StaticInner {
public StaticInner() { ... }
}
class InstanceInner {
public InstanceInner(int i) { ... }
}
}
我们将 Java 表达式与 JSNI 表达式进行比较
new TopLevel()
变为@pkg.TopLevel::new()()
new StaticInner()
变为@pkg.TopLevel.StaticInner::new()()
someTopLevelInstance.new InstanceInner(123)
变为@pkg.TopLevel.InstanceInner::new(Lpkg/TopLevel;I)(someTopLevelInstance, 123)
- 非静态类的封闭实例隐式定义为非静态类构造函数的第一个参数。无论非静态类嵌套多深,它只需要引用其直接封闭类型的实例。
从 JavaScript 访问 Java 字段
可以从手写 JavaScript 访问静态字段和实例字段。字段引用的形式为
[instance-expr.]@class-name::field-name
示例
以下是从 JSNI 访问静态字段和实例字段的示例。
public class JSNIExample {
String myInstanceField;
static int myStaticField;
void instanceFoo(String s) {
// use s
}
static void staticFoo(String s) {
// use s
}
public native void bar(JSNIExample x, String s) /*-{
// Call instance method instanceFoo() on this
[email protected]::instanceFoo(Ljava/lang/String;)(s);
// Call instance method instanceFoo() on x
[email protected]::instanceFoo(Ljava/lang/String;)(s);
// Call static method staticFoo()
@com.google.gwt.examples.JSNIExample::staticFoo(Ljava/lang/String;)(s);
// Read instance field on this
var val = [email protected]::myInstanceField;
// Write instance field on x
[email protected]::myInstanceField = val + " and stuff";
// Read static field (no qualifier)
@com.google.gwt.examples.JSNIExample::myStaticField = val + " and stuff";
}-*/;
}
提示:从 GWT 1.5 版本开始,支持 Java 可变参数结构。GWT 编译器会将两个 Java 代码之间的可变参数调用转换,但是,从 JSNI 调用可变参数 Java 方法需要 JavaScript 调用者传递适当类型的数组。
从手写 JavaScript 调用 Java 方法
有时,您需要从外部 JavaScript 代码访问在 GWT 中定义的方法或构造函数。此代码可能是手写的,包含在另一个 java 脚本文件中,或者可能是第三方库的一部分。在这种情况下,GWT 编译器将没有机会直接在您的 JavaScript 代码和 GWT 生成的 JavaScript 之间构建接口。
实现这种关系的一种方法是通过 JSNI 将方法分配给外部、全局可见的 JavaScript 名称,该名称可以被您的手工制作的 JavaScript 代码引用。
package mypackage;
public MyUtilityClass
{
public static int computeLoanInterest(int amt, float interestRate,
int term) { ... }
public static native void exportStaticMethod() /*-{
$wnd.computeLoanInterest =
$entry(@mypackage.MyUtilityClass::computeLoanInterest(IFI));
}-*/;
}
请注意,对导出方法的引用已包装在对$entry
函数的调用中。此隐式定义的函数确保使用未捕获异常处理程序执行 Java 派生方法,并提供许多其他实用程序服务。$entry
函数是可重入安全的,应该在任何从非 GWT 上下文调用 GWT 派生 JavaScript 的地方使用。
在应用程序初始化时,调用MyUtilityClass.exportStaticMethod()
(例如,从您的 GWT 入口点)。这会将函数分配给窗口对象中名为computeLoanInterest
的变量。
如果您要导出实例方法,并从 JS 正确使用它,则需要执行以下操作
在 Java 源代码和 JavaScript 之间共享对象
package mypackage;
public class Account {
private int balance = 0;
public int add(int amt) {
balance += amt;
}
public native void exportAdd() /*-{
var that = this;
$wnd.add = $entry(function(amt) {
[email protected]::add(I)(amt);
});
}-*/;
}
然后,您可以使用 JS 调用它
$wnd.add(5);
上面的代码对于 Java 开发人员来说可能看起来很奇怪,但这是必需的,因为 JavaScript 中的this
可能与 Java 中的this
作用不同。如果您有兴趣,可以在这里或这里找到关于 JavaScript 中this
的良好介绍。
在 Java 源代码和 JavaScript 之间共享对象
JSNI 方法中的参数和返回值类型声明为 Java 类型。对于传入和传出 JavaScript 代码的值如何处理,存在非常具体的规则。无论值通过正常的 Java 方法调用语义进入和离开,还是通过 Java 方法从 JSNI 代码调用的特殊语法进入和离开,都必须遵循这些规则。
将 Java 值传递到 JavaScript
传入 Java 类型 | 它在 JavaScript 代码中如何显示 |
---|---|
String | JavaScript 字符串,如var b = "foo"; 。 |
boolean | JavaScript 布尔值,如var b = true; 。 |
long | 不允许(参见注释) |
其他数字基元 | JavaScript 数值,如var x = 42; 。 |
JavaScriptObject | 必须来自 JavaScript 代码的JavaScriptObject ,通常是作为其他 JSNI 方法的返回值(参见注释)。 |
Java 数组 | 只能传递回 Java 代码的不透明值。 |
任何其他 Java Object |
可以通过特殊语法访问的不透明值。 |
将 JavaScript 值传递到 Java 代码
传出 Java 类型 | 必须传递什么 |
---|---|
String | JavaScript 字符串,如return "boo"; 。 |
boolean | JavaScript 布尔值,如return false; 。 |
long | 不允许(参见注释) |
Java 数字基元 | JavaScript 数值,如return 19; 。 |
JavaScriptObject | 原生 JavaScript 对象,如return document.createElement("div") (参见注释)。 |
任何其他 Java Object (包括数组)。 |
必须来自 Java 代码的正确类型 Java Object ;不能在 JavaScript 中从“无中生有”构造 Java 对象。 |
重要说明
Java
long
类型无法在 JavaScript 中表示为数字类型,因此 GWT 使用不透明数据结构来模拟它。这意味着 JSNI 方法不能将long
处理为数字类型。因此,编译器默认情况下不允许直接从 JSNI 访问long
:JSNI 方法不能以long
作为参数类型或返回值类型,也不能使用JSNI 引用访问long
。如果您发现自己想要将long
传入或传出 JSNI 方法,以下是一些选项- 对于适合
double
类型的数字,请使用double
类型而不是long
类型。 - 对于需要完整
long
语义的计算,请重新安排代码,以便计算在 Java 而不是 JavaScript 中执行。这样,它们将使用long
模拟。 - 对于旨在通过不变的值传递到 Java 代码的值,请将该值包装在
Long
中。JSNI 方法对Long
类型没有限制。 - 如果您确信知道自己在做什么,可以在方法中添加注释
com.google.gwt.core.client.UnsafeNativeLong
。然后,编译器将允许您将long
传入和传出 JavaScript。但是,它仍然是不透明数据类型,因此您唯一可以对它执行的操作是将其传递回 Java。
- 对于适合
在开发模式下违反任何这些编组规则将生成一个
com.google.gwt.dev.shell.HostedModeException
,详细说明问题。此异常是不可翻译的,并且永远不会在生产模式下抛出。JavaScriptObject从 GWT 编译器和开发模式获得了特殊处理。它的目的是为 Java 代码提供对原生 JavaScript 对象的不透明表示。
尽管 Java 数组不能直接在 JavaScript 中使用,但有一些辅助类可以有效地实现类似的效果:JsArray、JsArrayBoolean、JsArrayInteger、JsArrayNumber和JsArrayString。这些类是围绕原生 JavaScript 数组的包装器。
Java
null
和 JavaScriptnull
是相同的,并且始终是任何非基元 Java 类型的合法值。JavaScriptundefined
在传递到 Java 代码时也被认为等于null
(JavaScript 的规则规定,在 JavaScript 代码中,null == undefined
是true
,但null === undefined
是false
)。在 GWT 的先前版本中,undefined
不是可以传递到 Java 的合法值。
异常和 JSNI
在执行正常的 Java 代码或 JSNI 方法中的 JavaScript 代码期间,可能会抛出异常。当在 JSNI 方法中生成的异常向上传播到调用堆栈,并被 Java catch 块捕获时,抛出的 JavaScript 异常将在捕获时包装为JavaScriptException对象。此包装器对象仅包含发生的 JavaScript 异常的类名和描述。建议的做法是在 JavaScript 代码中处理 JavaScript 异常,在 Java 代码中处理 Java 异常。
Java 异常在通过 JSNI 方法传播时可以安全地保留身份。
例如,
- Java 方法
doFoo()
调用 JSNI 方法nativeFoo()
nativeFoo()
内部调用 Java 方法fooImpl()
fooImpl()
抛出异常
从fooImpl()
抛出的异常将通过nativeFoo()
传播,并且可以在doFoo()
中捕获。该异常将保留其类型和身份。