Item 69: Use independent JREs for side-by-side versioning



Item 69: Use independent JREs for side-by-side versioning

One of the touted features of the Microsoft .NET platform has been its support for "side-by-side versioning"—in other words, a successor version of the .NET platform can be installed on the same machine as a previous version without adverse effects. For the first version of an application release, this seems like an overvalued option. Once the second release of an application occurs, however, or another application is deployed into the application server, or two application servers need to coexist side by side, this becomes absolutely critical to ensuring everything runs correctly.

Consider, for example, a company that wrote its first EJB application against an application server bound to the JDK 1.3 a few years ago. This application is still in use and is quite popular with its community of users when the company is acquired by or merges with or acquires another company. Naturally, the first desire is to consolidate the second company's enterprise applications with the first—not an easy task if the second company is using an entirely different application server that requires JDK 1.4. Ideally, it would be possible to take the .ear files from the first application server and run them, untouched, in the second. Unfortunately, it's never that simple.

For starters, the new composite company entity has decided to maintain the first application server vendor as its enterprise application server partner—this means the second application server, the one dependent on JDK 1.4, can't be used going forward. Unfortunately, this leads to another problem. The second enterprise application made use of several JDK 1.4–specific features, like the Preferences and logging APIs.

If this story seems a bit incredible so far, consider another variant: currently, your company makes use of a standalone servlet container to handle all servlet/JSP operations and a separate EJB container for its EJB operations on the same machine (in order to avoid round-trips—see Item 17). When these servers were first deployed to the machine, they both required JDK 1.3; however, a security hole was discovered in the servlet container, and it needs to be upgraded to the latest version, which requires JDK 1.4.

Regardless of which story you find more credible and/or possible, the ultimate issue here is that a single machine is now facing the problem of having to support both JDK 1.3 and JDK 1.4 installations. Or, going forward, JDK 1.4 and JDK 1.5, or JDK 1.4 and JDK 2.0, and so on.

Normally, when Java is installed on a machine, the JDK is deployed to a shared directory, usually /usr/local/java on UNIX-based machines. In the case of Win32 boxes, two JRE installations are laid down: the first goes in the location specified by the user during the installation program, the second goes to two other locations. The bulk of the JRE goes in C:\Program Files\Java\(JRE version), but some executable files get copied to C:\WINNT\SYSTEM32 as well—this is so that "java" can always be accessible from any command prompt since C:\WINNT\SYSTEM32 (or C:\WINDOWS\SYSTEM32, on Win9x or XP or later installations) is intrinsically part of the PATH by default.

However, just because the installation program drops Java into a shared location doesn't mean all applications require Java to be run from that shared location; in fact, all the application really needs is the set of files required at runtime, the JRE. As it turns out, all it really needs at startup is to know where the JRE installation root is so that it can find the necessary configuration files to kick-start the JVM.

Go back to the launcher code we examined in Item 68 and take a look at the early parts of the main function again. There, the launcher discovers the path to the executing JRE. Remember, this is the code to java.exe, so technically the launcher is trying to discover exactly where it was launched from. This discovery comes from the function GetJREPath, which isn't defined anywhere inside the java.c source file; instead, it is a platform-dependent function whose implementation varies between Solaris, Linux, and Win32 builds of the JDK. It therefore is defined instead in the java_md.c source file, in the same directory as java.c; the relevant snippets (from the Win32 version in this case) are outlined here:






/*

 * Find path to JRE based on .exe's location or registry

 * settings.

 */

jboolean

GetJREPath(char *path, jint pathsize)

{

  char javadll[MAXPATHLEN];

  struct stat s;

  if (GetApplicationHome(path, pathsize))

  {

    /* Is JRE co-located with the application? */

    sprintf(javadll, "%s\\bin\\" JAVA_DLL, path);

    if (stat(javadll, &s) == 0)

    {

      goto found;

    }



    /* Does this app ship a private JRE in

       <apphome>\jre directory? */

    sprintf(javadll, "%s\\jre\\bin\\" JAVA_DLL, path);

    if (stat(javadll, &s) == 0)

    {

      strcat(path, "\\jre");

      goto found;

    }

  }



  /* Look for a public JRE on this machine. */

  if (GetPublicJREHome(path, pathsize))

  {

    goto found;

  }



  fprintf(stderr, "Error: could not find " JAVA_DLL "\n");

  return JNI_FALSE;



found:

  if (debug) printf("JRE path is %s\n", path);

  return JNI_TRUE;

}



/* ... Some code elided for brevity ... */



