内容


用 Java 代码处理本地对象的事件

采用观察者设计模式和代理设计模式简化实现

Comments

在面向对象系统中,对象可以触发一组事件。Java 编程语言为定义基于观察者设计模式(Observer design pattern)的事件侦听器提供了支持,但当您需要使用以其他语言编写的对象时,这还不够。使用 Java Native Interface (JNI) 在本地事件源和 Java 侦听器之间进行通信,可能需要一些技巧,尤其是有多线程环境中。在本文中,我们描述了一种透明处理从本地代码到 JVM 的事件通信的设计模式。您可以使用这种设计来提供到遗留本地应用程序的 Java 接口,或者构建带 Java 侦听器的本地应用程序。

观察者设计模式

观察者设计模式定义了事件侦听器与事件创建者之间的多对一依赖关系。当事件创建者触发一个事件时,其所有侦听器接收到该事件的通知。由于事件创建者和侦听器是无关的,您可以单独使用或修改它们。这种设计模式是事件驱动编程的核心,被广泛用于 GUI 框架,比如 Swing 和 SWT。

如果整个应用程序都使用 Java 编程语言编写,实现观察者设计模式相当简单。图 1 中的类图给出了一个例子:

图 1. 观察者设计模式
观察者设计模式
观察者设计模式

Java 本地应用程序侦听器

不过,在某些情况下,我们想要让本地应用程序支持 Java 侦听器。大量应用程序(包括应用程序入口点)可能以本地代码编写,而应用程序以与其用户界面交互的方式生成事件。在这种情形下,支持基于 Java 用户界面的最佳方式是让 Java 类将自身注册为应用程序生成的各种事件的侦听器。简而言之,通过支持以 Java 语言编写的侦听器,可以获得支持 Java 的 本地应用程序。

图 2 所示的类图给出了一个示例场景。CEventSource 类用 C++ 语言编写。它使用 addMouseDownListener()removeMouseDownListener() 让侦听器注册和取消注册其 “鼠标按下” 事件。它想要所有侦听器实现 IMouseDownListener 接口。

图 2. 示例场景的类图
示例场景的类图
示例场景的类图

注意,IMouseDownListener 是一个 C++ 抽象类。那么,Java 类如何注册事件才不会引入 Java 侦听器和 CEventSource 类之间的编译时绑定呢?在其注册事件后,CEventSource 类如何调用 Java 类中的方法呢?这正是 Java Invocation API 的用武之地。

Java Invocation API

Invocation API 让您可以将 JVM 加载到一个本地应用程序中,而不必显式地链接 JVM 源(请参阅 参考资料)。 通过在 jvm.dll 中调用一个函数,可以创建一个 JVM,jvm.dll 还将当前本地线程连接到 JVM。然后,您可以在 JVM 中从本地线程调用所有 Java 方法。

然而,Invocation API 无法彻底解决问题。您不希望 CEventSource 类具有与 Java 侦听器的编译时依赖关系。另外,Java 侦听器不应该承担使用 JNI 来注册带 CEventSource 的侦听器的责任。

代理设计模式

通过使用代理设计模式(Proxy design pattern),可以避免这一弊端。通常来说,代理是另一个对象的占位符。客户端对象可以处理代理对象,而代理封装了所有使用本地方法的细节。代理模式的细节显示在图 3 中:

图 3. 代理设计模式示例
代理设计模式
代理设计模式

EventSourceProxyCEventSource 类的代理。要将其自身注册为一个侦听器,客户端应该实现 IJMouseDownListener 接口。该接口类似于 IMouseDownListener,但它是用 Java 代码编写的。当第一个客户端调用其 addMouseDownListener() 方法时,它使用 registerListener() 本地方法来将其自身注册为一个带 CEventSource 的侦听器。registerListener() 方法用 C++ 语言来实现。它创建一个 JMouseDownListener 对象,并将其注册为一个带 CEventSource 的侦听器。当 JMouseDownListeneronMouseDown 事件被触发时,JMouseDownListener 中的 onMouseDownListener() 方法使用 Invocation API 来通知 EventSourceProxy

