HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML

The most frustrating and error-prone aspect of Java for the average user is starting a Java program. The monumental confusion of batch files, scripts, and command-line cut-and-paste that's necessary to start a Java program using the default launcher is an ongoing problem area even for veteran developers.

This article shows how you can wipe away the whole mess and easily write custom launchers for your applications. A custom launcher makes startup as simple as point-and-click and can be the difference between a program appearing professional or appearing unusable. The specifics on making launchers for all the major platforms are also covered.

A long-time barrier to user acceptance of Java has been the confusing and unfriendly default launcher. It requires long strings of command-line arguments before it will start a Java program. Both users and software developers constantly receive the java.lang.NoClassDefFound exception and other errors. Sun's noble response to this problem is to provide a public API, the invocation interface, that can be used to start the JVM and accompanying Java program. Although a few commercial applications use this API, it's woefully underutilized overall. I'll show how easy it is to create a custom launcher and provide templates that you can use to automate startup of your Java programs. There's even a generic configurable launcher that's ready to run, no compilation necessary.

I focus on the three major desktop platforms: Windows, Mac OS, and Unix. Each platform has its own quirks, but using a custom launcher brings benefits that are common to all three, such as:

  • Program startup is easier and more reliable.
  • Software identification and branding are better.
  • Greater customization and VM control is possible.
Given that a commercial-quality launcher requires only about 200 lines of code and templates are provided, there's no reason you can't get started today.

How Java Programs Are Launched
The complexity of launching a Java program is largely due to Java being an interpreted language. This makes startup and shutdown a multistep procedure (see Figure 1).

figure 1
Figure 1

As the figure suggests, any native executable can start a Java program. A Java launcher is a native executable dedicated solely to starting Java programs. The most commonly used launchers are the ones Sun supplies in the /bin directory of the Java runtime distribution. In the case of the Windows platform, these programs are "java.exe" and "javaw.exe". The former opens two windows: a console that receives System.out/err and output from the launcher and the Java window itself. The latter, a windowless launcher, opens only the Java window. On J2SE/EE platforms the virtual machine is implemented as a dynamic link library that's also in the /bin directory. On Windows it's called "java.dll", on Unix "java.so". Loading the VM equates to loading this DLL.

Users specify options to the VM in two ways. They can put the options on the command line to the launcher and/or define environment variables with the desired settings. One of the options, the startup class, can only be specified on the command line. This bifurcation of the execution configuration is a common source of confusion that can be eliminated by using a custom launcher.

When the virtual machine has finished running the main() method of the startup class, the launcher calls destroy() on the VM to free any detachable resources and then exits. Note that there's no way to unload a VM once it's been loaded. This makes no difference to a launcher since it will exit as soon as the Java program is done; however, for a native application that embeds a VM, such as a browser, it means there's a permanent commitment of memory that can't be reclaimed.

Nuances of Creating a Windows Launcher
Once you understand the Java life cycle you're ready to code a launcher. Be aware that some of the generic code examples floating around on the Web and in books, such as The Java Native Interface by Sheng Liang (see Resources), won't work on a platform such as Windows without changes. The working example in C++ for Windows illustrates some of the nuances (see Listing 1).

First, use a WinMain() entry point as you would for most Windows applications. Also, you need to prototype CreateJavaVM() to use the stdcall calling convention by typedef'ing it as a pointer to CALLBACK. These are Windows-specific requirements. Another platform-specific nuance is loading the VM DLL. The most reliable way to load the VM is by an explicit call to LoadLibrary:

HINSTANCE hJVM = LoadLibrary(sJVMpath.c_str());

First determine the path of the JVM's DLL and then explicitly load it. This differs from the example in The Java Native Interface, which uses implicit loading. The problem with implicit loading is that it makes assumptions about the location of the DLL that might not be true for all environments. By explicitly loading the JVM you can place it anywhere you like in your distribution and verify that it's really there before attempting to load it. Once you load the JVM, obtain a function pointer to CreateJavaVM() by using the kernel call GetProcAddress() and then calling that pointer to start the VM.

The next nuance in the listing is that the separators used in the startup class identifier are slashes, not dots. So in the listing the startup class is "javabunny/JavaBunny", not "javabunny.JavaBunny". This is because FindClass() is a virtual machine call and the virtual machine internally uses the slash as its package separator. By the way, the example hard codes the startup class (and other values). This may be appropriate for a shrink-wrapped product release, but in a development environment you'll probably want to pull this value from a configuration file. Later, I'll describe a more generic template that does this.

The example determines the startup method ID by using the JNI call GetStaticMethodID(). This call requires the method name ("main") and the type descriptor "([Ljava/lang/String;)V". This type descriptor means the method takes an array of strings as an argument and has a return type of void. For more information on type descriptors see The Java Virtual Machine Specification (see Resources). Notice that when you create a custom launcher you're not restricted to using a static void method called "main". You can start with any method at all, even an instance method or constructor.

The last tricky point of a launcher is hidden behind the following line at the end of the listing:

jvm->DestroyJavaVM();

