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
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)
jcpublic@attbi.com
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;
}