EventSourceProxy 还维持一组使用它来注册的侦听器。无论其 onMouseDown 何时被触发,它将通知该组中的所有侦听器。注意,即使针对该事件存在多个 Java 侦听器,只有代理的一个实例被注册为具有 CEventSource。代理将 onMouseDown 事件委托给它的所有侦听器。这防止了本地代码和 Java 代码之间不必要的上下文切换。

多线程问题

本地方法接收 JNI 接口指针作为一个参数。但是,一个想要将事件委托回其关联 Java 代理的本地侦听器没有现成的 JNI 接口指针。一旦获得 JNI 接口指针,应该将其保存起来以便后续使用。

JNI 接口指针只在当前线程中有效。实现 JNI 的 JVM 可以在 JNI 接口指针指向的区域中分配和存储本地线程数据。这意味着您也需要以本地线程数据保存 JNI 接口指针。

JNI 接口指针可以两种方式获得:

  • 一旦线程使用 JNI_CreateJavaVM 创建了 JVM,JNI 将接口指针值放在由第二个参数指定的位置。然后该值可以保存在本地线程区域中。
  • 如果 JVM 已由进程中某个其他线程创建,当前线程可以调用 AttachCurrentThread。JNI 将接口指针值放在由第一个参数指定的位置。

但是这还没有完。需要记住的是,程序是以 C/C++ 语言编写的,因此,无法使用自动垃圾回收,因为程序不是使用 Java 语言编写的。一旦线程完成 JNI 调用,它需要通过调用 DetachCurrentThread 来释放接口指针。如果未做此调用并且线程存在,进程将无法正常终止。相反,它将一直等待现在已不存在的线程以 DestroyJavaVM 调用的方式从 JVM 中离开。

在示例代码中(参见 下载),所有这些都封装在 CJvm 类的不同位置。另外,通过触发来自从主线程分离的工作线程的事件,还示范了多线程的应用。

环境设置和示例代码

现在,您已经准备好探索、构建和运行示例代码了(参见 下载)。

公共接口

IMouseDownListenerIEventSource 接口定义在 common.h 中。IMouseDownListener 只有一个方法:onMouseDown()。该方法接收鼠标单击的屏幕位置。IEventSource 接口包含了 addMouseDownListener()removeMouseDownListener() 方法,用于注册和取消注册侦听器。

Java Invocation API 的帮助例程

有 7 个必需的常用工具方法可用于简化 Java Invocation API 的使用,它们定义在 Jvm.h 中,在 Jvm.cpp 中实现:

  • CreateJavaObject() 创建一个 Java 对象,给出其类名和针对本地 IEventSource 的句柄。这个创建好的 Java 对象将用于侦听来自该本地句柄的事件。该句柄通过其构造方法传递给对象。
  • ReleaseJObject() 调用 Java 对象的 release() 方法。该方法用于从 EventSourceProxy 取消注册对象的侦听器。
  • DetachThread() 从 JVM 分离当前线程(如果当前线程连接到 JVM)。当线程正被连接时,该调用对于释放特定于线程的、已分配给 JNI 的资源是很必要的。

其余方法都自己附带有解释:

  • CreateJVM()
  • DestroyJVM()
  • GetJVM()
  • GetJNIEnv()

CJvm 类还在特定于线程的位置保存一个 JNI 环境指针。这是很有必要的,因为 JNI 环境指针是线程相关的。

本地事件源

在 EventSource.h 和 EventSource.cpp 中,CEventSource 是一个简单而直观的 IEventSource 接口的实现。

本地事件侦听器

在 MouseDownListener.h 和 MouseDownListener.cpp 中,CMouseDownListenerIMouseDownListener 接口的实现。该本地侦听器仅出于解释目的而编写。

入口点

main.cpp 包含 main()ThreadMain()main() 创建一个本地 EventSource、一个本地侦听器和一个 Java 侦听器。然后创建线程,并在睡眠几秒后让它执行。最后,它释放 Java 侦听器并销毁 JVM。

ThreadMain() 简单地触发一个事件,然后将自身从 JVM 分离出来。