This statement looks like optional cleanup added as an afterthought to program execution. Not true! If the Java program is multithreaded, it will still be executing during this call. For example, if a Swing program runs and its main method exits, this line will execute and block until all nondaemon threads have completed. This blocking behavior makes it critical that you include this line. If you omit it, the program will exit as soon as the main thread terminates, even if other threads (like the event loop of your GUI) are still running.

Launcher Configuration
In Listing 1 I hard code some of the key parameters such as the startup class. Notice, however, that none of the paths are hard coded. This is part of the beauty of a custom launcher all the paths are relative, so you can drag the application folder to another drive (or computer) and it will run flawlessly. Try doing that with a batch file. Listing 1 always uses a JRE located in a subfolder of the application folder. By distributing a JRE with your application, like this, you guarantee runtime compatibility and make your application totally independent of the user's environment. The extra disk space used by adding yet another JRE to the user's disk drive is meaningless compared to the increased reliability. When writing your own launchers you may want to use different directory layouts than the one in Listing 1. As long as all the paths are relative to the native executable's location, you're fine.

Resource paths can be made flexible enough that they don't need to be configured, but some values will need to be configurable outside of the launcher, especially in an oft-changing development environment. These include:

  • The startup class
  • The class path
  • Special VM parameters such as "-verbose"
The best way to specify these parameters is to load them from a configuration file located in the same directory as the launcher executable. (The source code for this article can be downloaded from below and includes code for a launcher that configures itself this way.) By using a resource editor to replace the icons in this launcher's binary with your own, you can use it repeatedly for all your applications without ever needing to compile.

Mac Launchers
In the Macintosh universe, life is much easier. Java development on the fruit boxes is divided into two scenarios: OS X and pre-OS X ("Classic Mac OS"). OS X has strong Java support compared to Classic Mac. For example, Classic Mac supports only 1.1.8, so for many developers it will be irrelevant. Swing support on Classic Mac is available if the user downloads and installs MRJ 2.2.5, but for anything more recent like J2EE, forget about it.

If these restrictions don't faze you, create a native launcher on Classic Mac by using Apple's old native toolkit called JDirect (don't confuse this with the obsolete "J/Direct" that worked with Microsoft's J++). A much easier way to create a clickable icon, however, is to use a special Apple tool called "JBindery". This tool creates a distribution that so closely resembles a native application that writing a native launcher is unnecessary. You can completely configure your distribution package using JBindery, including defining security settings and the appearance of the Java window. When you're done, use ResEdit to add a custom icon to the package and it's ready to run. Apple considers Mac Classic, JBindery, and this whole methodology obsolete, but if you want to support the many users who are sticking with OS 9.1/2, it's your best option.

The new Mac world is all OS X. In OS X the application layer is called "Cocoa" and you access it with Objective-C. Is that retro or what? Despite how weird it sounds, Java support is excellent because an interface called the "Java Bridge" wraps the Java Native Interface (including the invocation interface) and makes a seamless connection between your native code and Java code.

As with the Classic OS, writing a native launcher is unnecessary, since Apple has provided a great bundling tool, MRJAppBuilder. If your Objective-C skills are a little rusty and you're working solely in Java, the best approach is to use MRJAppBuilder. Apple has designed this bundler especially for packaging Java applications. Note that the bundling framework the tool uses is the standard way to deploy all Cocoa applications, not just Java applications. This enlightened approach to application distribution means that on OS X, a bundled Java application is externally indistinguishable from an Objective-C application and behaves in all ways like a native executable.

The powerful capabilities of the bundlers (JBindery for Mac Classic and MRJAppBuilder for OS X) eliminate the need for a custom launcher on the Macintosh unless you're doing something offbeat such as starting from an instance method. If you really need to go native on the Macintosh, the article's download package has some code examples that will get you started. Otherwise, stick with the bundlers and you can sit back and laugh at the PC programmers while they fiddle endlessly with batch files.

Unix Launchers
Unix (or Linux) is the inverse of OS X it has no explicit support for Java or even for native application packaging. For example, on Unix desktops the icons live separately from their applications and the relationships between them are managed by configuration files or scripts. Issues like compiling icon resources into the launcher binary don't exist under Unix. This means an easy and reliable startup mechanism is more a function of your installation script than anything else.

Even so, a custom launcher still has many benefits under Unix. For example, in a process listing, the Java command line is usually so long it gets truncated, and on a server machine running multiple VMs it can be a pain to identify which process is which. You can create a custom launcher that simplifies and shortens these startup commands and thus make the process listing more meaningful.

One of the advantages of Unix's simplicity is that its launcher code is the easiest of all the platforms. The basic Unix launcher is the same as the Windows example shown in Listing 1 without the Windows-specific type conversions and Windows configuration issues (see the download package for an example). Another advantage is that a Unix launcher will generally work in any Unix environment as long as it's recompiled once again, something that the installation script manages.

The disadvantage of this simplicity as compared to other OSs is that you are more or less obliged to use scripts of some sort even if you do implement a custom launcher. Good thing Unix has such great scripting capabilities.

Conclusion
By mastering the art of creating custom launchers for your Java applications, you can ramp up their convenience, professionalism, and reliability. The ease of creating launchers along with the use of configuration files makes them ideal for use in development environments as well as in release distributions. Do yourself a favor: learn to code a launcher and say goodbye to java.lang.NoClassDefFound.

