Java 9's other new enhancements, Part 4: Multi-release JAR files

Multiple, Java-release-specific versions of class/resource files can now coexist in the same JAR file

lock door security
J. Triepke (CC BY 2.0)

JEP 238: Multi-Release JAR Files extends the JAR file format to allow multiple, Java-release-specific versions of class/resource files to coexist in the same archive. This upgrade makes it easier for third-party libraries and frameworks to use language and API features introduced in newer Java releases. This post introduces you to multi-release JAR files.

Discovering multi-release JAR files

Many third-party Java frameworks and libraries support several versions of the Java platform. For example, as of version 4.0, the Spring Framework supports Java 6, 7, and 8. Java frameworks and libraries often don't leverage the language or API features that are available in newer Java releases because of the difficulty in expressing conditional platform dependencies (which generally involves using reflection) or in distributing different library artifacts for different platform versions. For example, Spring 4.x doesn't use any Java 8 language features in its own code. However, it can autodetect and automatically activate many Java 8 API features.

The aforementioned difficulties create a disincentive for libraries and frameworks to use new features, which in turn creates a disincentive for users to upgrade to new JDK versions. This vicious circle impedes adoption of these versions, which is problematic for everyone, and which led to JEP 238 and multi-release JAR files.

Multi-release JAR file architecture

A JAR file contains a content root that stores class and/or resource files in package hierarchies, and which is like a file system's root directory. It also contains META-INF, a subdirectory of the content root that stores metadata about the JAR file. Here is an example from Java 9's java.jnlp.jar file:

   0 Wed Jan 25 17:34:12 CST 2017 META-INF/
  65 Wed Jan 25 17:34:12 CST 2017 META-INF/MANIFEST.MF
 258 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/BasicService.class
 251 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/ClipboardService.class
1392 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/DownloadService.class
1089 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/DownloadService2$ResourceSpec.class
 651 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/DownloadService2.class
 349 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/DownloadServiceListener.class
 309 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/ExtendedService.class
 659 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/ExtensionInstallerService.class
 598 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/FileContents.class
 370 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/FileOpenService.class
 430 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/FileSaveService.class
 392 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/IntegrationService.class
1451 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/JNLPRandomAccessFile.class
 688 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/PersistenceService.class
 350 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/PrintService.class
1037 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/ServiceManager.class
 303 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/ServiceManagerStub.class
 185 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/SingleInstanceListener.class
 250 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/SingleInstanceService.class
 536 Wed Jan 25 17:34:10 CST 2017 javax/jnlp/UnavailableServiceException.class
 223 Wed Jan 25 17:34:10 CST 2017 module-info.class

According to the example, java.jnlp.jar's content root contains a META-INF directory, which stores MANIFEST.MF, a javax package directory with a jnlp subpackage directory, which stores various class files belonging to this package, and a module-info.class file.

A multi-release JAR file is a JAR file whose MANIFEST.MF file includes the entry Multi-Release: true in its main section. Furthermore, META-INF contains a versions subdirectory whose integer-named subdirectories -- starting with 9 (for Java 9) -- store version-specific class and resource files. JEP 238 offers the following (enhanced) example:

JAR content root
  A.class
  B.class
  C.class
  D.class
  META-INF
     MANIFEST.MF
     versions
        9
           A.class
           B.class

In this example, the content root directory contains class files A.class, B.class, C.class, and D.class. These class files contain a pre-Java 9 version of some application or library. It also provides access to Java 9-specific A.class and B.class files in the META-INF/versions/9 directory.

A pre-Java 9 JDK only observes the content root's class files; it doesn't see the Java 9-specific A.class and B.class files. In contrast, a Java 9 JDK sees first the version 9 A.class and B.class files and then sees the content root C.class and D.class files. It's like a JAR-specific class path with versions/9 appearing before the content root.

We can extend this example to a future Java 10 JDK in which A.class is updated to leverage some Java 10 feature(s). In this scenario, we would introduce a new 10 subdirectory of versions and store the new A.class file in 10. The resulting structure is shown below:

JAR content root
  A.class
  B.class
  C.class
  D.class
  META-INF
     MANIFEST.MF
     versions
        9
           A.class
           B.class
        10
           A.class

A Java 10 JDK sees the 10-specific version of A.class and the 9-specific version of B.class. Furthermore, it sees the content root's C.class and D.class. Of course, for any of this to work, MANIFEST.MF's main section must contain the Multi-Release: true entry.

Ultimately, this architecture enables framework and library developers to decouple the use of APIs in a specific Java platform release version from the requirement that all their users migrate to that version. Library and framework maintainers can gradually migrate to and support new features while still carrying around support for the old features.

Working with multi-release JAR files

Before Java 9, obtaining a process identifier (PID) required working with the Java Native Interface and native APIs (such as the Windows GetCurrentProcessId() function), using ManagementFactory.getRuntimeMXBean().getName() and parsing out the PID from the returned string (only on Sun/Oracle JVMs) -- see Listing 1, or trying another technique.

Listing 1. Obtaining a PID prior to Java 9

import java.lang.management.ManagementFactory;

public class Util
{
   public static long getPid() 
   {
      // ManagementFactory.getRuntimeMXBean().getName() returns the name that 
      // represents the currently running JVM. On Sun and Oracle JVMs, this 
      // name is in the format <pid>@<hostname>.

      final String jvmName = ManagementFactory.getRuntimeMXBean().getName();

      // Assume the preceding format. Not all JVMs will comply.

      final int index = jvmName.indexOf('@');
      if (index < 1)
         return 0; // No PID.

      try 
      {
         return Long.parseLong(jvmName.substring(0, index));
      } 
      catch (NumberFormatException nfe) 
      {
         return 0;
      }
   }
}