Java 模块

IJMouseDownListener.java 中的 IJMouseDownListener 只是本地接口针对 Java 平台的一个克隆。

MouseDownListener 是 Java 中的一个示例侦听器,在 MouseDownListener.java 中实现。它在其构造方法中接收本地 EventSource 句柄。它定义了一个 release() 方法,该方法取消注册带 EventSourceProxy 的侦听器。

EventSourceProxy 是一个用于来自本地模块的 EventSource 的占位符或代理项。它在 EventSourceProxy.java 中实现。它维持一个静态哈希表,以将一个代理映射到实际 EventSource

addMouseDownListener()removeMouseDownListener() 允许您维持一个 Java 侦听器集合。单个本地 EventSource 可以有多个 Java 侦听器,但只有在必要时代理才注册/取消注册本地 EventSource

当从本地 EventSource 转发事件时,EventSourceProxy 的本地实现调用 fireMouseDownEvent()。该方法迭代 Java 侦听器的哈希集合,并通知它们。

EventSourceProxy 的本地部分还维持一个到自身的全局引用。这对于稍后调用 fireMouseDownEvent() 是很必要的。

构建并执行示例代码

示例代码中的所有 Java 类都使用普通过程构建,无需特殊步骤。对于 EventSourceProxy 的本地实现,您需要使用 javah 生成头文件:

javah -classpath .\java\bin events.EventSourceProxy

为了构建针对 Win32 平台的 C++ 模块,我们提供了 Microsoft Developer Studio 项目文件和 cpp.dsw 工作区。您可以打开工作区,简单地构建 main 项目。工作区中的所有项目都以适当的依赖关系相关联。确保您的 Developer Studio 可以找到 JNI 头和编译时 JNI 库。可以通过选择 Tools > Options > Directories 菜单项完成这一工作。

构建成功之后,在可以执行示例程序之前,还需要完成几个步骤。

首先,因为用于构建 Java 类并包含 JNI 头和库的 JDK 可能有针对 Java Invocation API 的运行时组件,例如 jvm.dll,您必需设置它。最简单的方法是更新 PATH 变量。

其次,main 程序带有命令行参数,这些参数是简单的 JVM 参数。您需要至少传递两个参数给 JVM:

main.exe "-Djava.class.path=.\\java\\bin" 
"-Djava.library.path=.\\cpp\\listener\\Debug"

得到的控制台输出如下:

In CMouseDownListener::onMouseDown
        X = 50
        Y = 100
In MouseDownListener.onMouseDown
        X = 50
        Y = 100

正如您从控制台输出所看到的,Java 侦听器产生与出于解释目的而构建的本地侦听器相同的结果。

结束语

本文展示了如何为本地应用程序生成的事件注册一个 Java 类作为侦听器。通过使用观察者设计模式,您已经减少了事件源与侦听器之间的耦合。您还通过使用代理设计模式隐藏了来自 Java 侦听器的事件源的实现细节。您可以使用该设计模式组合来将一个 Java UI 添加到现有的本地应用程序。


下载资源


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • Java Native Interface 规范:JNI 允许运行在 JVM 中的 Java 代码与以其他编程语言编写的应用程序和库实现互操作。
  • The Invocation API:The Invocation API 是 JNI 规范的一部分。
  • e-BIT bytes: JNI pitfalls”(作者:Mark Bluemel,developerWorks,2002 年 6 月):这篇技术文章展示了 JNI 分解的一些实例,并提供了一些何时绝对应该使用它的建议。
  • Design Patterns: Elements of Reusable Object-Oriented Software,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 编著(Addison-Wesley,1995 年):一本关于面向对象的设计模式的经典教材。
  • Head First Design Patterns,由 Elisabeth Freeman、Eric Freeman、Bert Bates 和 Kathy Sierra 编著(O'Reilly Media,2004 年):一本使用丰富的实际方法讲授设计模式的书。
  • Java 技术专区:数百篇有关 Java 编程各个方面的文章。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=163086
ArticleTitle=用 Java 代码处理本地对象的事件
publish-date=03062006