分享

打造个性化Java启动器

 9loong 2012-03-28


2008-04-03 11:01:29|  

主要内容:

________________________

 一、Java程序的启动过程

 二、Windows平台的启动器

 三、配置和使用

________________________

对于普通用户来说,Java最让人不习惯的是程序的启动过程;即使对于富有经验的开发者,为了用默认的装载器启动Java程序,不得不编写大量批命令、脚本文件,不得不在命令行环境下进行大量的复制/粘贴操作,也很容易出现误操作。

用惯了Windows方便快捷的GUI,人们早就习惯了通过双击运行程序的方式。对于 Java程序,要实现这个本机启动功能就必须编写定制的启动器。用定制启动器启动Java程序不仅方便了最终用户,而且使软件作品看起来更专业。本文就以 Windows平台为例,介绍如何构造Java定制启动器。

一、Java程序的启动过程

和C/C++程序相比,Java程序的启动过程要复杂得多,这主要是因为Java是一种编译成中间语言(字节码)后解释执行的语言。启动和关闭Java程序需要多个步骤才能完成,如图一所示。

图一

Java程序可以由任何本机运行的程序调用执行。所谓Java启动器,就是一个专门用来启动 Java程序的本机执行程序。最常见的启动器是Sun在Java Runtime Environment的/bin目录中提供的启动器,就Windows平台而言,它们是java.exe和javaw.exe。前者运行时打开两个窗 口:一个是接收System.out/err和启动器输出的控制台窗口,另一个是Java程序本身的窗口;javaw运行时不打开控制台窗口。在J2SE /EE平台中,虚拟机以动态库的形式实现,也放在/bin目录下。动态库的名字在Windows中是java.dll,在Unix中是java.so。所 谓“装入虚拟机”,就是指装入这个动态库。

提供给VM的参数可以通过两种方式指定,或者是在启动器的命令行参数中指定,或者通过 定义相应的环境变量指定。只有一个参数例外——要启动的类的名称只能在启动器的命令行参数中指定。虽然指定方式的多样性为人们各取所需带来了方便,但不可 否认地,它也正是许多混乱的根源。使用定制启动器能够完全避免这方面的问题。

当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参数
    // vAddOption(string("-verbose"));
    vAddOption(sOption_ClassPath);
    vAddOption(sOption_AppHome);
    // VM初始化参数
    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 = "创建VM失败。";
        vShowError(sErrorMessage);
        vDestroyVM(env, jvm);
        return 0;
    }
    // 要启动的类
    string sStartupClass = "javabunny/JavaBunny";
    // 注意句点符号已经被转换成斜杠
    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;
    }
    // 构造启动方法的参数
    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();
}

/* 如果缓冲区是"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目录
        // 会出现这种情形
        buf[0] = '\0';
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

首先,就象大多数Windows程序一样,启动器需要一个WinMain()入 口。与Windows这一特定平台相关的问题,除了必要的类型转换(例如对CreateJavaVM()的转换)之外,另外一个要注意的地方就是装入VM 的DLL文件。装入DLL文件最可靠的办法是显式地调用LoadLibrary()。装入JVM之后,就可以利用内核调用 GetProcAddress()获得CreateJavaVM()的函数指针,然后调用该指针启动VM。

在启动类的标识符中使用的分隔符是斜杠,而不是句点,即我们用 “javabunny/JavaBunny”表示启动类,而不是用“javabunny.JavaBunny”的形式。这是因为,FindClass() 是一个虚拟机调用,而虚拟机内部用斜杠作为分隔符。随便说明一下,这个例子把启动类的名字(和其他一些配置选项)直接写进了代码之中(称为“硬编码”), 对于提供给最终用户使用的产品,这种做法有其优点;但对于开发环境来说,这些值最好拿出来放在某个配置文件中。

Java程序启动后执行的第一个方法称为启动方法,通常是main()。本例通过 JNI调用GetStaticMethodID()获得启动方法的ID。GetStaticMethodID()要求指定方法的名字(“main”)和方 法的类型描述符(“([Ljava/lang/String;)V”)。这个类型描述符表示方法的参数是一个字符串的数组,返回值类型是void。有关类 型描述符的更详细的说明,请参见JVM相关资料。注意,从这里可以看出,在使用定制启动器时,Java程序的启动方法不必一定是static void的main方法,可以用任何方法作为Java程序中第一个执行的方法,甚至包括实例方法或构造函数。

示例程序中最后一个需要注意的地方是jvm->DestroyJavaVM() 调用。从表面看起来,这个语句似乎是程序执行后进行清理工作的方法,可有可无。其实不然,如果Java程序是多线程的,在调用这个方法时程序仍旧在运行。 例如,对于一个运行着的Swing程序,如果它的main方法结束,DestroyJavaVM()的执行将被阻塞,直至所有非守护线程都执行完毕,所以 这行代码是必不可少的。如果省略这行代码,则当主线程执行完毕,即使其他线程(例如Swing GUI的事件循环)仍旧在运行,整个程序也会立即退出。

三、配置和使用

如前所述,这个启动器以硬编码的方式指定了启动类的名字,但是没有一个路径是硬编码 的。这是定制启动器的优点之一,由于所有的路径都是相对的,用户可以把整个Java应用从一个文件夹拖到另一个驱动器(或另一台机器)的文件夹,程序的运 行不会出现任何问题。本文的启动器假定JRE总是在应用软件所在目录的一个子目录下,也就是说,JRE应当随同应用软件一起发布。这样做的好处是使得应用 软件完全不依赖于用户的环境,确保了JRE与应用程序的兼容性。即使用户系统中原来已经有JRE,增加一个额外的JRE也只不过稍微占用了一点磁盘空间, 但却能有效地保证应用软件的稳定性。

在某些场合,你可能需要将一些配置参数移出程序,例如放入一个配置文件,特别是在需要频繁改动启动方式的开发阶段。建议移出程序之外的配置选项包括:启动类,类的路径,某些VM参数,例如“-verbose”。


(###)

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多