JAVA語言及其標準API(應用程序編程接口)應付應用程序的編寫已綽綽有餘。但在某些情況下,還是必須使用非JAVA編碼。例如,我們有時要訪問操作系統的專用特性,與特殊的硬件設備打交道,重複使用現有的非Java接口,或者要使用“對時間敏感”的代碼段,等等。與非Java代碼的溝通要求獲得編譯器和“虛擬機”的專門支持,並需附加的工具將Java代碼映射成非Java代碼(也有一個簡單方法:在第15章的“一個Web應用”小節中,有個例子解釋瞭如何利用標準輸入輸出同非Java代碼連接)。目前,不同的開發商為我們提供了不同的方案:Java 1.1有“Java固有接口”(Java Native Interface,JNI),網景提出了自己的“Java運行期接口”(Java Runtime Interface)計劃,而微軟提供了J/Direct、“本源接口”(Raw Native Interface,RNI)以及Java/COM集成方案。
各開發商在這個問題上所持的不同態度對程序員是非常不利的。若Java應用必須調用固有方法,則程序員或許要實現固有方法的不同版本——具體由應用程序運行的平臺決定。程序員也許實際需要不同版本的Java代碼,以及不同的Java虛擬機。
另一個方案是CORBA(通用對象請求代理結構),這是由OMG(對象管理組,一家非贏利性的公司協會)開發的一種集成技術。CORBA並非任何語言的一部分,只是實現通用通信總線及服務的一種規範。利用它可在由不同語言實現的對象之間實現“相互操作”的能力。這種通信總線的名字叫作ORB(對象請求代理),是由其他開發商實現的一種產品,但並不屬於Java語言規範的一部分。 本附錄將對JNI,J/DIRECT,RNI,JAVA/COM集成和CORBA進行概述。但不會作更深層次的探討,甚至有時還假定讀者已對相關的概念和技術有了一定程度的認識。但到最後,大家應該能夠自行比較不同的方法,並根據自己要解決的問題挑選出最恰當的一種。
JNI是一種包容極廣的編程接口,允許我們從Java應用程序裡調用固有方法。它是在Java 1.1裡新增的,維持著與Java 1.0的相應特性——“固有方法接口”(NMI)——某種程度的兼容。NMI設計上一些特點使其未獲所有虛擬機的支持。考慮到這個原因,Java語言將來的版本可能不再提供對NMI的支持,這兒也不準備討論它。
目前,JNI只能與用C或C++寫成的固有方法打交道。利用JNI,我們的固有方法可以:
-
創建、檢查及更新Java對象(包括數組和字符串)
-
調用Java方法
-
俘獲和丟棄“異常”
-
裝載類並獲取類信息
-
進行運行期類型檢查
所以,原來在Java中能對類及對象做的幾乎所有事情在固有方法中同樣可以做到。
我們先從一個簡單的例子開始:一個Java程序調用固有方法,後者再調用Win32的API函數MessageBox()
,顯示出一個圖形化的文
本框。這個例子稍後也會與J/Direct一志使用。若您的平臺不是Win32,只需將包含了下述內容的C頭:
#include <windows.h>
替換成:
#include <stdio.h>
並將對MessageBox()
的調用換成調用printf()
即可。
第一步是寫出對固有方法及它的參數進行聲明的Java代碼:
class ShowMsgBox {
public static void main(String [] args) {
ShowMsgBox app = new ShowMsgBox();
app.ShowMessage("Generated with JNI");
}
private native void ShowMessage(String msg);
static {
System.loadLibrary("MsgImpl");
}
}
在固有方法聲明的後面,跟隨有一個static
代碼塊,它會調用System.loadLibrary()
(可在任何時候調用它,但這樣做更恰當)System.loadLibrary()
將一個DLL載入內存,並建立同它的鏈接。DLL必須位於您的系統路徑,或者在包含了Java類文件的目錄中。根據具體的平臺,JVM會自動添加適當的文件擴展名。
現在編譯您的Java源文件,並對編譯出來的.class
文件運行javah
。javah
是在1.0版裡提供的,但由於我們要使用Java 1.1 JNI,所以必須指定-jni
參數:
javah -jni ShowMsgBox
javah會讀入類文件,併為每個固有方法聲明在C或C++頭文件裡生成一個函數原型。下面是輸出結果——ShowMsgBox.h源文件(為符合本書的要求,稍微進行了一下修改):
/* DO NOT EDIT THIS FILE
- it is machine generated */
#include <jni.h>
/* Header for class ShowMsgBox */
#ifndef _Included_ShowMsgBox
#define _Included_ShowMsgBox
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: ShowMsgBox
* Method: ShowMessage
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL
Java_ShowMsgBox_ShowMessage
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
從#ifdef_cplusplus
這個預處理引導命令可以看出,該文件既可由C編譯器編譯,亦可由C++編譯器編譯。第一個#include
命令包括jni.h
——一個頭文件,作用之一是定義在文件其餘部分用到的類型;JNIEXPORT
和JNICALL
是一些宏,它們進行了適當的擴充,以便與那些不同平臺專用的引導命令配合;JNIEnv
,jobject
以及jstring
則是JNI數據類型定義。
JNI統一了固有方法的命名規則;這一點是非常重要的,因為它屬於虛擬機將Java調用與固有方法鏈接起來的機制的一部分。從根本上說,所有固有方法都要以一個“Java”起頭,後面跟隨Java方法的名字;下劃線字符則作為分隔符使用。若Java固有方法“重載”(即命名重複),那麼也把函數簽名追加到名字後面。在原型前面的註釋裡,大家可看到固有的簽名。欲瞭解命名規則和固有方法簽名更詳細的情況,請參考相應的JNI文檔。
此時,我們要做的全部事情就是寫一個C或C++源文件,在其中包含由javah
生成的頭文件;並實現固有方法;然後編譯它,生成一個動態鏈接庫。這一部分的工作是與平臺有關的,所以我假定讀者已經知道如何創建一個DLL。通過調用一個Win32 API,下面的代碼實現了固有方法。隨後,它會編譯和鏈接到一個名為MsgImpl.dll
的文件裡:
#include <windows.h>
#include "ShowMsgBox.h"
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD dwReason, void** lpReserved) {
return TRUE;
}
JNIEXPORT void JNICALL
Java_ShowMsgBox_ShowMessage(JNIEnv * jEnv,
jobject this, jstring jMsg) {
const char * msg;
msg = (*jEnv)->GetStringUTFChars(jEnv, jMsg,0);
MessageBox(HWND_DESKTOP, msg,
"Thinking in Java: JNI",
MB_OK | MB_ICONEXCLAMATION);
(*jEnv)->ReleaseStringUTFChars(jEnv, jMsg,msg);
}
若對Win32沒有興趣,只需跳過MessageBox()
調用;最有趣的部分是它周圍的代碼。傳遞到固有方法內部的參數是返回Java的大門。第一個參數是類型JNIEnv
的,其中包含了回調JVM需要的所有掛鉤(下一節再詳細講述)。由於方法的類型不同,第二個參數也有自己不同的含義。對於象上例那樣的非static
方法(也叫作實例方法),第二個參數等價於C++的this
指針,並類似於Java的this
:都引用了調用固有方法的那個對象。對於static
方法,它是對特定Class
對象的一個引用,方法就是在那個Class
對象裡實現的。
剩餘的參數代表傳遞到固有方法調用裡的Java對象。基本類型也是以這種形式傳遞的,但它們進行的“按值”傳遞。
在後面的小節裡,我們準備講述如何從一個固有方法的內部訪問和控制JVM,同時對上述代碼進行更詳盡的解釋。
利用JNI函數,程序員可從一個固有方法的內部與JVM打交道。正如大家在前面的例子中看到的那樣,每個JNI固有方法都會接收一個特殊的參數作為自己的第一個參數:JNIEnv
參數——它是指向類型為JNIEnv_
的一個特殊JNI數據結構的指針。JNI數據結構的一個元素是指向由JVM生成的一個數組的指針;該數組的每個元素都是指向一個JNI函數的指針。可從固有方法的內部發出對JNI函數的調用,做法是撤消對這些指針的引用(具體的操作實際很簡單)。每種JVM都以自己的方式實現了JNI函數,但它們的地址肯定位於預先定義好的偏移處。
利用JNIEnv
參數,程序員可訪問一系列函數。這些函數可劃分為下述類別:
- 獲取版本信息
- 進行類和對象操作
- 控制對Java對象的全局和局部引用
- 訪問實例字段和靜態字段
- 調用實例方法和靜態方法
- 執行字符串和數組操作
- 產生和控制Java異常
JNI函數的數量相當多,這裡不再詳述。相反,我會向大家揭示使用這些函數時背後的一些基本原理。欲瞭解更詳細的情況,請參閱自己所用編譯器的JNI文檔。
若觀察一下jni.h
頭文件,就會發現在#ifdef _cplusplus
預處理器條件的內部,當由C++編譯器編譯時,JNIEnv_
結構被定義成一個類。這個類包含了大量內嵌函數。通過一種簡單而且熟悉的語法,這些函數讓我們可以從容訪問JNI函數。例如,前例包含了下面這行代碼:
(*jEnv)->ReleaseStringUTFChars(jEnv, jMsg,msg);
它在C++裡可改寫成下面這個樣子:
jEnv->ReleaseStringUTFChars(jMsg,msg);
大家可注意到自己不再需要同時撤消對jEnv
的兩個引用,相同的指針不再作為第一個參數傳遞給JNI函數調用。在這些例子剩下的地方,我會使用C++風格的代碼。
作為訪問JNI函數的一個例子,請思考上述的代碼。在這裡,我們利用JNIEnv
的參數jEnv
來訪問一個Java字符串。Java字符串採取的是Unicode格式,所以假若收到這樣一個字符串,並想把它傳給一個非Unicode函數(如printf()
),首先必須用JNI函數GetStringUTFChars()
將其轉換成ASCII字符。該函數能接收一個Java字符串,然後把它轉換成UTF-8字符(用8位寬度容納ASCII值,或用16位寬度容納Unicode;若原始字符串的內容完全由ASCII構成,那麼結果字符串也是ASCII)。
GetStringUTFChars
是JNIEnv
間接指向的那個結構裡的一個字段,而這個字段又是指向一個函數的指針。為訪問JNI函數,我們用傳統的C語法來調用一個函數(通過指針)。利用上述形式可實現對所有JNI函數的訪問。
在前例中,我們將一個字符串傳遞給固有方法。事實上,亦可將自己創建的Java對象傳遞給固有方法。
在我們的固有方法內部,可訪問已收到的那些對象的字段及方法。
為傳遞對象,聲明固有方法時要採用原始的Java語法。如下例所示,MyJavaClass
有一個public
(公共)字段,以及一個public
方法。UseObjects
類聲明瞭一個固有方法,用於接收MyJavaClass
類的一個對象。為調查固有方法是否能控制自己的參數,我們設置了參數的public
字段,調用固有方法,然後打印出public
字段的值。
class MyJavaClass {
public void divByTwo() { aValue /= 2; }
public int aValue;
}
public class UseObjects {
public static void main(String [] args) {
UseObjects app = new UseObjects();
MyJavaClass anObj = new MyJavaClass();
anObj.aValue = 2;
app.changeObject(anObj);
System.out.println("Java: " + anObj.aValue);
}
private native void
changeObject(MyJavaClass obj);
static {
System.loadLibrary("UseObjImpl");
}
}
編譯好代碼,並將.class
文件傳遞給javah
後,就可以實現固有方法。在下面這個例子中,一旦取得字段和方法ID,就會通過JNI函數訪問它們。
JNIEXPORT void JNICALL
Java_UseObjects_changeObject(
JNIEnv * env, jobject jThis, jobject obj) {
jclass cls;
jfieldID fid;
jmethodID mid;
int value;
cls = env->GetObjectClass(obj);
fid = env->GetFieldID(cls,
"aValue", "I");
mid = env->GetMethodID(cls,
"divByTwo", "()V");
value = env->GetIntField(obj, fid);
printf("Native: %d\n", value);
env->SetIntField(obj, fid, 6);
env->CallVoidMethod(obj, mid);
value = env->GetIntField(obj, fid);
printf("Native: %d\n", value);
}
除第一個參數外,C++函數會接收一個jobject
,它代表Java對象引用“固有”的那一面——那個引用是我們從Java代碼裡傳遞的。我們簡單地讀取aValue
,把它打印出來,改變這個值,調用對象的divByTwo()
方法,再將值重新打印一遍。
為訪問一個字段或方法,首先必須獲取它的標識符。利用適當的JNI函數,可方便地取得類對象、元素名以及簽名信息。這些函數會返回一個標識符,利用它可訪問對應的元素。儘管這一方式顯得有些曲折,但我們的固有方法確實對Java對象的內部佈局一無所知。因此,它必須通過由JVM返回的索引訪問字段和方法。這樣一來,不同的JVM就可實現不同的內部對象佈局,同時不會對固有方法造成影響。
若運行Java程序,就會發現從Java那一側傳來的對象是由我們的固有方法處理的。但傳遞的到底是什麼呢?是指針,還是Java引用?而且垃圾收集器在固有方法調用期間又在做什麼呢?
垃圾收集器會在固有方法執行期間持續運行,但在一次固有方法調用期間,我們的對象可保證不會被當作“垃圾”收集去。為確保這一點,事先創建了“局部引用”,並在固有方法調用之後立即清除。由於它們的“生命期”與調用過程息息相關,所以能夠保證對象在固有方法調用期間的有效性。
由於這些引用會在每次函數調用的時候創建和析構,所以不可在static
變量中製作固有方法的局部副本(本地拷貝)。若希望一個引用在函數存在期間持續有效,就需要一個全局引用。全局引用不是由JVM創建的,但通過調用特定的JNI函數,程序員可將局部引用擴展為全局引用。創建一個全局引用時,需對引用對象的“生存時間”負責。全局引用(以及它引用的對象)會一直留在內存裡,直到用特定的JNI函數明確釋放了這個引用。它類似於C的malloc()
和free()
。
利用JNI,可丟棄、捕捉、打印以及重新丟棄Java異常,就象在一個Java程序裡那樣。但對程序員來說,需自行調用專用的JNI函數,以便對異常進行處理。下面列出用於異常處理的一些JNI函數:
Throw()
:丟棄一個現有的異常對象;在固有方法中用於重新丟棄一個異常。ThrowNew()
:生成一個新的異常對象,並將其丟棄。ExceptionOccurred()
:判斷一個異常是否已被丟棄,但尚未清除。ExceptionDescribe()
:打印一個異常和棧跟蹤信息。ExceptionClear()
:清除一個待決的異常。FatalError()
:造成一個嚴重錯誤,不返回。
在所有這些函數中,最不能忽視的就是ExceptionOccurred()
和ExceptionClear()
。大多數JNI函數都能產生異常,而且沒有象在Java的try
塊內的那種語言特性可供利用。所以在每一次JNI函數調用之後,都必須調用ExceptionOccurred()
,瞭解異常是否已被丟棄。若偵測到一個異常,可選擇對其加以控制(可能時還要重新丟棄它)。然而,必須確保異常最終被清除。這可以在自己的函數中用ExceptionClear()
來實現;若異常被重新丟棄,也可能在其他某些函數中進行。但無論如何,這一工作是必不可少的。
我們必須保證異常被徹底清除。否則,假若在一個異常待決的情況下調用一個JNI函數,獲得的結果往往是無法預知的。也有少數幾個JNI函數可在異常時安全調用;當然,它們都是專門的異常控制函數。
由於Java是一種多線程語言,幾個線程可能同時發出對一個固有方法的調用(若另一個線程發出調用,固有方法可能在運行期間暫停)。此時,完全要由程序員來保證固有調用在多線程的環境中安全進行。例如,要防範用一種未進行監視的方法修改共享數據。此時,我們主要有兩個選擇:將固有方法聲明為“同步”,或在固有方法內部採取其他某些策略,確保數據處理正確地併發進行。
此外,絕對不要通過線程傳遞JNIEnv
,因為它指向的內部結構是在“每線程”的基礎上分配的,而且包含了只對那些特定的線程才有意義的信息。
為實現JNI固有方法,最簡單的方法就是在一個Java類裡編寫固有方法的原型,編譯那個類,再通過javah
運行.class
文件。但假若我們已有一個大型的、早已存在的代碼庫,而且想從Java裡調用它們,此時又該如何是好呢?不可將DLL中的所有函數更名,使其符合JNI命名規則,這種方案是不可行的。最好的方法是在原來的代碼庫“外面”寫一個封裝DLL。Java代碼會調用新DLL裡的函數,後者再調用原始的DLL函數。這個方法並非僅僅是一種解決方案;大多數情況下,我們甚至必須這樣做,因為必須面向對象引用調用JNI函數,否則無法使用它們。
到本書完稿時為止,微軟仍未提供對JNI的支持,只是用自己的專利方法提供了對非Java代碼調用的支持。這一支持內建到編譯器Microsoft JVM以及外部工具中。只有程序用Microsoft Java編譯器編譯,而且只有在Microsoft Java虛擬機(JVM)上運行的時候,本節講述的特性才會有效。若計劃在因特網上發行自己的應用,或者本單位的內聯網建立在不同平臺的基礎上,就可能成為一個嚴重的問題。
微軟與Win32代碼的接口為我們提供了連接Win32的三種途徑:
(1) J/Direct:方便調用Win32 DLL函數的一種途徑,具有某些限制。
(2) 本原接口(RNI):可調用Win32 DLL函數,但必須自行解決“垃圾收集”問題。
(3) Java/COM集成:可從Java裡直接揭示或調用COM服務。
後續的小節將分別探討這三種技術。
寫作本書的時候,這些特性均通過了Microsoft SDK for Java 2.0 beta 2的支持。可從微軟公司的Web站點下載這個開發平臺(要經歷一個痛苦的選擇過程,他們叫作“Active Setup”)。Java SDK是一套命令行工具的集合,但編譯引擎可輕易嵌入Developer Studio環境,以便我們用Visual J++ 1.1來編譯Java 1.1代碼。
J/Direct是調用Win32 DLL函數最簡單的方式。它的主要設計目標是與Win32API打交道,但完全可用它調用其他任何API。但是,儘管這一特性非常方便,但它同時也造成了某些限制,且降低了性能(與RNI相比)。但J/Direct也有一些明顯的優點。首先,除希望調用的那個DLL裡的代碼之外,沒有必要再編寫額外的非Java代碼,換言之,我們不需要一個包裝器或者代理/存根DLL。其次,函數參數與標準數據類型之間實現了自動轉換。若必須傳遞用戶自定義的數據類型,那麼J/Direct可能不按我們的希望工作。第三,就象下例展示的那樣,它非常簡單和直接。只需少數幾行,這個例子便能調用Win32 API函數MessageBox()
,它能彈出一個小的模態窗口,並帶有一個標題、一條消息、一個可選的圖標以及幾個按鈕。
public class ShowMsgBox {
public static void main(String args[])
throws UnsatisfiedLinkError {
MessageBox(0,
"Created by the MessageBox() Win32 func",
"Thinking in Java", 0);
}
/** @dll.import("USER32") */
private static native int
MessageBox(int hwndOwner, String text,
String title, int fuStyle);
}
令人震驚的是,這裡便是我們利用J/Direct調用Win32 DLL函數所需的全部代碼。其中的關鍵是位於示範代碼底部的MessageBox()
聲明之前的@dll.import
引導命令。它表面上看是一條註釋,但實際並非如此。它的作用是告訴編譯器:引導命令下面的函數是在USER32 DLL
裡實現的,而且應相應地調用。我們要做的全部事情就是提供與DLL內實現的函數相符的一個原型,並調用函數。但是毋需在Java版本里手工鍵入需要的每一個Win32 API函數,一個Microsoft Java包會幫我們做這件事情(很快就會詳細解釋)。為了讓這個例子正常工作,函數必須“按名稱”由DLL導出。但是,也可以用@dll.import
引導命令“按順序”鏈接。舉個例子來說,我們可指定函數在DLL裡的入口位置。稍後還會具體講述@dll.import
引導命令的特性。
用非Java代碼進行鏈接的一個重要問題就是函數參數的自動配置。正如大家看到的那樣,MessageBox()
的Java聲明採用了兩個字符串參數,但原來的C方案則採用了兩個char
指針。編譯器會幫助我們自動轉換標準數據類型,同時遵照本章後一節要講述的規則。
最好,大家或許已注意到了main()
聲明中的UnsatisfiedLinkError
異常。在運行期的時候,一旦鏈接程序不能從非Java函數裡解析出符號,就會觸發這一異常(事件)。這可能是由多方面的原因造成的:.dll
文件未找到;不是一個有效的DLL;或者J/Direct未獲您所使用的虛擬機的支持。為了使DLL能被找到,它必須位於Windows
或Windows\System
目錄下,位於由PATH環境變量列出的一個目錄中,或者位於和.class
文件相同的目錄。J/Direct獲得了Microsoft Java編譯器1.02.4213版本及更高版本的支持,也獲得了Microsoft JVM 4.79.2164及更高版本的支持。為了解自己編譯器的版本號,請在命令行下運行JVC,不要加任何參數。為了解JVM的版本號,請找到msjava.dll的圖標,並利用右鍵彈出菜單觀察它的屬性。
作為使用J/Direct唯一的途徑,@dll.import
引導命令相當靈活。它提供了為數眾多的修改符,可用它們自定義同非Java代碼建立鏈接關係的方式。它亦可應用於類內的一些方法,或應用於整個類。也就是說,我們在那個類內聲明的所有方法都是在相同的DLL裡實現的。下面讓我們具體研究一下這些特性。
為了使@dll.import
引導命令能象上面顯示的那樣工作,DLL內的函數必須按名字導出。然而,我們有時想使用與DLL裡原始名字不同的一個名字(別名處理),否則函數就可能按編號(比如按順序)導出,而不是按名字導出。下面這個例子聲明瞭FinestraDiMessaggio()
(用意大利語說的MessageBox
)。正如大家看到的那樣,使用的語法是非常簡單的。
public class Aliasing {
public static void main(String args[])
throws UnsatisfiedLinkError {
FinestraDiMessaggio(0,
"Created by the MessageBox() Win32 func",
"Thinking in Java", 0);
}
/** @dll.import("USER32",
entrypoint="MessageBox") */
private static native int
FinestraDiMessaggio(int hwndOwner, String text,
String title, int fuStyle);
}
下面這個例子展示瞭如何同DLL裡並非按名字導出的一個函數建立鏈接,那個函數事實是按它們在DLL裡的位置導出的。這個例子假設有一個名為MYMATH
的DLL,這個DLL在位置編號3處包含了一個函數。那個函數獲取兩個整數作為參數,並返回兩個整數的和。
public class ByOrdinal {
public static void main(String args[])
throws UnsatisfiedLinkError {
int j=3, k=9;
System.out.println("Result of DLL function:"
+ Add(j,k));
}
/** @dll.import("MYMATH", entrypoint = "#3") */
private static native int Add(int op1,int op2);
}
可以看出,唯一的差異就是entrypoint參
數的形式。
@dll.import
引導命令可應用於整個類。也就是說,那個類的所有方法都是在相同的DLL裡實現的,並具有相同的鏈接屬性。引導命令不會由子類繼承;考慮到這個原因,而且由於DLL裡的函數是自然的static
函數,所以更佳的設計模式是將API函數封裝到一個獨立的類裡,如下所示:
/** @dll.import("USER32") */
class MyUser32Access {
public static native int
MessageBox(int hwndOwner, String text,
String title, int fuStyle);
public native static boolean
MessageBeep(int uType);
}
public class WholeClass {
public static void main(String args[])
throws UnsatisfiedLinkError {
MyUser32Access.MessageBeep(4);
MyUser32Access.MessageBox(0,
"Created by the MessageBox() Win32 func",
"Thinking in Java", 0);
}
}
由於MessageBeep()
和MessageBox()
函數已在不同的類裡被聲明成static
函數,所以必須在調用它們時規定作用域。大家也許認為必須用上述的方法將所有Win32 API(函數、常數和數據類型)都映射成Java類。但幸運的是,根本不必這樣做。
Win32 API的體積相當龐大——包含了數以千計的函數、常數以及數據類型。當然,我們並不想將每個Win32 API函數都寫成對應Java形式。微軟考慮到了這個問題,發行了一個Java包,可通過J/Direct將Win32 API映射成Java類。這個包的名字叫作com.ms.win32
。安裝Java SDK 2.0時,若在安裝選項中進行了相應的設置,這個包就會安裝到我們的類路徑中。這個包由大量Java類構成,它們完整再現了Win32 API的常數、數據類型以及函數。包容能力最大的三個類是User32.class
,Kernel.class
以及Gdi32.class
。它們包含的是Win32 API的核心內容。為使用它們,只需在自己的Java代碼裡導入即可。前面的ShowMsgBox
示例可用com.ms.win32
改寫成下面這個樣子(這裡也考慮到了用更恰當的方式使用UnsatisfiedLinkError
):
import com.ms.win32.*;
public class UseWin32Package {
public static void main(String args[]) {
try {
User32.MessageBeep(
winm.MB_ICONEXCLAMATION);
User32.MessageBox(0,
"Created by the MessageBox() Win32 func",
"Thinking in Java",
winm.MB_OKCANCEL |
winm.MB_ICONEXCLAMATION);
} catch(UnsatisfiedLinkError e) {
System.out.println("Can’t link Win32 API");
System.out.println(e);
}
}
}
Java包是在第一行導入的。現在,可在不進行其他聲明的前提下調用MessageBeep()
和MessageBox()
函數。在MessageBeep()
裡,我們可看到包導入時也聲明瞭Win32常數。這些常數是在大量Java接口裡定義的,全部命名為winx
(x
代表欲使用之常數的首字母)。
寫作本書時,com.ms.win32
包的開發仍未正式完成,但已可堪使用。
“彙集”(Marshaling)是指將一個函數參數從它原始的二進制形式轉換成與語言無關的某種形式,再將這種通用形式轉換成適合調用函數採用的二進制格式。在前面的例子中,我們調用了MessageBox()
函數,並向它傳遞了兩個字符串。MessageBox()
是個C函數,而且Java字符串的二進制佈局與C字符串並不相同。但儘管如此,參數仍獲得了正確的傳遞。這是由於在調用C代碼前,J/Direct已幫我們考慮到了將Java字符串轉換成C字符串的問題。這種情況適合所有標準的Java類型。下面這張表格總結了簡單數據類型的默認對應關係:
Java | C |
---|---|
byte |
BYTE 或CHAR |
short |
SHORT 或WORD |
int |
INT ,UINT ,LONG ,ULONG 或DWORD |
char |
TCHAR |
long |
__int64 |
float |
Float |
double |
Double |
boolean |
BOOL |
String |
LPCTSTR (只允許在OLE模式中作為返回值) |
byte[] |
BYTE * |
short[] |
WORD * |
char[] |
TCHAR * |
int[] |
DWORD * |
這個列表還可繼續下去,但已很能說明問題了。大多數情況下,我們不必關心與簡單數據類型之間的轉換問題。但一旦必須傳遞用戶自定義類型的參數,情況就立即變得不同了。例如,可能需要傳遞一個結構化的、用戶自定義的數據類型,或者需要把一個指針傳給原始內存區域。在這些情況下,有一些特殊的編譯引導命令標記一個Java類,使其能作為一個指針傳給結構(@dll.struct
引導命令)。欲知使用這些關鍵字的細節,請參考產品文檔。
有些Win32 API函數要求將一個函數指針作為自己的參數使用。Windows API函數隨後就可以調用參數函數(通常是在以後發生特定的事件時)。這一技術就叫作“回調函數”。回調函數的例子包括窗口進程以及我們在打印過程中設置的回調(為後臺打印程序提供回調函數的地址,使其能更新狀態,並在必要的時候中止打印)。
另一個例子是API函數EnumWindows()
,它能枚舉目前系統內所有頂級窗口。EnumWindows()
要求獲取一個函數指針作為自己的參數,然後搜索由Windows內部維護的一個列表。對於列表內的每個窗口,它都會調用回調函數,將窗口引用作為一個參數傳給回調。
為了在Java裡達到同樣的目的,必須使用com.ms.dll
包裡的Callback
類。我們從Callback
裡繼承,並取消callback()
。這個方法只能接近int
參數,並會返回int
或void
。方法簽名和具體的實現取決於使用這個回調的Windows API函數。
現在,我們要進行的全部工作就是創建這個Callback
派生類的一個實例,並將其作為函數指針傳遞給API函數。隨後,J/Direct會幫助我們自動完成剩餘的工作。
下面這個例子調用了Win32 API函數EnumWindows()
;EnumWindowsProc
類裡的callback()
方法會獲取每個頂級窗口的引用,獲取標題文字,並將其打印到控制檯窗口。
import com.ms.dll.*;
import com.ms.win32.*;
class EnumWindowsProc extends Callback {
public boolean callback(int hwnd, int lparam) {
StringBuffer text = new StringBuffer(50);
User32.GetWindowText(
hwnd, text, text.capacity()+1);
if(text.length() != 0)
System.out.println(text);
return true; // to continue enumeration.
}
}
public class ShowCallback {
public static void main(String args[])
throws InterruptedException {
boolean ok = User32.EnumWindows(
new EnumWindowsProc(), 0);
if(!ok)
System.err.println("EnumWindows failed.");
Thread.currentThread().sleep(3000);
}
}
對sleep()
的調用允許窗口進程在main()
退出前完成。
通過@dll.import
引導命令內的修改符(標記),還可用到J/Direct的另兩項特性。第一項是對OLE函數的簡化訪問,第二項是選擇API函數的ANSI及Unicode版本。
根據約定,所有OLE函數都會返回類型為HRESULT
的一個值,它是由COM定義的一個結構化整數值。若在COM那一級編寫程序,並希望從一個OLE函數裡返回某些不同的東西,就必須將一個特殊的指針傳遞給它——該指針指向函數即將在其中填充數據的那個內存區域。但在Java中,我們沒有指針可用;此外,這種方法並不簡練。利用J/Direct,我們可在@dll.import
引導命令裡使用ole
修改符,從而方便地調用OLE函數。標記為ole
函數的一個固有方法會從Java形式的方法簽名(通過它決定返回類型)自動轉換成COM形式的函數。
第二項特性是選擇ANSI或者Unicode字符串控制方法。對字符串進行控制的大多數Win32 API函數都提供了兩個版本。例如,假設我們觀察由USER32.DLL
導出的符號,那麼不會找到一個MessageBox()
函數,相反會看到MessageBoxA()
和MessageBoxW()
函數——分別是該函數的ANSI和Unicode版本。如果在@dll.import
引導命令裡不規定想調用哪個版本,JVM就會試著自行判斷。但這一操作會在程序執行時花費較長的時間。所以,我們一般可用ansi
,unicode
或auto
修改符硬性規定。
欲瞭解這些特性更詳細的情況,請參考微軟公司提供的技術文檔。
同J/Direct相比,RNI是一種比非Java代碼複雜得多的接口;但它的功能也十分強大。RNI比J/Direct更接近於JVM,這也使我們能寫出更有效的代碼,能處理固有方法中的Java對象,而且能實現與JVM內部運行機制更緊密的集成。
RNI在概念上類似Sun公司的JNI。考慮到這個原因,而且由於該產品尚未正式完工,所以我只在這裡指出它們之間的主要差異。欲瞭解更詳細的情況,請參考微軟公司的文檔。
JNI和RNI之間存在幾方面引人注目的差異。下面列出的是由msjavah
生成的C頭文件(微軟提供的msjavah
在功能上相當於Sun的javah
),應用於前面在JNI例子裡使用的Java類文件ShowMsgBox
。
/* DO NOT EDIT -
automatically generated by msjavah */
#include <native.h>
#pragma warning(disable:4510)
#pragma warning(disable:4512)
#pragma warning(disable:4610)
struct Classjava_lang_String;
#define Hjava_lang_String Classjava_lang_String
/* Header for class ShowMsgBox */
#ifndef _Included_ShowMsgBox
#define _Included_ShowMsgBox
#define HShowMsgBox ClassShowMsgBox
typedef struct ClassShowMsgBox {
#include <pshpack4.h>
long MSReserved;
#include <poppack.h>
} ClassShowMsgBox;
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void __cdecl
ShowMsgBox_ShowMessage (struct HShowMsgBox *,
struct Hjava_lang_String *);
#ifdef __cplusplus
}
#endif
#endif /* _Included_ShowMsgBox */
#pragma warning(default:4510)
#pragma warning(default:4512)
#pragma warning(default:4610)
除可讀性較差外,代碼裡還隱藏著一些技術性問題,待我一一道來。
在RNI中,固有方法的程序員知道對象的二進制佈局。這樣便允許我們直接訪問自己希望的信息;我們不必象在JNI裡那樣獲得一個字段或方法標識符。但由於並非所有虛擬機都需要將相同的二進制佈局應用於自己的對象,所以上面的固有方法只能在Microsoft JVM下運行。
在JNI中,通過JNIEnv參數可訪問大量函數,以便同JVM打交道。在RNI中,用於控制JVM運作的函數變成了可直接調用。它們中的某一些(如控制異常的那一個)類似於它們的JNI“兄弟”。但大多數RNI函數都有與JNI中不同的名字和用途。
JNI和RNI最重大的一個區別是“垃圾收集”的模型。在JNI中,垃圾收集在固有方法執行期間遵守與Java代碼執行時相同的規則。而在RNI中,要由程序員在固有方法活動期間自行負責“垃圾收集器”器的啟動與中止。默認情況下,垃圾收集器在進入固有方法前處於不活動狀態;這樣一來,程序員就可假定準備使用的對象用不著在那個時間段內進行垃圾收集。然而一旦固有方法準備長時間執行,程序員就應考慮激活垃圾收集器——通過調用GCEnable()
這個RNI函數(GC是“Garbage Collector”的縮寫,即“垃圾收集”)。
也存在與全局引用特性類似的機制——程序員可利用可保證特定的對象在GC活動期間不至於被當作“垃圾”收掉。概念是類似的,但名稱有所差異——在RNI中,人們把它叫作GCFrames
。
RNI與Microsoft JVM緊密集成這一事實既是它的優點,也是它的缺點。RNI比JNI複雜得多,但它也為我們提供了對JVM內部活動的高度控制;其中包括垃圾收集。此外,它顯然針對速度進行了優化,採納了C程序員熟悉的一些折衷方案和技術。但除了微軟的JVM之外,它並不適於其他JVM。
COM(以前稱為OLE)代表微軟公司的“組件對象模型”(Component Object Model),它是所有ActiveX技術(包括ActiveX控件、Automation以及ActiveX文檔)的基礎。但COM還包含了更多的東西。它是一種特殊的規範,按照它開發出來的組件對象可通過操作系統的專門特性實現“相互操作”。在實際應用中,為Win32系統開發的所有新軟件都與COM有著一定的關係——操作系統通過COM對象揭示出自己的一些特性。由其他廠商開發的組件也可以建立在COM的基礎上,我們能創建和註冊自己的COM組件。通過這樣或那樣的形式,如果我們想編寫Win32代碼,那麼必須和COM打交道。在這裡,我們將僅僅重述COM編程的基本概念,而且假定讀者已掌握了COM服務器(能為COM客戶提供服務的任何COM對象)以及COM客戶(能從COM服務器那裡申請服務的一個COM對象)的概念。本節將盡可能地使敘述變得簡單。工具實際的功能要強大得多,而且我們可通過更高級的途徑來使用它們。但這也要求對COM有著更深刻的認識,那已經超出了本附錄的範圍。如果您對這個功能強大、但與不同平臺有關的特性感興趣,應該研究COM和微軟公司的文檔資料,仔細閱讀有關Java/COM集成的那部分內容。如果想獲得更多的資料,向您推薦Dale Rogerson編著的《Inside COM》,該書由Microsoft Press於1997年出版。
由於COM是所有新型Win32應用程序的結構核心,所以通過Java代碼使用(或揭示)COM服務的能力就顯得尤為重要。Java/COM集成無疑是Microsoft Java編譯器以及虛擬機最有趣的特性。Java和COM在它們的模型上是如此相似,所以這個集成在概念上是相當直觀的,而且在技術上也能輕鬆實現無縫結合——為訪問COM,幾乎不需要編寫任何特殊的代碼。大多數技術細節都是由編譯器和/或虛擬機控制的。最終的結果便是Java程序員可象對待原始Java對象那樣對待COM對象。而且COM客戶可象使用其他COM服務器那樣使用由Java實現的COM服務器。在這裡提醒大家,儘管我使用的是通用術語“COM”,但根據擴展,完全可用Java實現一個ActiveX Automation服務器,亦可在Java程序中使用一個ActiveX控件。
Java和COM最引人注目的相似之處就是COM接口與Java的interface
關鍵字的關係。這是接近完美的一種相符,因為:
-
COM對象揭示出了接口(也只有接口)
-
COM接口本身並不具備實現方案;要由揭示出接口的那個COM對象負責它的實現
-
COM接口是對語義上相關的一組函數的說明;不會揭示出任何數據
-
COM類將COM接口組合到了一起。Java類可實現任意數量的Java接口。
-
COM有一個引用對象模型;程序員永遠不可能“擁有”一個對象,只能獲得對對象一個或多個接口的引用。Java也有一個引用對象模型——對一個對象的引用可“轉換”成對它的某個接口的引用。
-
COM對象在內存裡的“生存時間”取決於使用對象的客戶數量;若這個數量變成零,對象就會將自己從內存中刪去。在Java中,一個對象的生存時間也由客戶的數量決定。若不再有對那個對象的引用,對象就會等候垃圾收集器的處理。
Java與COM之間這種緊密的對應關係不僅使Java程序員可以方便地訪問COM特性,也使Java成為編寫COM代碼的一種有效語言。COM是與語言無關的,但COM開發事實上採用的語言是C++和Visual Basic。同Java相比,C++在進行COM開發時顯得更加強大,並可生成更有效的代碼,只是它很難使用。Visual Basic比Java簡單得多,但它距離基礎操作系統太遠了,而且它的對象模型並未實現與
COM很好的對應(映射)關係。Java是兩者之間一種很好的折衷方案。 接下來,讓我們對COM開發的一些關鍵問題進行討論。編寫Java/COM客戶和服務器時,這些問題是首先需要弄清楚的。
COM是一種二進制規範,致力於實現可相互操作的對象。例如,COM認為一個對象的二進制佈局必須能夠調用另一個COM對象裡的服務。由於是對二進制佈局的一種描述,所以只要某種語言能生成這樣的一種佈局,就可通過它實現COM對象。通常,程序員不必關注象這樣的一些低級細節,因為編譯器可自動生成正確的佈局。例如,假設您的程序是用C++寫的,那麼大多數編譯器都能生成符合COM規範的一張虛擬函數表格。對那些不生成可執行代碼的語言,比如VB和Java,在運行期則會自動掛接到COM。
COM庫也提供了幾個基本的函數,比如用於創建對象或查找系統中一個已註冊COM類的函數。
一個組件對象模型的基本目標包括:
-
讓對象調用其他對象裡的服務
-
允許新類型對象(或更新對象)無縫插入環境
第一點正是面向對象程序設計要解決的問題:我們有一個客戶對象,它能向一個服務器對象發出請求。在這種情況下,“客戶”和“服務器”這兩個術語是在常規意義上使用的,並非指一些特定的硬件配置。對於任何面向對象的語言,第一個目標都是很容易達到的——只要您的代碼是一個完整的代碼塊,同時實現了服務器對象代碼以及客戶對象代碼。若改變了客戶和服務器對象相互間的溝通形式,只需簡單地重新編譯和鏈接一遍即可。重新啟動應用程序時,它就會自動採用組件的最新版本。
但假若應用程序由一些未在自己控制之下的組件對象構成,情況就會變得迥然有異——我們不能控制它們的源碼,而且它們的更新可能完全獨立於我們的應用程序進行。例如,當我們在自己的程序裡使用由其他廠商開發的ActiveX控件時,就會面臨這一情況。控件會安裝到我們的系統裡,我們的程序能夠(在運行期)定位服務器代碼,激活對象,同它建立鏈接,然後使用它。以後,我們可安裝控件的新版本,我們的應用程序應該仍然能夠運行;即使在最糟的情況下,它也應禮貌地報告一條出錯消息,比如“控件未找到”等等;一般不會莫名其妙地掛起或死機。
在這些情況下,我們的組件是在獨立的可執行代碼文件裡實現的:DLL或EXE。若服務器對象在一個獨立的可執行代碼文件裡實現,就需要由操作系統提供的一個標準方法,從而激活這些對象。當然,我們並不想在自己的代碼裡使用DLL或EXE的物理名稱及位置,因為這些參數可能經常發生變化。此時,我們想使用的是由操作系統維護的一些標識符。另外,我們的應用程序需要對服務器展示出來的服務進行的一個描述。下面這兩個小節將分別討論這兩個問題。
COM採用結構化的整數值(長度為128位)唯一性地標識系統中註冊的COM項目。這些數字的正式名稱叫作GUID(Globally Unique IDentifier,全局唯一標識符),可由特殊的工具生成。此外,這些數字可以保證在“任何空間和時間”裡獨一無二,沒有重複。在空間,是由於數字生成器會讀取網卡的ID號碼;在時間,是由於同時會用到系統的日期和時間。可用GUID標識COM類(此時叫作CLSID)或者COM接口(IID)。儘管名字不同,但基本概念與二進制結構都是相同的。GUID亦可在其他環境中使用,這裡不再贅述。
GUID以及相關的信息都保存在Windows註冊表中,或者說保存在“註冊數據庫”(Registration Database)中。這是一種分級式的數據庫,內建於操作系統中,容納了與系統軟硬件配置有關的大量信息。對於COM,註冊表會跟蹤系統內安裝的組件,比如它們的CLSID、實現它們的可執行文件的名字及位置以及其他大量細節。其中一個比較重要的細節是組件的ProgID;ProgID在概念上類似於GUID,因為它們都標識著一個COM組件。區別在於GUID是一個二進制的、通過算法生成的值。而ProgID則是由程序員定義的字符串值。ProgID是隨同一個CLSID分配的。
我們說一個COM組件已在系統內註冊,最起碼的一個條件就是它的CLSID和它的執行文件已存在於註冊表中(ProgID通常也已就位)。在後面的例子裡,我們主要任務就是註冊與使用COM組件。
註冊表的一項重要特點就是它作為客戶和服務器對象之間的一個去耦層使用。利用註冊表內保存的一些信息,客戶會激活服務器;其中一項信息是服務器執行模塊的物理位置。若這個位置發生了變動,註冊表內的信息就會相應地更新。但這個更新過程對於客戶來說是“透明”或者看不見的。後者只需直接使用ProgID或CLSID即可。換句話說,註冊表使服務器代碼的位置透明成為了可能。隨著DCOM(分佈式COM)的引入,在本地機器上運行的一個服務器甚至可移到網絡中的一臺遠程機器,整個過程甚至不會引起客戶對它的絲毫注意(大多數情況下如此)。
由於COM具有動態鏈接的能力,同時由於客戶和服務器代碼可以分開獨立發展,所以客戶隨時都要動態偵測由服務器展示出來的服務。這些服務是用“類型庫”(Type Library)中一種二進制的、與語言無關的形式描述的(就象接口和方法簽名)。它既可以是一個獨立的文件(通常採用.TLB擴展名),也可以是鏈接到執行程序內部的一種Win32資源。運行期間,客戶會利用類型庫的信息調用服務器中的函數。
我們可以寫一個Microsoft Interface Definition Language(微軟接口定義語言,MIDL)源文件,用MIDL編譯器編譯它,從而生成一個.TLB
文件。MIDL語言的作用是對COM類、接口以及方法進行描述。它在名稱、語法以及用途上都類似OMB/CORBA IDL。然而,Java程序員不必使用MIDL。後面還會講到另一種不同的Microsoft工具,它能讀入Java類文件,並能生成一個類型庫。
由服務器展示出來的COM函數會返回一個值,採用預先定義好的HRESULT
類型。HRESULT
代表一個包含了三個字段的整數。這樣便可使用多個失敗和成功代碼,同時還可以使用其他信息。由於COM函數返回的是一個HRESULT
,所以不能用返回值從函數調用裡取回原始數據。若必須返回數據,可傳遞指向一個內存區域的指針,函數將在那個區域裡填充數據。我們把這稱為“外部參數”。作為Java/COM程序員,我們不必過於關注這個問題,因為虛擬機會幫助我們自動照管一切。這個問題將在後續的小節裡講述。
同C++/COM程序員相比,Microsoft Java編譯器、虛擬機以及各式各樣的工具極大簡化了Java/COM程序員的工作。編譯器有特殊的引導命令和包,可將Java類當作COM類對待。但在大多數情況下,我們只需依賴Microsoft JVM為COM提供的支持,同時利用兩個有力的外部工具。
Microsoft Java Virtual Machine(JVM)在COM和Java對象之間扮演了一座橋樑的角色。若將Java對象創建成一個COM服務器,那麼我們的對象仍然會在JVM內部運行。Microsoft JVM是作為一個DLL實現的,它向操作系統展示出了COM接口。在內部,JVM將對這些COM接口的函數調用映射成Java對象中的方法調用。當然,JVM必須知道哪個Java類文件對應於服務器執行模塊;之所以能夠找出這方面的信息,是由於我們事前已用Javareg
在Windows註冊表內註冊了類文件。Javareg
是與Microsoft Java SDK配套提供的一個工具程序,能讀入一個Java類文件,生成相應的類型庫以及一個GUID,並可將類註冊到系統內。亦可用Javareg
註冊遠程服務器。例如,可用它註冊在不同機器上運行的一個服務器。
如果想寫一個Java/COM客戶,必須經歷一系列不同的步驟。Java/COM“客戶”是一些特殊的Java代碼,它們想激活和使用系統內註冊的一個COM服務器。同樣地,虛擬機會與COM服務器溝通,並將它提供的服務作為Java類內的各種方法展示(揭示)出來。另一個Microsoft工具是jactivex
,它能讀取一個類型庫,並生成相應的Java源文件,在其中包含特殊的編譯器引導命令。生成的源文件屬於我們在指定類型庫之後命名的一個包的一部分。下一步是在自己的COM客戶Java源文件中導入那個包。
接下來讓我們討論兩個例子。
本節將介紹ActiveX控件、Automation服務器或者其他任何符合COM規範的服務器的開發過程。下面這個例子實現了一個簡單的Automation服務器,它能執行整數加法。我們用setAddend()
方法設置addend
的值。每次調用sum()
方法的時候,addend
就會添加到當前result
裡。我們用getResult()
獲得result
值,並用clear()
重新設置值。用於實現這一行為的Java類是非常簡單的:
public class Adder {
private int addend;
private int result;
public void setAddend(int a) { addend = a; }
public int getAddend() { return addend; }
public int getResult() { return result; }
public void sum() { result += addend; }
public void clear() {
result = 0;
addend = 0;
}
}
為了將這個Java類作為一個COM對象使用,我們將Javareg
工具應用於編譯好的Adder.class
文件。這個工具提供了一系列選項;在這種情況下,我們指定Java類文件名("Adder"
),想為這個服務器在註冊表裡置入的ProgID("JavaAdder.Adder.1"
),以及想為即將生成的類型庫指定的名字("JavaAdder.tlb"
)。由於尚未給出CLSID,所以Javareg
會自動生成一個。若我們再次對同樣的服務器調用Javareg
,就會直接使用現成的CLSID。
javareg /register
/class:Adder /progid:JavaAdder.Adder.1
/typelib:JavaAdder.tlb
Javareg也會將新服務器註冊到Windows註冊表。此時,我們必須記住將Adder.class
複製到Windows\Java\trustlib
目錄。考慮到安全方面的原因(特別是涉及程序片調用COM服務的問題),只有在COM服務器已安裝到trustlib
目錄的前提下,這些服務器才會被激活。
現在,我們已在自己的系統中安裝了一個新的Automation服務器。為進行測試,我們需要一個Automation控制器,而Automation控制器就是Visual Basic(VB)。在下面,大家會看到幾行VB代碼。按照VB的格式,我設置了一個文本框,用它從用戶那裡接收要相加的值。並用一個標籤顯示結果,用兩個下推按鈕分別調用sum()
和clear()
方法。最開始,我們聲明瞭一個名為Adder
的對象變量。在Form_Load
子例程中(在窗體首次顯示時載入),會調用Adder自動服務器的一個新實例,並對窗體的文本字段進行初始化。一旦用戶按下Sum
或者Clear
按鈕,就會調用服務器中對應的方法。
Dim Adder As Object
Private Sub Form_Load()
Set Adder = CreateObject("JavaAdder.Adder.1")
Addend.Text = Adder.getAddend
Result.Caption = Adder.getResult
End Sub
Private Sub SumBtn_Click()
Adder.setAddend (Addend.Text)
Adder.Sum
Result.Caption = Adder.getResult
End Sub
Private Sub ClearBtn_Click()
Adder.Clear
Addend.Text = Adder.getAddend
Result.Caption = Adder.getResult
End Sub
注意,這段代碼根本不知道服務器是用Java實現的。
運行這個程序並調用了CreateObject()
函數以後,就會在Windows註冊表裡搜索指定的ProgID。在與ProgID有關的信息中,最重要的是Java類文件的名字。作為一個響應,會啟動Java虛擬機,而且在JVM內部調用Java對象的實例。從那個時候開始,JVM就會自動接管客戶和服務器代碼之間的交流。
現在,讓我們轉到另一側,並用Java開發一個COM客戶。這個程序會調用系統已安裝的COM服務器內的服務。就目前這個例子來說,我們使用的是在前一個例子裡為服務器實現的一個客戶。儘管代碼在Java程序員的眼中看起來比較熟悉,但在幕後發生的一切卻並不尋常。本例使用了用Java寫成的一個服務器,但它可應用於系統內安裝的任何ActiveX控件、ActiveX Automation服務器或者ActiveX組件——只要我們有一個類型庫。
首先,我們將Jactivex
工具應用於服務器的類型庫。Jactivex
有一系列選項和開關可供選擇。但它最基本的形式是讀取一個類型庫,並生成Java源文件。這個源文件保存於我們的windows/java/trustlib
目錄中。通過下面這行代碼,它應用於為外部COM Automation服務器生成的類型庫:
jactivex /javatlb JavaAdder.tlb
Jactivex
完成以後,我們再來看看自己的windows/java/trustlib
目錄。此時可在其中看到一個新的子目錄,名為javaadder
。這個目錄包含了用於新包的源文件。這是在Java裡與類型庫的功能差不多的一個庫。這些文件需要使用Microsoft編譯器的專用引導命令:@com
。jactivex
生成多個文件的原因是COM使用多個實體來描述一個COM服務器(另一個原因是我沒有對MIDL文件和Java/COM工具的使用進行細緻的調整)。
名為Adder.java
的文件等價於MIDL文件中的一個coclass
引導命令:它是對一個COM類的聲明。其他文件則是由服務器揭示出來的COM接口的Java等價物。這些接口(比如Adder_DispatchDefault.java
)都屬於“分發”(Dispatch)接口,屬於Automation控制器與Automation服務器之間的溝通機制的一部分。Java/COM集成特性也支持雙接口的實現與使用。但是,IDispatch
和雙接口的問題已超出了本附錄的範圍。
在下面,大家可看到對應的客戶代碼。第一行只是導入由jactivex
生成的包。然後創建並使用COM Automation服務器的一個實例,就象它是一個原始的Java類那樣。請注意行內的類型模型,其中“例示”了COM對象(即生成並調用它的一個實例)。這與COM對象模型是一致的。在COM中,程序員永遠不會得到對整個對象的一個引用。相反,他們只能擁有對類內實現的一個或多個接口的引用。
“例示”Adder
類的一個Java對象以後,就相當於指示COM激活服務器,並創建這個COM對象的一個實例。但我們隨後必須指定自己想使用哪個接口,在由服務器實現的接口中挑選一個。這正是類型模型完成的工作。這兒使用的是“默認遣送”接口,它是Automation控制器用於同一個Automation服務器通信的標準接口。欲瞭解這方面的細節,請參考由Ibid編著的《Inside COM》。請注意激活服務器並選擇一個COM接口是多麼容易!
import javaadder.*;
public class JavaClient {
public static void main(String [] args) {
Adder_DispatchDefault iAdder =
(Adder_DispatchDefault) new Adder();
iAdder.setAddend(3);
iAdder.sum();
iAdder.sum();
iAdder.sum();
System.out.println(iAdder.getResult());
}
}
現在,我們可以編譯它,並開始運行程序。
com.ms.com
包為COM的開發定義了數量眾多的類。它支持GUID的使用——Variant(變體)和SafeArray Automation(安全數組自動)類型——能與ActiveX控件在一個較深的層次打交道,並可控制COM異常。
由於篇幅有限,這裡不可能涉及所有這些主題。但我想著重強調一下COM異常的問題。根據規範,幾乎所有COM函數都會返回一個HRESULT
值,它告訴我們函數調用是否成功,以及失敗的原因。但若觀察服務器和客戶代碼中的Java方法簽名,就會發現沒有HRESULT
。相反,我們用函數返回值從一些函數那裡取回數據。“虛擬機”(VM)會將Java風格的函數調用轉換成COM風格的函數調用,甚至包括返回參數。但假若我們在服務器裡調用的一個函數在COM這一級失敗,又會在虛擬機裡出現什麼事情呢?在這種情況下,JVM會認為HRESULT
值標誌著一次失敗,並會產生類com.ms.com.ComFailException
的一個固有Java異常。這樣一來,我們就可用Java異常控制機制來管理COM錯誤,而不是檢查函數的返回值。
如欲深入瞭解這個包內包含的類,請參考微軟公司的產品文檔。
Java/COM集成一個有趣的結果就是ActiveX/Beans的集成。也就是說,Java Bean可包含到象VB或任何一種Microsoft Office產品那樣的ActiveX容器裡。而一個ActiveX控件可包含到象Sun BeanBox這樣的Beans容器裡。Microsoft JVM會幫助我們考慮到所有的細節。一個ActiveX控件僅僅是一個COM服務器,它展示了預先定義好的、請求的接口。Bean只是一個特殊的Java類,它遵循特定的編程風格。但在寫作本書的時候,這一集成仍然不能算作完美。例如,虛擬機不能將JavaBeans事件映射成為COM事件模型。若希望從ActiveX容器內部的一個Bean裡對事件加以控制,Bean必須通過低級技術攔截象鼠標行動這類的系統事件,不能採用標準的JavaBeans委託事件模型。
拋開這個問題不管,ActiveX/Beans集成仍然是非常有趣的。由於牽涉的概念與工具與上面討論的完全相同,所以請參閱您的Microsoft文檔,瞭解進一步的細節。
固有方法為我們帶來了安全問題的一些考慮。若您的Java代碼發出對一個固有方法的調用,就相當於將控制權傳遞到了虛擬機“體系”的外面。固有方法擁有對操作系統的完全訪問權限!當然,如果由自己編寫固有方法,這正是我們所希望的。但這對程序片來說卻是不可接受的——至少不能默許這樣做。我們不想看到從因特網遠程服務器下載回來的一個程序片自由自在地操作文件系統以及機器的其他敏感區域,除非特別允許它這樣做。為了用J/Direct,RNI和COM集成防止此類情況的發生,只有受到信任(委託)的Java代碼才有權發出對固有方法的調用。根據程序片的具體使用,必須滿足不同的條件才可放行。例如,使用J/Direct的一個程序片必須擁有數字化簽名,指出自己受到完全信任。在寫作本書的時候,並不是所有這些安全機制都已實現(對於Microsoft SDK for Java,beta 2版本)。所以當新版本出現以後,請務必留意它的文檔說明。
在大型的分佈式應用中,我們的某些要求並非前面講述的方法能夠滿足的。舉個例子來說,我們可能想同以前遺留下來的數據倉庫打交道,或者需要從一個服務器對象裡獲取服務,無論它的物理位置在哪裡。在這些情況下,都要求某種形式的“遠程過程調用”(RPC),而且可能要求與語言無關。此時,CORBA可為我們提供很大的幫助。
CORBA並非一種語言特性,而是一種集成技術。它代表著一種具體的規範,各個開發商通過遵守這一規範,可設計出符合CORBA標準的集成產品。CORBA規範是由OMG開發出來的。這家非贏利性的機構致力於定義一個標準框架,從而實現分佈式、與語言無關對象的相互操作。
利用CORBA,我們可實現對Java對象以及非Java對象的遠程調用,並可與傳統的系統進行溝通——採用一種“位置透明”的形式。Java增添了連網支持,是一種優秀的“面向對象”程序設計語言,可構建出圖形化和非圖形化的應用(程序)。Java和OMG對象模型存在著很好的對應關係;例如,無論Java還是CORBA都實現了“接口”的概念,並且都擁有一個引用(參考)對象模型。
由OMG制訂的對象相互操作規範通常稱為“對象管理體系”(ObjectManagement Architecture,OMA)。OMA定義了兩個組件:“核心對象模型”(Core Object Model)和“OMA參考體系”(OMA Reference Model)。OMA參考體系定義了一套基層服務結構及機制,實現了對象相互間進行操作的能力。OMA參考體系包括“對象請求代理”(Object Request Broker,ORB)、“對象服務”(Object Services,也稱作CORBAservices)以及一些通用機制。
ORB是對象間相互請求的一條通信總線。進行請求時,毋需關心對方的物理位置在哪裡。這意味著在客戶代碼中看起來象一次方案調用的過程實際是非常複雜的一次操作。首先,必須存在與服務器對象的一條連接途徑。而且為了創建一個連接,ORB必須知道具體實現服務器的代碼存放在哪裡。建好連接後,必須對方法參數進行“彙集”。例如,將它們轉換到一個二進制流裡,以便通過網絡傳送。必須傳遞的其他信息包括服務器的機器名稱、服務器進程以及對那個進程內的服務器對象進行標識的信息等等。最後,這些信息通過一種低級線路協議傳遞,信息在服務器那一端解碼,最後正式執行調用。ORB將所有這些複雜的操作都從程序員眼前隱藏起來了,並使程序員的工作幾乎和與調用本地對象的方法一樣簡單。
並沒有硬性規定應如何實現ORB核心,但為了在不同開發商的ORB之間實現一種基本的兼容,OMG定義了一系列服務,它們可通過標準接口訪問。
CORBA是面向語言的透明而設計的:一個客戶對象可調用屬於不同類的服務器對象方法,無論對方是用何種語言實現的。當然,客戶對象事先必須知道由服務器對象揭示的方法名稱及簽名。這時便要用到IDL。CORBA IDL是一種與語言無關的設計方法,可用它指定數據類型、屬性、操作、接口以及更多的東西。IDL的語法類似於C++或Java語法。下面這張表格為大家總結了三種語言一些通用概念,並展示了它們的對應關係。
CORBA IDL | Java | C++ |
---|---|---|
模塊(Module) | 包(Package) | 命名空間(Namespace) |
接口(Interface) | 接口(Interface) | 純抽象類(Pure abstract class) |
方法(Method) | 方法(Method) | 成員函數(Member function) |
繼承概念也獲得了支持——就象C++那樣,同樣使用冒號運算符。針對需要由服務器和客戶實現和使用的屬性、方法以及接口,程序員要寫出一個IDL描述。隨後,IDL會由一個由廠商提供的IDL/Java編譯器進行編譯,後者會讀取IDL源碼,並生成相應的Java代碼。
IDL編譯器是一個相當有用的工具:它不僅生成與IDL等價的Java源碼,也會生成用於彙集方法參數的代碼,並可發出遠程調用。我們將這種代碼稱為“根幹”(Stub and Skeleton)代碼,它組織成多個Java源文件,而且通常屬於同一個Java包的一部分。
命名服務屬於CORBA基本服務之一。CORBA對象是通過一個引用訪問的。儘管引用信息用我們的眼睛來看沒什麼意義,但可為引用分配由程序員定義的字符串名。這一操作叫作“引用的字符串化”。一個叫作“命名服務”(Naming Service)的OMA組件專門用於執行“字符串到對象”以及“對象到字符串”轉換及映射。由於命名服務扮演了服務器和客戶都能查詢和操作的一個電話本的角色,所以它作為一個獨立的進程運行。創建“對象到字符串”映射的過程叫作“綁定一個對象”;刪除映射關係的過程叫作“取消綁定”;而讓對象引用傳遞一個字符串的過程叫作“解析名稱”。
比如在啟動的時候,服務器應用可創建一個服務器對象,將對象同命名服務綁定起來,然後等候客戶發出請求。客戶首先獲得一個服務器引用,解析出字符串名,然後通過引用發出對服務器的調用。
同樣地,“命名服務”規範也屬於CORBA的一部分,但實現它的應用程序是由ORB廠商(開發商)提供的。由於廠商不同,我們訪問命名服務的方式也可能有所區別。
這兒顯示的代碼可能並不詳盡,因為不同的ORB有不同的方法來訪問CORBA服務,所以無論什麼例子都要取決於具體的廠商(下例使用了JavaIDL,這是Sun公司的一個免費產品。它配套提供了一個簡化版本的ORB、一個命名服務以及一個“IDL→Java”編譯器)。除此之外,由於Java仍處在發展初期,所以在不同的Java/CORBA產品裡並不是包含了所有CORBA特性。
我們希望實現一個服務器,令其在一些機器上運行,其他機器能向它查詢正確的時間。我們也希望實現一個客戶,令其請求正確的時間。在這種情況下,我們讓兩個程序都用Java實現。但在實際應用中,往往分別採用不同的語言。
第一步是為提供的服務編寫一個IDL描述。這通常是由服務器程序員完成的。隨後,程序員就可用任何語言實現服務器,只需那種語言裡存在著一個CORBA IDL編譯器。
IDL文件已分發給客戶端的程序員,併成為兩種語言間的橋樑。
下面這個例子展示了時間服務器的IDL描述情況:
module RemoteTime {
interface ExactTime {
string getTime();
};
};
這是對RemoteTime
命名空間內的ExactTime
接口的一個聲明。該接口由單獨一個方法構成,它以字符串格式返回當前時間。
第二步是編譯IDL,創建Java根幹代碼。我們將利用這些代碼實現客戶和服務器。與JavaIDL產品配套提供的工具是idltojava
:
idltojava -fserver -fclient RemoteTime.idl
其中兩個標記告訴idltojava
同時為根和幹生成代碼。idltojava
會生成一個Java包,它在IDL模塊、RemoteTime
以及生成的Java文件置入RemoteTime
子目錄後命名。_ExactTimeImplBase.java
代表我們用於實現服務器對象的“幹”;
而_ExactTimeStub.java
將用於客戶。在ExactTime.java
中,用Java方式表示了IDL接口。此外還包含了用到的其他支持文件,例如用於簡化訪問命名服務的文件。
大家在下面看到的是服務器端使用的代碼。服務器對象是在ExactTimeServer
類裡實現的。RemoteTimeServer
這個應用的作用是:創建一個服務器對象,通過ORB為其註冊,指定對象引用時採用的名稱,然後“安靜”地等候客戶發出請求。
import RemoteTime.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;
import java.util.*;
import java.text.*;
// Server object implementation
class ExactTimeServer extends _ExactTimeImplBase{
public String getTime(){
return DateFormat.
getTimeInstance(DateFormat.FULL).
format(new Date(
System.currentTimeMillis()));
}
}
// Remote application implementation
public class RemoteTimeServer {
public static void main(String args[]) {
try {
// ORB creation and initialization:
ORB orb = ORB.init(args, null);
// Create the server object and register it:
ExactTimeServer timeServerObjRef =
new ExactTimeServer();
orb.connect(timeServerObjRef);
// Get the root naming context:
org.omg.CORBA.Object objRef =
orb.resolve_initial_references(
"NameService");
NamingContext ncRef =
NamingContextHelper.narrow(objRef);
// Assign a string name to the
// object reference (binding):
NameComponent nc =
new NameComponent("ExactTime", "");
NameComponent path[] = {nc};
ncRef.rebind(path, timeServerObjRef);
// Wait for client requests:
java.lang.Object sync =
new java.lang.Object();
synchronized(sync){
sync.wait();
}
}
catch (Exception e) {
System.out.println(
"Remote Time server error: " + e);
e.printStackTrace(System.out);
}
}
}
正如大家看到的那樣,服務器對象的實現是非常簡單的;它是一個普通的Java類,從IDL編譯器生成的“幹”代碼中繼承而來。但在與ORB以及其他CORBA服務進行聯繫的時候,情況卻變得稍微有些複雜。
這裡要簡單介紹一下JavaIDL相關代碼所做的工作(注意暫時忽略了CORBA代碼與不同廠商有關這一事實)。main()
的第一行代碼用於啟動ORB。而且理所當然,這正是服務器對象需要同它進行溝通的原因。就在ORB初始化以後,緊接著就創建了一個服務器對象。實際上,它正式名稱應該是“短期服務對象”:從客戶那裡接收請求,“生存時間”與創建它的進程是相同的。創建好短期服務對象後,就會通過ORB對其進行註冊。這意味著ORB已知道它的存在,可將請求轉發給它。
到目前為止,我們擁有的全部東西就是一個timeServerObjRef
——只有在當前服務器進程裡才有效的一個對象引用。下一步是為這個服務對象分配一個字符串形式的名字。客戶會根據那個名字尋找服務對象。我們通過命名服務(Naming Service)完成這一操作。首先,我們需要對命名服務的一個對象引用。通過調用resolve_initial_references()
,可獲得對命名服務的字符串式對象引用(在JavaIDL中是NameService
),並將這個引用返回。這是對採用narrow()方
法的一個特定NamingContext
引用的模型。我們現在可開始使用命名服務了。
為了將服務對象同一個字符串形式的對象引用綁定在一起,我們首先創建一個NameComponent
對象,用ExactTime
進行初始化。ExactTime
是我們想用於綁定服務對象的名稱字符串。隨後使用rebind()
方法,這是受限於對象引用的字符串化引用。我們用rebind()
分配一個引用——即使它已經存在。而假若引用已經存在,那麼bind()
會造成一個異常。在CORBA中,名稱由一系列NameContext
構成——這便是我們為什麼要用一個數組將名稱與對象引用綁定起來的原因。
服務對象最好準備好由客戶使用。此時,服務器進程會進入一種等候狀態。同樣地,由於它是一種“短期服務”,所以生存時間要受服務器進程的限制。JavaIDL目前尚未提供對“持久對象”(只要創建它們的進程保持運行狀態,對象就會一直存在下去)的支持。 現在,我們已對服務器代碼的工作有了一定的認識。接下來看看客戶代碼:
import RemoteTime.*;
import org.omg.CosNaming.*;
import org.omg.CORBA.*;
public class RemoteTimeClient {
public static void main(String args[]) {
try {
// ORB creation and initialization:
ORB orb = ORB.init(args, null);
// Get the root naming context:
org.omg.CORBA.Object objRef =
orb.resolve_initial_references(
"NameService");
NamingContext ncRef =
NamingContextHelper.narrow(objRef);
// Get (resolve) the stringified object
// reference for the time server:
NameComponent nc =
new NameComponent("ExactTime", "");
NameComponent path[] = {nc};
ExactTime timeObjRef =
ExactTimeHelper.narrow(
ncRef.resolve(path));
// Make requests to the server object:
String exactTime = timeObjRef.getTime();
System.out.println(exactTime);
} catch (Exception e) {
System.out.println(
"Remote Time server error: " + e);
e.printStackTrace(System.out);
}
}
}
前幾行所做的工作與它們在服務器進程裡是一樣的:ORB獲得初始化,並解析出對命名服務的一個引用。
接下來,我們需要用到服務對象的一個對象引用,所以將字符串形式的對象引用直接傳遞給resolve()
方法,並用narrow()
方法將結果轉換到ExactTime
接口引用裡。最後調用getTime()
。
現在,我們已分別獲得了一個服務器和一個客戶應用,它們已作好相互間進行溝通的準備。大家知道兩者都需要利用命名服務綁定和解析字符串形式的對象引用。在運行服務或者客戶之前,我們必須啟動命名服務進程。在JavaIDL中,命名服務屬於一個Java應用,是隨產品配套提供的。但它可能與其他產品有所不同。JavaIDL命名服務在JVM的一個實例裡運行,並(默認)監視網絡端口900。
現在,我們已準備好啟動服務器和客戶應用(之所以按這一順序,是由於服務器的存在是“短期”的)。若各個方面都設置無誤,那麼獲得的就是在客戶控制檯窗口內的一行輸出文字,提醒我們當前的時間是多少。當然,這一結果本身並沒有什麼令人興奮的。但應注意一個問題:即使都處在同一臺機器上,客戶和服務器應用仍然運行於不同的虛擬機內。它們之間的通信是通過一個基本的集成層進行的——即ORB與命名服務的集成。
這只是一個簡單的例子,面向非網絡環境設計。但通常將ORB配置成“與位置無關”。若服務器與客戶分別位於不同的機器上,那麼ORB可用一個名為“安裝庫”(Implementation Repository)的組件解析出遠程字符串式引用。儘管“安裝庫”屬於CORBA的一部分,但它幾乎沒有具體的規格,所以各廠商的實現方式是不盡相同的。
正如大家看到的那樣,CORBA還有許多方面的問題未在這兒進行詳細講述。但通過以上的介紹,應已對其有一個基本的認識。若想獲得CORBA更詳細的資料,最傳真的起點莫過於OMB Web站點,地址是 http://www.omg.org
。這個地方提供了豐富的文檔資料、白頁、程序以及對其他CORBA資源和產品的鏈接。
Java程序片可扮演一名CORBA客戶的角色。這樣一來,程序片就可訪問由CORBA對象揭示的遠程信息和服務。但程序片只能同最初下載它的那個服務器連接,所以程序片與它溝通的所有CORBA對象都必須位於那臺服務器上。這與CORBA的宗旨是相悖的:它許諾可以實現“位置的透明”,或者“與位置無關”。
將Java程序片作為CORBA客戶使用時,也會帶來一些安全方面的問題。如果您在內聯網中,一個辦法是放寬對瀏覽器的安全限制。或者設置一道防火牆,以便建立與外部服務器安全連接。
針對這一問題,有些Java ORB產品專門提供了自己的解決方案。例如,有些產品實現了一種名為“HTTP通道”(HTTP Tunneling)的技術,另一些則提供了特別的防火牆功能。
作為放到附錄中的內容,所有這些主題都顯得太複雜了。但它們確實是需要重點注意的問題。
我們已經知道,CORBA的一項主要特性就是對RPC(遠程過程調用)的支持。利用這一技術,我們的本地對象可調用位置遠程對象內的方法。當然,目前已有一項固有的Java特性可以做完全相同的事情:RMI(參考第15章)。儘管RMI使Java對象之間進行RPC調用成為可能,但CORBA能在用任何語言編制的對象之間進行RPC。這顯然是一項很大的區別。
然而,可通過RMI調用遠程、非Java代碼的服務。我們需要的全部東西就是位於服務器那一端的、某種形式的封裝Java對象,它將非Java代碼“包裹”於其中。封裝對象通過RMI同Java客戶建立外部連接,並於內部建立與非Java代碼的連接——採用前面講到的某種技術,如JNI或J/Direct。
使用這種方法時,要求我們編寫某種類型的“集成層”——這其實正是CORBA幫我們做的事情。但是這樣做以後,就不再需要其他廠商開發的ORB了。
我們在這個附錄討論的都是從一個Java應用裡調用非Java代碼最基本的技術。每種技術都有自己的優缺點。但目前最主要的問題是並非所有這些特性都能在所有JVM中找到。因此,即使一個Java程序能調用位於特定平臺上的固有方法,仍有可能不適用於安裝了不同JVM的另一種平臺。
Sun公司提供的JNI具有靈活、簡單(儘管它要求對JVM內核進行大量控制)、功能強大以及通用於大多數JVM的優點。到本書完稿時為止,微軟仍未提供對JNI的支持,而是提供了自己的J/Direct(調用Win32 DLL函數的一種簡便方法)和RNI(特別適合編寫高效率的代碼,但要求對JVM內核有很深入的理解)。微軟也提供了自己的專利Java/COM集成方案。這一方案具有很強大的功能,且將Java變成了編寫COM服務器和客戶的有效語言。只有微軟公司的編譯器和JVM能提供對J/Direct、RNI以及Java/COM的支持。
我們最後研究的是CORBA,它使我們的Java對象可與其他對象溝通——無論它們的物理位置在哪裡,也無論是用何種語言實現的。CORBA與前面提到的所有技術都不同,因為它並未集成到Java語言裡,而是採用了其他廠商(第三方)的集成技術,並要求我們購買其他廠商提供的ORB。CORBA是一種有趣和通用的方案,但如果只是想發出對操作系統的調用,它也許並非一種最佳方案。