Java 与 JavaScript 通信






4.86/5 (11投票s)
2000 年 5 月 26 日

423146

3163
赋予 Java Applet第二次机会(作为 COM 式二进制组件)。
致谢
-
本文附带的演示聊天应用程序的创意均来自我本人。我所见过的所有聊天应用程序要么完全基于 Java,要么完全基于 HTML。我的方法在这两者之间取得了很好的平衡。
- Java 和 JavaScript 之间通信的技术信息是从阅读 Netscape Developer 网站上的大量文章中收集的。
- 我在阅读 Danny Goodman(Netscape 网站上的 JavaScript 大师)的文章时,首次遇到了“无界面小程序”这个术语。因此,我将这个非常恰当的术语归功于 Danny。
免责声明
-
本文讨论的技术在 Windows 2000 Professional 机器上使用 Internet Explorer 5.0 和 Netscape Navigator 4.7 进行过测试。据我所知,这些技术应该可以在任一浏览器的 4.0+ 版本中使用,但由于没有时间用所有这些版本进行测试,因此无法保证。
- 由于 JavaScript 是唯一能被两大浏览器原生支持的语言,因此所有脚本代码片段都使用此语言。我之前曾使用过 Ncompasslabs.com 的一款商业插件,该插件可在 Netscape Navigator 中启用 VBScript 支持,但当我访问其网站验证信息时,该产品已不再列出。
引言
在过去两年多里,我一直在我公司电子商务产品工具包团队的技术领导岗位上,深入研究现代 COM。我理解了精心设计的接口以及在 Visual C++、Visual Basic 以及脚本等各种环境中运行良好的实现的重要性。面向接口编程的力量永远地根植于我的脑海中,并尝试将其应用于我作为一名软件工程师所参与的每一件事。
在过去的一年里,经验告诉我们,所有的业务逻辑都应该封装在 COM 对象中,而 ASP + 脚本应该仅作为这些对象的胶水。设计和开发基础设施和业务对象通常需要比实际使用它们更高的技能水平。在我们组织中,开发这些对象的首选环境是 Visual C++ / ATL / STL。微软也提倡使用 Visual Basic 作为这些对象的替代开发环境。
这些对象通常被称为无界面对象,因为它们实现了大量逻辑但没有用户界面。表示层要么是富客户端,要么是瘦客户端,它们具有向最终用户展示信息和从最终用户收集信息的逻辑。这样的客户端然后使用无界面对象来有意义地处理信息。整个想法是,表示层通常需要大量定制,而业务对象则不经常更改。表示层所需的更改也可以由经验较少的程序员来实现。
将相同的原则应用于浏览器环境,将复杂的客户端逻辑封装在无界面的二进制模块中,同时使用脚本语言处理表示方面似乎是合乎逻辑的。在 Windows 平台上,这些模块的选项是 Java 小程序和 ActiveX 控件/服务器。本文的重点是使用 Java 小程序来实现此目标,因为小程序在很大程度上是与浏览器、平台和处理器无关的。
小程序简史
SUN Microsystems 于 1995 年隆重推出了 Java 小程序。小程序立即席卷了 Web 世界,因为它们在本质上是静态 HTML 的世界中,增加了在浏览器中显示动态 Web 内容的能力。
在最初的日子里,使用 Java 小程序似乎是向网页添加动态内容的最佳方法。微软最初试图用其 ActiveX 控件技术来对抗 SUN 的产品,但使用控件在网页中存在两个主要问题:
-
这些是二进制模块,是特定于处理器的,因此不适合作为网页的一部分运行。万维网之所以如此成功,主要原因之一是使用 W3C 标准 HTML 编写的网页(在很大程度上)是独立于浏览器和处理器的。ActiveX 控件完全不符合这一范例。
- 安全性是一个大问题,因为控件编写者可以完全访问客户端机器上的资源。签名控件允许任何查看网页的人就某个特定控件是否应下载到其机器上做出明智的决定,但只需意外(或愚昧地)单击一个按钮,客户端机器就会容易受到某些恶意控件编写者的意图的攻击。
随着动态 HTML 的最终成型,情况发生了巨大变化。文档对象模型(DOM)将网页中的元素暴露为可编程组件,这些组件具有自己的属性和方法。尽管 Internet Explorer 和 Netscape Navigator 浏览器中动态 HTML 的实现大相径庭,但通过页面内脚本代码以编程方式更改内容的根本主题是引起了巨大轰动。小程序突然显得过时且原始。W3C 对动态 HTML 的认可最终为新一代的复杂、动态网页定下了基调。
在浏览器中使用 Java 小程序有几个优点,如下所列:
-
小程序(在很大程度上)可以在多个浏览器、平台和处理器上运行。
- Java 语言是一种类型化语言,具有强大的构造。
- JDK 附带了许多在高级类库中通常才有的有用类。
- 安全性是设计到技术中的,小程序默认在沙箱中运行。小程序必须签名才能打破沙箱的限制。
- 小程序可以回传到 Web 服务器以发送自定义消息、上传/下载文件等。签名的小程序可以与任何服务器通信,而不仅仅是托管它们的那一个。
- 小程序代码无法通过浏览器的“查看源代码”选项查看,从而保护知识产权。
- Java .class 文件体积非常小,下载速度更快。
使用小程序的一个缺点是:
-
在浏览器会话中,小程序要么在第一次显示使用它们的页面时下载,要么在页面稍后刷新时下载。小程序代码在浏览器会话之间不会驻留在客户端机器上。在大多数情况下,这实际上可以被认为是一个优点。
- 小程序初始化耗时较长。
- 由于 Java .class 文件是字节码,由 Java 虚拟机 (JVM) 解释,因此小程序运行速度比本机代码慢。
- 小程序只是网页上的一块区域,并没有无缝集成到页面内容中。层叠样式表不直接影响小程序所占用的矩形区域。
- Netscape Navigator 4.x 对活动小程序数量的限制是十 (10) 个。我不知道 Internet Explorer 4.0+ 是否有类似的限制。
快速了解 Java 小程序的使用
Java 小程序使用 applet 标签包含在 HTML 页面中。W3C 网站上的 HTML 4.01 规范的第 13.4 节详细介绍了此标签。它还提到该标签已弃用,推荐使用 <object> 标签。
下面是一个包含小程序的示例文档页面:
<html>
<head>
<title>Calculator</title>
</head>
<body>
<applet id="Calculator" width="300" height="500" code ="Calculator.class" codebase=".">
<param name="InitialMode" value="Normal">
</applet>
</body>
</html>
上面示例中使用的属性解释如下:
ID |
小程序实例的标识符。客户端脚本代码可以使用此 id 来引用小程序。 |
宽度 |
此属性指定小程序显示区域的初始宽度(不包括小程序创建的任何窗口或对话框)。尽管我在免责声明部分提到的浏览器中成功使用了零 (0) 宽度,但建议使用 1 作为可能的最小宽度。 |
高度 |
此属性指定小程序显示区域的初始高度(不包括小程序创建的任何窗口或对话框)。与 *width* 属性一样,建议使用 1 作为可能的最小高度。 |
代码 |
此属性指定包含小程序已编译的小程序子类的类文件名,或者获取类文件的路径,包括类文件本身。它相对于小程序的代码库进行解释。 |
代码库 |
此属性指定小程序的基 URI。如果未指定此属性,则默认为与当前文档相同的基 URI。 |
*code*、*width* 和 *height* 属性是必需的。
«param» 标签包含名称-值对,允许在小程序首次启动时进行配置。
下面是一个调用上面小程序中某个方法的示 JavaScript 函数:
<script language=Javascript>
function SetCalculatorMode(Mode)
{
document.Calculator.SetCalculatorMode (Mode);
// Alternative way to reference the applet.
// document.applets[0].SetCalculatorMode(Mode);
}
</script>
分工
在本篇文章的引言中,我提出了一个方法,即将复杂的浏览器端处理封装在无界面的 Java 小程序中,而表示层则由 JavaScript 代码管理。此方法需要 Java 和 JavaScript 之间的双向通信。接下来的几段将探讨可用的选项。
从 JavaScript 代码访问 Java 小程序公开的公共成员和函数非常简单,正如上一节的 `SetCalculatorMode()` 函数所示。文档中的小程序可以通过其 *Id* / *Name* 或通过 `applets` 集合中的索引来引用。
例如,
document.Calculator.SetCalculatorMode(Mode);
// or
document.applets[0].SetCalculatorMode(Mode);
反向通信(Java 到 JavaScript)通过 `netscape.javascript.JSObject` 和 `netscape.javascript.JSException` 类实现。为了找出这些 .class 文件的位置,我在硬盘上搜索了所有包含“JSObject”文本的文件。令我惊讶的是,这些文件在许多不同的应用程序中得到了广泛使用,包括任何 Visual Interdev 项目的脚本库。
如果您的计算机上安装了 Netscape Navigator 4.0+,这些 .class 文件可在 Java40.jar 文件中找到,该文件本身位于 «Navigator 安装目录»\communicator\program\java\classes 目录下。
我还发现这些 .class 文件位于 «Windows 安装目录»\Java\Packages 目录下的四个不同的 .zip 文件中。这些 .zip 文件显然是由微软产品安装的,因为它们包含许多 com.ms 包。关键是这两个类在任一浏览器中都可用,您可以将 CLASSPATH 环境变量设置为上述任何路径。另一种方法是使用 WinZip 等实用程序将这些文件从 .jar 或 .zip 文件提取到您小程序所在的目录中。
JSObject 类
为了更好地理解此类实用性,此处提供了 JSObject 类的成员函数的简要描述。
public static JSObject getWindow (Applet applet )
此静态方法返回给定小程序所在窗口的 JSObject。例如:
JSObject MainWindow = JSObject.getWindow ( this );
public Object call ( String methodName, Object args[ ] )
此方法从 Java 小程序中调用 JavaScript 方法。例如:
JSObject MainWindow = JSObject.getWindow ( this );
String Arguments[ ] = {"90", "2"}; // {"Percent complete", "Time remaining"}
MainWindow.call ( "UpdateProgressIndicator", Arguments );
public Object eval ( String s )
此方法评估 JavaScript 表达式。该表达式是 JavaScript 源代码字符串,将在 `this` 给定的上下文中进行评估。例如:
JSObject MainWindow = JSObject.getWindow ( this );
JSObject UserName = MainWin.eval ( "document.UserInfoForm.UserName" );
public Object getMember ( String name )
此方法检索 JavaScript 对象的索引成员。等同于 JavaScript 中的 `this.name`。例如:
JSObject MainWindow = JSObject.getWindow ( this );
JSObject DocumentPage = (JSObject)MainWindow.getMember ( "document" );
JSObject UserInfoForm = (JSObject) DocumentPage.getMember ( "UserInfoForm" );
JSObject UserName = (JSObject) UserInfoForm.getMember ( "UserName" );
public Object getSlot ( int index)
此方法检索 JavaScript 对象的索引成员。等同于 JavaScript 中的 `this [index]`。例如:
JSObject MainWindow = JSObject.getWindow ( this );
JSObject DocumentPage = (JSObject)MainWindow.getMember ( "document" );
JSObject Applets = (JSObject) DocumentPage.getMember ( "applets" );
Object theApplet = Applets.getSlot ( index );
public void removeMember ( String name )
此方法删除 JavaScript 对象的命名成员。
public void setMember ( String name, Object value )
此方法设置 JavaScript 对象的命名成员。它等同于 JavaScript 中的 `this.name = value`。例如:
JSObject MainWin = JSObject.getWindow ( this );
JSObject DocumentPage = (JSObject) MainWin.getMember ( "document" );
JSObject UserInfoForm = (JSObject) DocumentPage.getMember ( "UserInfoForm" );
JSObject UserName = (JSObject) UserInfoForm.getMember ( "UserName" );
UserName.setMember ( "value", "Jeremiah S. Talkar" );
public void setSlot ( int index, Object value )
此方法设置 JavaScript 对象的索引成员。它等同于 JavaScript 中的 `this[index] = value`。
public String toString ()
此方法将 JSObject 转换为字符串。
正如上面提供的示例所示,JSObject 类的公共方法不限于从 JavaApplet 中调用 JavaScript 函数。它们还允许小程序直接操作文档对象模型(DOM)元素。
有关这些类的完整文档,请访问 http://developer.netscape.com/docs/manuals/communicator/jsref/pkg.htm
该文档还解释了 Java 和 JavaScript 之间如何处理数据类型。
MAYSCRIPT 属性的重要性
即使小程序使用 JSObject 调用 JavaScript 函数或直接访问文档对象模型,如果 «applet» 标签不包含 MAYSCRIPT 属性,JSObject 方法将失败。这使得页面设计者能够确定小程序是否可以调用 JavaScript。
无界面小程序之间的通信
当在网页中使用无界面的、可重用的 Java 小程序时,一个小程序可能需要直接与其他小程序通信。这种调用也可以通过中间的 JavaScript 函数来完成,但最好了解所有可用的选项。
«java.applet» 包中的 `AppletContext` 接口为小程序提供了对其执行上下文的有限访问,例如小程序运行的浏览器、小程序所在的网页以及同一页面上的其他小程序。
例如,下面是一个包含两个小程序的 HTML 页面:
<html>
<head>
<title>Communication between applets</title>
</head>
<body>
<applet code="CircleArea.class" name="CircleArea" width=1 height=1>
</applet>
<applet code="PICalculator.class" name="PICalculator" width=1 height=1>
</applet>
...
</body>
</html>
以下代码展示了 AppletContext 对象的用法:
AppletContext context = getAppletContext();
PICalculator PIApplet = (PICalculator) context.getApplet ( "PICalculator" );
PIApplet.getValueOfPI();
另一个选项是使用 `AppletContext::getApplets()` 方法,该方法返回一个枚举来访问文档中的所有小程序。尽管小程序之间的通信是标准 Java 小程序 API 的一部分,但并非所有支持 Java 的浏览器都支持。硬编码其他小程序的名称也是不灵活的。最好的方法可能是使用中间的 JavaScript 函数来处理此类通信。
Java 中的 IUnknown::QueryInterface
Class Object 是类层次结构的根。每个类都有 Object 作为超类。所有对象(包括数组)都实现了此对象的方法。`Object::getClass()` 方法返回“Class”,它有许多有用的函数来发现 Java 类本身的详细信息。虽然详细解释超出了本文的范围,但我希望指出 `getInterfaces()` 方法,该方法允许动态发现 Java 类实现的接口。
我没有尝试从 JavaScript 中调用 `getClass()` 方法,因此无法对其可行性发表评论。但是,使用基础设施类型的小程序,可以轻松地将此功能提供给脚本代码。
安装示例文件
本文的示例代码打包在 Java2JavaScript.zip 文件中。示例文件演示了一个聊天应用程序,该应用程序已简化为在客户端自身上路由消息。在实际生活中,聊天参与者位于不同的机器上,消息被发送到一个服务器,然后服务器将其广播给所有参与者。
构成示例的文件包括:
ISession.java |
定义 ISession 接口的源文件。 |
ISession.class |
ISession 接口的 Java 字节码。 |
ChatClient.java |
演示 ChatClient 小程序的源文件。 |
ChatClient.class |
ChatClient 小程序类的 Java 字节码。 |
CompileChatClient.bat |
Java 源文件的原始 makefile。 |
TestChatClient.htm |
承载 ChatClient 小程序的 HTML 文件。 |
JSObject.class |
JSObject 类的字节码。 |
JSException.class |
JSException 类的字节码。 |
安装和运行示例程序的步骤是:
-
将 Java2JavaScript.zip 文件解压缩到运行个人 Web 服务器或 Internet 信息服务器实例的计算机上的任意目录。
- 确保 JSObject 和 JSException 类文件在安装目录下的 netscape\javascript 子目录中可用。
- 右键单击安装目录,然后选择“属性”。
- 单击“Web 共享”选项卡,然后选择“共享此文件夹”单选按钮,接受弹出对话框中显示的默认“虚拟目录”。
- 最后,启动 Internet Explorer 4.0+ 或 Netscape Navigator 4.0+ 的实例,然后输入 URL http:// <machine name>/<virtual directory>/TestChatClient.htm。
在两个输入字段中输入一些文本,然后单击相应的“发送”按钮,您将在聊天窗口中看到显示的消息。
示例代码说明
聊天应用程序是 Web 上流行的协作机制。我见过的聊天应用程序有两种类型:
-
一个 Java 小程序,负责管理用户界面以及与服务器的所有通信。
- 一个每隔几秒钟自动刷新的 HTML 页面,以显示自上次刷新以来发送的任何新聊天消息。
最近,我不得不为我们的电子商务产品实现一个生产级别的聊天应用程序。经过深思熟虑,我决定采用一种混合方法。首先,Java 小程序是一个无界面的小程序,它实现了 ISession 接口。
public interface ISession
{
// Type is used to differentiate the actual message string
// and can be set to ‘Text’, Hyperlink’ etc.
// Should be invoked first to indicate to the server that a
// new person has joined the chat.
public int BeginSession(String strAuthor, String strOptions, String strType, String strMessage);
// Should be invoked when the author wants to exit the chat.
public int EndSession(String strAuthor, String strType, String strMessage);
// Used to send the chat messages.
public int SendMessage(String strAuthor, String strType, String strMessage);
}
我已对该接口进行了修改,使其与 Chat 应用程序的生产版本略有不同,以便为 `EndSession()` 和 `SendMessage()` 都包含 Author 参数。这是因为我的演示使用一个小程序来路由来自两个不同作者的聊天消息。
ChatClient.java 文件是 ISession 接口的实际实现。如果已实现,浏览器将调用 `init()`、`start()` 和 `stop()` 函数。文档窗口的 JSObject 在 `init()` 方法期间获取。
// Get the JavaScript window that will have the various scripts that this applet will call.
m_JScriptWin = JSObject.getWindow(this);
由于 Java 小程序会调用两个不同的 JavaScript 函数,因此我决定允许 Web 开发者通过参数将这些函数的名称指定为小程序的参数,同时为它们提供默认值。
m_strMessageHandler = getParameter("MessageHandler");
m_strErrorHandler = getParameter("ErrorHandler");
`BeginSession()` 和 `EndSession()` 是虚拟实现,它们仅调用 `SendMessage()`。
`SendMessage()` 调用存储在 m_strMessageHandler 成员变量中的 JavaScript 函数。默认值为“HandleSessionMessage”。相关代码如下所示。
if (m_JScriptWin != null)
{
String Arguments[] = {strAuthor, strType, strMessage};
m_JScriptWin.call(m_strMessageHandler, Arguments);
}
`HandleSessionError()` 调用存储在 `m_strErrorHandler` 成员变量中的 JavaScript 函数。默认值为“HandleSessionError”。
TestChatClient.htm 文件处理聊天程序的表示方面。使用 «applet» 标签将小程序包含在页面中。
<applet id="ChatApplet" width="1" height="1" code="ChatClient.class" codebase="." VIEWASTEXT mayscript>
<param name="MessageHandler" value="HandleSessionMessageEx">
<param name="ErrorHandler" value="HandleSessionErrorEx">
</applet>
参数指定了小程序调用的两个 JavaScript 函数的名称。我仅使用了与默认值不同的名称来演示其灵活性。
页面上的两个表单模拟了两个人之间的聊天。相应的 HTML 非常简单。
实际的消息本身显示在 ChatMessages «DIV» 中。对于 Internet Explorer,我使用表格对象模型将每条消息显示在一个单独的行中。因此,ChatMessagesTable 定义在上述 DIV 内。
最后,`HandleSessionMessagesEx()` JavaScript 函数负责所有表示方面的工作。在 Internet Explorer 中,对于发送的每条消息,都会在 ChatMessagesTable 表中添加一个新行。如果需要,将显示滚动条。在 Netscape Navigator 中,我将新消息追加到 Messages 变量,并使用后者来更新 ChatMessages «DIV»。由于无法在 Netscape «DIV»(实际上是一个 LAYER)中自动显示滚动条,因此我显示了收到的最后一条消息。我找到了一些关于如何在 Navigator 中支持 LAYER 的滚动条的文章,但这与本次演示无关。
最终想法
本文尝试介绍一些(巧妙的)实现浏览器端逻辑的技术。正如我在本文前面提到的,`JSObject` 被许多应用程序广泛使用,包括 Microsoft Corporation。尽管如此,请仔细考虑您个人的具体情况,以确定本文介绍的技术是否适用。
至于本文附带的演示示例,我认为允许使用 JavaScript / DHTML 实现聊天程序的表示,使得这段代码可以由入门级/初级程序员维护。使用 DHTML / JavaScript 进行用户界面定制也更容易。此外,它还允许使用更强大的表示技术,这些技术似乎与页面的其余内容保持一致。
在此应用程序的生产版本中,我添加了对交换在参与者计算机上打开的超链接的支持,使用层叠样式表为消息动态选择颜色等。
欢迎任何反馈。