Resources

  • Liang, S. (1999). The Java Native Interface: Programmer's Guide and Specification. Addison-Wesley.
  • Lindholm, T., and Yellin, F. (1999). The Java Virtual Machine Specification. Addison-Wesley.

    Author Bio
    John Chamberlain is a consultant in the Boston area. He holds a master's degree in computer science, is a frequent contributor to technical journals, and has been a speaker at JavaOne. (http://johnchmberlain.com) [email protected]

    	
    
    
    Listing 1: Typical Windows launcher for a 1.2 or later 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;
    
    
       // Path Determination
    
    
       // --- application home
       char home[2000];
       if (!GetApplicationHome(home, sizeof(home))) {
          vShowError("Unable to determine \
            application home.");
       return 0;
       }
       string sAppHome(home);
       string sOption_AppHome = "-Dapplication.home="
          + sAppHome;
    
    
       string sJREPath = sAppHome + "\\jre";
    
    
       // --- VM Path
       string sRuntimePath  = sJREPath +
          "\\bin\\classic\\"; // must contain jvm.dll
       string sJVMpath = sRuntimePath + "jvm.dll";
    
    
       // --- boot path
       string sBootPath = sJREPath + "\\lib";
       string sOption_BootPath =
          "-Dsun.boot.class.path=" + sBootPath;
    
    
       // --- class path
       string sClassPath = sAppHome + "\\classes";
       string sOption_ClassPath =
          "-Djava.class.path=" + sClassPath;
    
    
       // setup VM options
       // vAddOption(string("-verbose"));
       vAddOption(sOption_ClassPath);
       vAddOption(sOption_AppHome);
    
    
       // initialize args
       JavaVMInitArgs vm_args;
       vm_args.version = 0x00010002;
       vm_args.options = vm_options;
       vm_args.nOptions = mctOptions;
       vm_args.ignoreUnrecognized = JNI_TRUE;
    
    
       // load jvm library
       HINSTANCE hJVM = LoadLibrary(sJVMpath.c_str());
       if( hJVM == NULL ){
          vShowLastError("Failed to load JVM from "
            + sJVMpath);
          return 0;
       }
    
    
       // try to start 1.2/3/4 VM
       // uses handle above to locate entry point
       CreateJavaVM lpfnCreateJavaVM = (CreateJavaVM)
          GetProcAddress(hJVM, "JNI_CreateJavaVM");
       jintVMStartupReturnValue = (*lpfnCreateJavaVM)
          (&jvm, &env, &vm_args);
    
    
       // test for success
       if (jintVMStartupReturnValue < 0) {
          string sErrorMessage = "Unable to create VM.";
          vShowError(sErrorMessage);
          vDestroyVM(env, jvm);
          return 0;
       }
    
    
       // find startup class
       string sStartupClass = "javabunny/JavaBunny";
       // notice dots are translated to slashes
       jclassStartup = 
          env->FindClass(sStartupClass.c_str());
       if (jclassStartup == NULL) {
          string sErrorMessage =
            "Unable to find startup class [" +
            sStartupClass + "]";
          vShowError(sErrorMessage);
          vDestroyVM(env, jvm);
          return 0;
       }
    
    
       // find startup method
       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 =
          "Unable to find startup method ["
          + sStartupClass + "."
          + sStartupMethod_Identifier
          + "] with type descriptor [" +
          sStartupMethod_TypeDescriptor + "]";
          vShowError(sErrorMessage);
          vDestroyVM(env, jvm);
          return 0;
       }
    
    
       // create array of args to startup method
       jstring jstringExampleArg;
       jclass jclassString;
       jobjectArray jobjectArray_args;
       jstringExampleArg =
         env->NewStringUTF("example string");
       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;
       }
    
    
       // call the startup method -
       // this starts the Java program
       env->CallStaticVoidMethod(jclassStartup,
         midStartup, jobjectArray_args);
    
    
       // attempt to detach main thread before exiting
       if (jvm->DetachCurrentThread() != 0) {
          vShowError("Could not detach main thread.\n");
       }
    
    
       // this call will hang as long as there are
       // non-daemon threads remaining
       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(),
         "Model App Error", MB_OK);
    }
    
    
    /* Shows an error message in an OK box with the
       system GetLastError appended in brackets */
    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();
    }
    
    
    /* If buffer is "c:\app\bin\java",
     * then put "c:\app" into buf. */
    jboolean GetApplicationHome(char *buf, jint sz) {
       char *cp;
       GetModuleFileName(0, buf, sz);
       *strrchr(buf, '\\') = '\0';
       if ((cp = strrchr(buf, '\\')) == 0) {
          // This happens if the application is in a
        // drive root, and there is no bin directory.
          buf[0] = '\0';
          return JNI_FALSE;
       }
       return JNI_TRUE;
    }
    
     
    

    Additional Code For This Article (~ 49.2 KB ~zip format )

    All Rights Reserved
    Copyright ©  2004 SYS-CON Media, Inc.
      E-mail: [email protected]

    Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.