/*

 * If app is "c:\foo\bin\javac", then put "c:\foo" into buf.

 */

jboolean

GetApplicationHome(char *buf, jint bufsize)

{

  char *cp;

  GetModuleFileName(0, buf, bufsize);

  *strrchr(buf, '\\') = '\0'; /* Remove .exe file name */

  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;

  }

  *cp = '\0';  /* Remove the bin\ part */

  return JNI_TRUE;

}


While we could walk through the definitions of GetApplicationHome and GetPublicJREHome, it's not really necessary to understand the low-level details of what's going on here. When the launcher seeks to discover the path to the JRE, it first checks whether the JRE is co-located with the application; that is, whether the root of the JRE's directory structure is the same directory the application itself runs in. Second, assuming a structure similar to the JRE's isn't found there, it looks to see if the application is running a "private" JRE—the JRE is installed in a subdirectory underneath the application's home directory. Last, assuming neither of those tests pan out to be true, the launcher does a Win32 Registry lookup. It searches for the key Software/JavaSoft/Java Runtime Environment under the HKEY_LOCAL_MACHINE hive. The value named CurrentVersion holds the most recently installed version of the JVM, which in turn acts as the last part of the key; assuming, for example, that the value of CurrentVersion is 1.4.1, the launcher looks up Software/JavaSoft/Java Runtime Environment/1.4.1 and finds three values: JavaHome, where the JRE is rooted in the directory structure; MicroVersion, which indicates the minor version number of the release (1 in this case since this is JDK 1.4.1); and RuntimeLib, the location of the jvm.dll file containing the native parts of the JVM itself. Once all this is discovered, the launcher dynamically loads the jvm.dll file, uses that to create a JVM instance via the JNI Invocation API, and passes control into the JVM by calling the main method of the class specified as a command-line argument.

The key thing to recognize out of all this is the order in which this discovery takes place—in particular, the fact that the launcher first looks to see where the JRE is in relation to the directory in which execution is taking place. This implies that each Java application can in turn have its own JRE without conflict. In fact, it's as simple as copying the entire subdirectory tree of the desired JRE from the JDK (the jre subdirectory inside the main JDK installation root) to anywhere underneath the container's installation, and using that java launcher to invoke the container. For example, I routinely run multiple JDKs as well as the Tomcat servlet container on my laptop, so I've got a JRE copied to the root of the Tomcat installation, and the startup batch scripts point to it instead of relying on the PATH to identify which java.exe launcher to use.

This provides a measure of isolation against accidental or unknown version incompatibilities on that system. Each JRE remains entirely independent of any other JRE installed on the system, meaning that the server or container will continue to run regardless of what JDKs are installed or uninstalled on the box. It can also simplify deployment in some cases, removing the need for the system administrators to know which is the correct version of the JRE to deploy. Simply provide the JRE as a tarball or zip file for them to unpack in the correct place; no other changes are necessary. (The JNLP seeks to handle this transparently for client-side applications.)

By the way, nothing in this item is Win32-specific; I just listed the code for the Win32 version because of its greater complexity (owing to its use of the Registry to look up the location of the "public" JRE). The UNIX version of GetApplicationHome is actually much simpler and provides all the information necessary to the launcher to bootstrap the JVM into existence. No reliance on a GetJREPath function is necessary at that point. The launcher uses the in-memory address of the GetApplicationHome function to look up the dynamic-loader information about the executable (either an executable application or shared library) in which that function resides, then takes that resulting information, which contains the directory in which the executable file resides, and walks back up the known JRE directory structure to determine what the root must be. (See the dladdr system call in your system's programmer's API reference for details.) Again, the point is that the lookup is entirely relative to the launcher executable's location.

Now, going into production, the application server, servlet container, Java-powered database, and other Java services can each run under a precisely defined JVM environment. Note that this isolation of JVMs across containers also provides an opportunity to better segregate Java extensions (Java .jar files copied into the extensions directory, jre/lib/ext) between containers and helps eliminate some confusion with respect to the JDK 1.4 "endorsed standards directory" and XML parsers and CORBA ORBs. By the way, this item isn't limited to just enterprise server containers. I routinely give the most recent Ant installation on my laptop its own JRE to play with, and I install all of the Ant-dependency .jar files, like JUnit, into the extensions directory of that JRE, just for simplicity. Each JVM runs with its own settings, its own extensions, and, perhaps most importantly, its own Hotspot configuration file (see Item 68). Changes to one won't affect the others, which is exactly what we want in any kind of production environment.