主要內容:
________________________
一、Java程序的啟動過程
二、Windows平臺的啟動器
三、配置和使用
________________________
對于普通用戶來說,Java最讓人不習慣的是程序的啟動過程;即使對于富有經(jīng)驗的開發(fā)者,為了用默認的裝載器啟動Java程序,不得不編寫大量批命令、腳本文件,不得不在命令行環(huán)境下進行大量的復制/粘貼操作,也很容易出現(xiàn)誤操作。
用慣了Windows方便快捷的GUI,人們早就習慣了通過雙擊運行程序的方式。對于
Java程序,要實現(xiàn)這個本機啟動功能就必須編寫定制的啟動器。用定制啟動器啟動Java程序不僅方便了最終用戶,而且使軟件作品看起來更專業(yè)。本文就以
Windows平臺為例,介紹如何構造Java定制啟動器。
一、Java程序的啟動過程
和C/C++程序相比,Java程序的啟動過程要復雜得多,這主要是因為Java是一種編譯成中間語言(字節(jié)碼)后解釋執(zhí)行的語言。啟動和關閉Java程序需要多個步驟才能完成,如圖一所示。
圖一
Java程序可以由任何本機運行的程序調用執(zhí)行。所謂Java啟動器,就是一個專門用來啟動
Java程序的本機執(zhí)行程序。最常見的啟動器是Sun在Java Runtime
Environment的/bin目錄中提供的啟動器,就Windows平臺而言,它們是java.exe和javaw.exe。前者運行時打開兩個窗
口:一個是接收System.out/err和啟動器輸出的控制臺窗口,另一個是Java程序本身的窗口;javaw運行時不打開控制臺窗口。在J2SE
/EE平臺中,虛擬機以動態(tài)庫的形式實現(xiàn),也放在/bin目錄下。動態(tài)庫的名字在Windows中是java.dll,在Unix中是java.so。所
謂“裝入虛擬機”,就是指裝入這個動態(tài)庫。
提供給VM的參數(shù)可以通過兩種方式指定,或者是在啟動器的命令行參數(shù)中指定,或者通過
定義相應的環(huán)境變量指定。只有一個參數(shù)例外——要啟動的類的名稱只能在啟動器的命令行參數(shù)中指定。雖然指定方式的多樣性為人們各取所需帶來了方便,但不可
否認地,它也正是許多混亂的根源。使用定制啟動器能夠完全避免這方面的問題。
當VM結束啟動類的main()方法的運行,啟動器調用destroy()方法釋放各
種資源并退出。應當注意的是,VM一旦開始運行,我們就不能再卸載它。對于Java啟動器來說,能否關閉VM無關緊要,因為啟動器會隨著Java程序的退
出而退出;然而,對于嵌入了VM的本機應用,例如瀏覽器,這意味著有一塊內存被永久性地占用,不能再收回。
二、Windows平臺的啟動器
搞清楚了Java程序的啟動過程,我們就可以開始編寫啟動器的代碼。下面這個啟動器用C++寫成,適合于所有Windows平臺。
// Windows平臺下的Java程序啟動器
// 適用于1.2或更高版本的VM
#include <windows.h>
#include <jni.h>
#include <string>
using namespace std;
void vShowError(string sErrorMessage);
void vShowLastError(string sErrorMessage);
void vDestroyVM(JNIEnv *env, JavaVM *jvm);
void vAddOption(string& sName);
JavaVMOption* vm_options;
int mctOptions = 0;
int mctOptionCapacity = 0;
boolean GetApplicationHome(char *buf, jint sz);
typedef jint (CALLBACK *CreateJavaVM)(JavaVM **pvm, JNIEnv **penv, void *args);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow){
JNIEnv *env;
JavaVM *jvm;
jint jintVMStartupReturnValue;
jclass jclassStartup;
jmethodID midStartup;
// 確定各種文件所在的路徑
// -應用的主目錄
char home[2000];
if (!GetApplicationHome(home, sizeof(home))) {
vShowError("不能確定應用的主目錄。");
return 0;
}
string sAppHome(home);
string sOption_AppHome = "-Dapplication.home=" + sAppHome;
string sJREPath = sAppHome + "\\jre";
// -VM路徑
string sRuntimePath = sJREPath + "\\bin\\classic\\";
string sJVMpath = sRuntimePath + "jvm.dll";
// -啟動路徑
string sBootPath = sJREPath + "\\lib";
string sOption_BootPath = "-Dsun.boot.class.path=" + sBootPath;
// -CLASSPATH
string sClassPath = sAppHome + "\\classes";
string sOption_ClassPath = "-Djava.class.path=" + sClassPath;
// 設置VM參數(shù)
// vAddOption(string("-verbose"));
vAddOption(sOption_ClassPath);
vAddOption(sOption_AppHome);
// VM初始化參數(shù)
JavaVMInitArgs vm_args;
vm_args.version = 0x00010002;
vm_args.options = vm_options;
vm_args.nOptions = mctOptions;
vm_args.ignoreUnrecognized = JNI_TRUE;
// 裝入JVM庫
HINSTANCE hJVM = LoadLibrary(sJVMpath.c_str());
if( hJVM == NULL ){
vShowLastError("不能從下面的路徑裝入JVM:" + sJVMpath);
return 0;
}
// 啟動1.2/3/4 VM
CreateJavaVM lpfnCreateJavaVM = (CreateJavaVM) GetProcAddress(hJVM, "JNI_CreateJavaVM");
jintVMStartupReturnValue = (*lpfnCreateJavaVM) (&jvm, &env, &vm_args);
// 是否成功?
if (jintVMStartupReturnValue < 0) {
string sErrorMessage = "創(chuàng)建VM失敗。";
vShowError(sErrorMessage);
vDestroyVM(env, jvm);
return 0;
}
// 要啟動的類
string sStartupClass = "javabunny/JavaBunny";
// 注意句點符號已經(jīng)被轉換成斜杠
jclassStartup = env->FindClass(sStartupClass.c_str());
if (jclassStartup == NULL) {
string sErrorMessage ="找不到啟動類[" +sStartupClass + "]";
vShowError(sErrorMessage);
vDestroyVM(env, jvm);
return 0;
}
// 要啟動的方法
string sStartupMethod_Identifier = "main";
string sStartupMethod_TypeDescriptor ="([Ljava/lang/String;)V";
midStartup = env->GetStaticMethodID(jclassStartup,
sStartupMethod_Identifier.c_str(),
sStartupMethod_TypeDescriptor.c_str());
if (midStartup == NULL) {
string sErrorMessage = "找不到啟動方法["+ sStartupClass + "."+ sStartupMethod_Identifier
+ "],類型描述符是[" + sStartupMethod_TypeDescriptor + "]";
vShowError(sErrorMessage);
vDestroyVM(env, jvm);
return 0;
}
// 構造啟動方法的參數(shù)
jstring jstringExampleArg;
jclass jclassString;
jobjectArray jobjectArray_args;
jstringExampleArg = env->NewStringUTF("string1");
if (jstringExampleArg == NULL){
vDestroyVM(env, jvm);
return 0;
}
jclassString = env->FindClass("java/lang/String");
jobjectArray_args = env->NewObjectArray(1, jclassString, jstringExampleArg);
if (jobjectArray_args == NULL){
vDestroyVM(env, jvm);
return 0;
}
// 調用啟動方法啟動Java程序
env->CallStaticVoidMethod(jclassStartup, midStartup, jobjectArray_args);
// 在退出之前嘗試分離主線程
if (jvm->DetachCurrentThread() != 0) {
vShowError("分離主線程失敗。\n");
}
// 只要還有非守護線程,下面的調用將一直被掛起
jvm->DestroyJavaVM();
return 0;
}
void vDestroyVM(JNIEnv *env, JavaVM *jvm){
if (env->ExceptionOccurred()) {
env->ExceptionDescribe();
}
jvm->DestroyJavaVM();
}
void vShowError(string sError) {
MessageBox(NULL, sError.c_str(), "錯誤", MB_OK);
}
/* 在對話框中顯示錯誤信息,括號內包含
的GetLastError錯誤信息 */
void vShowLastError(string sLocalError) {
LPVOID lpSystemMsgBuf;
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpSystemMsgBuf, 0, NULL );
string sSystemError = string((LPTSTR)lpSystemMsgBuf);
vShowError(sLocalError + " [" + sSystemError + "]");
}
void vAddOption(string& sValue) {
mctOptions++;
if (mctOptions >= mctOptionCapacity) {
if (mctOptionCapacity == 0) {
mctOptionCapacity = 3;
vm_options = (JavaVMOption*)malloc(mctOptionCapacity * sizeof(JavaVMOption));
} else {
JavaVMOption *tmp;
mctOptionCapacity *= 2;
tmp = (JavaVMOption*)malloc(mctOptionCapacity * sizeof(JavaVMOption));
memcpy(tmp, vm_options, (mctOptions-1) * sizeof(JavaVMOption));
free(vm_options);
vm_options = tmp;
}
}
vm_options[mctOptions-1].optionString = (char*)sValue.c_str();
}
/* 如果緩沖區(qū)是"c:\app\bin\java",則把"c:\app"放入buf。*/
jboolean GetApplicationHome(char *buf, jint sz) {
char *cp;
GetModuleFileName(0, buf, sz);
*strrchr(buf, '\\') = '\0';
if ((cp = strrchr(buf, '\\')) == 0) {
// 如果應用程序放在驅動器的根目錄下,且不存在bin目錄
// 會出現(xiàn)這種情形
buf[0] = '\0';
return JNI_FALSE;
}
return JNI_TRUE;
} |
首先,就象大多數(shù)Windows程序一樣,啟動器需要一個WinMain()入
口。與Windows這一特定平臺相關的問題,除了必要的類型轉換(例如對CreateJavaVM()的轉換)之外,另外一個要注意的地方就是裝入VM
的DLL文件。裝入DLL文件最可靠的辦法是顯式地調用LoadLibrary()。裝入JVM之后,就可以利用內核調用
GetProcAddress()獲得CreateJavaVM()的函數(shù)指針,然后調用該指針啟動VM。
在啟動類的標識符中使用的分隔符是斜杠,而不是句點,即我們用
“javabunny/JavaBunny”表示啟動類,而不是用“javabunny.JavaBunny”的形式。這是因為,F(xiàn)indClass()
是一個虛擬機調用,而虛擬機內部用斜杠作為分隔符。隨便說明一下,這個例子把啟動類的名字(和其他一些配置選項)直接寫進了代碼之中(稱為“硬編碼”),
對于提供給最終用戶使用的產品,這種做法有其優(yōu)點;但對于開發(fā)環(huán)境來說,這些值最好拿出來放在某個配置文件中。
Java程序啟動后執(zhí)行的第一個方法稱為啟動方法,通常是main()。本例通過
JNI調用GetStaticMethodID()獲得啟動方法的ID。GetStaticMethodID()要求指定方法的名字(“main”)和方
法的類型描述符(“([Ljava/lang/String;)V”)。這個類型描述符表示方法的參數(shù)是一個字符串的數(shù)組,返回值類型是void。有關類
型描述符的更詳細的說明,請參見JVM相關資料。注意,從這里可以看出,在使用定制啟動器時,Java程序的啟動方法不必一定是static
void的main方法,可以用任何方法作為Java程序中第一個執(zhí)行的方法,甚至包括實例方法或構造函數(shù)。
示例程序中最后一個需要注意的地方是jvm->DestroyJavaVM()
調用。從表面看起來,這個語句似乎是程序執(zhí)行后進行清理工作的方法,可有可無。其實不然,如果Java程序是多線程的,在調用這個方法時程序仍舊在運行。
例如,對于一個運行著的Swing程序,如果它的main方法結束,DestroyJavaVM()的執(zhí)行將被阻塞,直至所有非守護線程都執(zhí)行完畢,所以
這行代碼是必不可少的。如果省略這行代碼,則當主線程執(zhí)行完畢,即使其他線程(例如Swing GUI的事件循環(huán))仍舊在運行,整個程序也會立即退出。
三、配置和使用
如前所述,這個啟動器以硬編碼的方式指定了啟動類的名字,但是沒有一個路徑是硬編碼
的。這是定制啟動器的優(yōu)點之一,由于所有的路徑都是相對的,用戶可以把整個Java應用從一個文件夾拖到另一個驅動器(或另一臺機器)的文件夾,程序的運
行不會出現(xiàn)任何問題。本文的啟動器假定JRE總是在應用軟件所在目錄的一個子目錄下,也就是說,JRE應當隨同應用軟件一起發(fā)布。這樣做的好處是使得應用
軟件完全不依賴于用戶的環(huán)境,確保了JRE與應用程序的兼容性。即使用戶系統(tǒng)中原來已經(jīng)有JRE,增加一個額外的JRE也只不過稍微占用了一點磁盤空間,
但卻能有效地保證應用軟件的穩(wěn)定性。
在某些場合,你可能需要將一些配置參數(shù)移出程序,例如放入一個配置文件,特別是在需要頻繁改動啟動方式的開發(fā)階段。建議移出程序之外的配置選項包括:啟動類,類的路徑,某些VM參數(shù),例如“-verbose”。
(###)