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:
- Create
v1
andv2
subdirectories of the current directory. Copy Listings 1 and 3 tov1
and Listings 2 and 3 tov2
. - Assuming Java 8 is current, compile the source files in
v1
. Assuming Java 9 is current, compile the source files inv2
. - 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 thisJarFile
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 frombaseVersion()
.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();
}
}