用Java代碼處理本地對象的事件
下載本文源代碼
當您需要使用以其他語言編寫的對象時,本地事件源和 Java? 偵聽器之間的通信可能需要一些小技巧 —— 尤其是在多線程環境中。本文通過使用一種透明處理從本地代碼到 JVM 的事件通信的設計模式,幫助您有效地處理傳統的本地庫。
在面向對象系統中,對象可以觸發一組事件。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. 代理設計模式示例
EventSourceProxy 是 CEventSource 類的代理。要將其自身注冊為一個偵聽器,客戶端應該實現 IJMouseDownListener 接口。該接口類似于 IMouseDownListener,但它是用 Java 代碼編寫的。當第一個客戶端調用其 addMouseDownListener() 方法時,它使用 registerListener() 本地方法來將其自身注冊為一個帶 CEventSource 的偵聽器。registerListener() 方法用 C++ 語言來實現。它創建一個 JMouseDownListener 對象,并將其注冊為一個帶 CEventSource 的偵聽器。當 JMouseDownListener 的 onMouseDown 事件被觸發時,JMouseDownListener 中的 onMouseDownListener() 方法使用 Invocation API 來通知 EventSourceProxy。
EventSourceProxy 還維持一組使用它來注冊的偵聽器。無論其 onMouseDown 何時被觸發,它將通知該組中的所有偵聽器。注意,即使針對該事件存在多個 Java 偵聽器,只有代理的一個實例被注冊為具有 CEventSource。代理將 onMouseDown 事件委托給它的所有偵聽器。這防止了本地代碼和 Java 代碼之間不必要的上下文切換。
多線程問題
本地方法接收 JNI 接口指針作為一個參數。但是,一個想要將事件委托回其關聯 Java 代理的本地偵聽器沒有現成的 JNI 接口指針。一旦獲得 JNI 接口指針,應該將其保存起來以便后續使用。
JNI 接口指針只在當前線程中有效。實現 JNI 的 JVM 可以在 JNI 接口指針指向的區域中分配和存儲本地線程數據。這意味著您也需要以本地線程數據保存 JNI 接口指針。
JNI 接口指針可以兩種方式獲得:
環境設置
公共接口
IMouseDownListener 和 IEventSource 接口定義在 common.h 中。IMouseDownListener 只有一個方法:onMouseDown()。該方法接收鼠標單擊的屏幕位置。IEventSource 接口包含了 addMouseDownListener() 和 removeMouseDownListener() 方法,用于注冊和取消注冊偵聽器。
Java Invocation API 的幫助例程
有 7 個必需的常用工具方法可用于簡化 Java Invocation API 的使用,它們定義在 Jvm.h 中,在 Jvm.cpp 中實現:
本地事件源
在 EventSource.h 和 EventSource.cpp 中,CEventSource 是一個簡單而直觀的 IEventSource 接口的實現。
本地事件偵聽器
在 MouseDownListener.h 和 MouseDownListener.cpp 中,CMouseDownListener 是 IMouseDownListener 接口的實現。該本地偵聽器僅出于解釋目的而編寫。
入口點
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 生成頭文件:
為了構建針對 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:
得到的控制臺輸出如下:
正如您從控制臺輸出所看到的,Java 偵聽器產生與出于解釋目的而構建的本地偵聽器相同的結果。
結束語
本文展示了如何為本地應用程序生成的事件注冊一個 Java 類作為偵聽器。通過使用觀察者設計模式,您已經減少了事件源與偵聽器之間的耦合。您還通過使用代理設計模式隱藏了來自 Java 偵聽器的事件源的實現細節。您可以使用該設計模式組合來將一個 Java UI 添加到現有的本地應用程序。
當您需要使用以其他語言編寫的對象時,本地事件源和 Java? 偵聽器之間的通信可能需要一些小技巧 —— 尤其是在多線程環境中。本文通過使用一種透明處理從本地代碼到 JVM 的事件通信的設計模式,幫助您有效地處理傳統的本地庫。
在面向對象系統中,對象可以觸發一組事件。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. 代理設計模式示例

EventSourceProxy 是 CEventSource 類的代理。要將其自身注冊為一個偵聽器,客戶端應該實現 IJMouseDownListener 接口。該接口類似于 IMouseDownListener,但它是用 Java 代碼編寫的。當第一個客戶端調用其 addMouseDownListener() 方法時,它使用 registerListener() 本地方法來將其自身注冊為一個帶 CEventSource 的偵聽器。registerListener() 方法用 C++ 語言來實現。它創建一個 JMouseDownListener 對象,并將其注冊為一個帶 CEventSource 的偵聽器。當 JMouseDownListener 的 onMouseDown 事件被觸發時,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 將接口指針值放在由第一個參數指定的位置。
環境設置
公共接口
IMouseDownListener 和 IEventSource 接口定義在 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()
本地事件源
在 EventSource.h 和 EventSource.cpp 中,CEventSource 是一個簡單而直觀的 IEventSource 接口的實現。
本地事件偵聽器
在 MouseDownListener.h 和 MouseDownListener.cpp 中,CMouseDownListener 是 IMouseDownListener 接口的實現。該本地偵聽器僅出于解釋目的而編寫。
入口點
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 .javain 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 添加到現有的本地應用程序。