Java 9 made it much easier to obtain the current PID. In my previous post, I presented the new java.lang.ProcessHandle interface and its long getPid() method for returning a PID. Listing 2 obtains the current process's PID by executing ProcessHandle.current().getPid().

Listing 2. Obtaining a PID starting with Java 9

import java.lang.management.ManagementFactory;

public class Util
{
   public static long getPid() 
   {
      System.out.println("Java 9");
      return ProcessHandle.current().getPid();
   }
}

Listing 3 presents the source code to a simple PrintPID application that works with either Util class and its long getPid() method on Java 9, or only Listing 1's Util class/getPid() method on Java 8 and lower Java versions to obtain the PID, which it then outputs.

Listing 3. Obtaining and printing the current PID

public class PrintPID
{
   public static void main(String[] args)
   {
      System.out.printf("PID: %d%n", Util.getPid());
   }
}

We can use these three classes to demonstrate a multi-release JAR file. Its content root will contain PrintPID and Listing 1's Util class (supporting Java 8 and lower Java versions), and the META-INF/versions/9 directory (Java 9 only) will store Listing 3's Util class. Complete the following steps to create this JAR file:

  1. Create v1 and v2 subdirectories of the current directory. Copy Listings 1 and 3 to v1 and Listings 2 and 3 to v2.
  2. Assuming Java 8 is current, compile the source files in v1. Assuming Java 9 is current, compile the source files in v2.
  3. Assuming that Java 9 is still current, execute the following command to create an executable pid.jar file:
    jar cfe pid.jar PrintPID -C v1 PrintPID.class -C v1 Util.class --release 9 -C v2 Util.class

Having successfully created pid.jar, execute it under Java 8 and Java 9. When you execute java -jar pid.jar under Java 8, you should observe output that's similar to the following:

PID: 8820

When you execute this command under Java 9, you should output something like that shown here:

Java 9
PID: 2484

API enhancements that support multi-release JAR files

Various Java tools (e.g., jar) and APIs have been modified to support multi-release JAR files. For example, the java.net.URLClassLoader class has been enhanced to read selected versions of class files as indicated by the running Java platform version. Also, the resource URL now refers to a versioned resource. For example, instead of specifying

jar:file:/mrjar.jar!/images/image1.png

you would now specify

jar:file:/mrjar.jar!/META-INF/versions/9/images/image1.png

Additionally, the java.util.jar.JarFile class has been enhanced to support multi-release JAR files. By default, a JarFile object for a multi-release JAR file is configured to process the multi-release JAR file as if it was a plain (unversioned) JAR file. As such, an entry name is associated with at most one base (content root) entry. However, the JarFile may be configured to process a multi-release JAR file by creating the JarFile object via the JarFile(File file, boolean verify, int mode, Runtime.Version version) constructor. The Runtime.Version object passed to this constructor sets a maximum version used when searching for versioned entries. Essentially, this is the release version for a multi-release JAR file. When so configured, an entry name can correspond with at most one base entry and zero or more versioned entries. A search is required to associate the entry name with the latest versioned entry whose version is less than or equal to the maximum version.

Along with the new constructor, JarFile includes the following new methods that also relate to multi-release JAR files:

  • static Runtime.Version baseVersion(): Return the version that represents the unversioned configuration of a multi-release JAR file.
  • static Runtime.Version runtimeVersion(): Return the version that represents the effective runtime versioned configuration of a multi-release JAR file.
  • Runtime.Version getVersion(): Return the maximum version used when searching for versioned entries. If this JarFile object doesn't represent a multi-release JAR file or isn't configured to be processed as such, the returned version object will be the same as the object returned from baseVersion().
  • boolean isMultiRelease(): Return true when this JAR file is a multi-release JAR file.

I've created an MRJI (Multi-Release JAR Information) application that demonstrates JarFile's new constructor and methods. Listing 4 presents this application's source code.

Listing 4. Obtaining information from a multi-release JAR file

import java.io.File;
import java.io.IOException;

import java.util.Enumeration;

import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class MRJI
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 2)
      {
         System.err.println("usage: java MRJI jarfile name");
         return;
      }

      JarFile jarFile = new JarFile(new File(args[0]), false, 
                                    JarFile.OPEN_READ);
      dumpBasicInfo(jarFile);
      dumpEntryInfo(jarFile, args[1]);

      jarFile = new JarFile(new File(args[0]), false, JarFile.OPEN_READ,
                            Runtime.Version.parse("9"));
      dumpBasicInfo(jarFile);
      dumpEntryInfo(jarFile, args[1]);
   }

   static void dumpBasicInfo(JarFile jarFile)
   {
      System.out.printf("Base version: %s%n", jarFile.baseVersion());
      System.out.printf("Runtime version: %s%n", jarFile.runtimeVersion());
      System.out.printf("Version: %s%n", jarFile.runtimeVersion());
      System.out.printf("Multi-release JAR file: %b%n", 
                        jarFile.isMultiRelease());
      System.out.println();
   }

   static void dumpEntryInfo(JarFile jarFile, String name)
   {
      Enumeration entries = jarFile.entries();
      while (entries.hasMoreElements())
         System.out.println(entries.nextElement());
      System.out.println();
      System.out.println(jarFile.getJarEntry(name).getTimeLocal());
      System.out.println();
   }
}
1 2 Page 1