JEP 238: Multi-Release JAR Files

OwnerPaul Sandoz
Created2014/06/18 22:29
Updated2016/06/23 18:11
TypeFeature
StatusTargeted
Componenttools / jar
ScopeSE
Discussioncore dash libs dash dev at openjdk dot java dot net
EffortM
DurationM
Priority2
Reviewed byAlan Bateman, Brian Goetz, Paul Sandoz, Steve Drach
Endorsed byBrian Goetz
Release9
Issue8047305

Summary

Extend the JAR file format to allow multiple, Java-release-specific versions of class files to coexist in a single archive.

Goals

  1. Enhance the Java Archive Tool (jar) so that it can create multi-release JAR files.

  2. Implement multi-release JAR files in the JRE, including support in the standard class loaders and JarFile API.

  3. Enhance other critical tools (e.g., javac, javap, jdeps, etc.) to interpret multi-release JAR files.

  4. Support multi-release modular JAR files for goals 1 to 3.

  5. Preserve performance: The performance of tools and components that use multi-release JAR files must not be significantly impacted. In particular, performance when accessing ordinary (i.e., not multi-release) JAR files must not be degraded.

Motivation

Third party libraries and frameworks typically support a range of Java platform versions, generally going several versions back. As a consequence they often do not take advantage of language or API features available in newer releases since it is difficult to express conditional platform dependencies, which generally involves reflection, or to distribute different library artifacts for different platform versions.

This creates a disincentive for libraries and frameworks to use new features, that in turn creates a disincentive for users to upgrade to new JDK versions---a vicious circle that impedes adoption, to everyone's detriment.

Some libraries and frameworks, furthermore, use internal APIs of the JDK that will be made inaccessible in Java 9 when module boundaries are strictly enforced. This also creates a disincentive to support new platform versions when there are public, supported API replacements for such internal APIs.

Description

A JAR file has a content root, which contains classes and resources, as well as a META-INF directory which contains metadata about the JAR. By adding some versioning metadata to specific groups of files the JAR format can encode, in a compatible way, multiple versions of a library for different target Java platform releases.

A multi-release JAR ("MRJAR") will contain the main attribute:

Multi-Release: true

declared in the main section of the JAR MANIFEST.MF. The attribute name is also declared as a constant java.util.jar.Attributes.MULTI_RELEASE. Like other main attributes the name declared in the MANIFEST.MF is case insensitive. The value is also case-insensitive, but there must be no preceding or trailing white space (such a restriction helps ensure the performance goal is met).

A multi-release JAR ("MRJAR") will contain additional directories for classes and resources specific to particular Java platform releases. A JAR for a typical library might look like this:

jar root
  - A.class
  - B.class
  - C.class
  - D.class

Suppose there are alternate versions of A and B that can take advantage of Java 8 features, and yet another version of A that can take advantage of Java 9 features. We can bundle them into a single JAR as follows:

jar root
  - A.class
  - B.class
  - C.class
  - D.class
  - META-INF
     - versions
        - 8
           - A.class
           - B.class
        - 9
           - A.class

In a JDK that does not support MRJARs, only the classes and resources in the root directory will be visible, and the two packagings will be indistinguishable. In a JDK that does support MRJARs, the directories corresponding to any later Java platform release would be ignored; it would search for classes and resources first in the Java platform-specific directory corresponding to the currently-running major Java platform release version, then search those for lower versions, and finally the JAR root. On a Java 9 JDK, it would be as if there were a JAR-specific class path containing first the version 9 files, then the version 8 files, and then the JAR root; on a Java 8 JDK, this class path would contain only the version 8 files and the JAR root.

By this scheme, it is possible for versions of a class designed for a later Java platform release to override the version of that same class designed for an earlier Java platform release. In the example above, when running on an MRJAR-aware Java 8 JDK, it would see the 8-specific versions of A and B and the general versions of C and D; on an MRJAR-aware Java 9 JDK, it would see the 9-specific version of A and the 8-specific version of B; on older or non-MRJAR-aware JDKs it would only see the base versions of all.

JAR metadata, such as that found in the MANIFEST.MF file and the META-INF/services directory, need not be versioned. An MRJAR is essentially one unit of release, so it has just one release version (which is no different from a normal JAR, distributed say via Maven Central), even though internally it contains multiple versions of a library implementation for use on different Java platform releases. Every version of the library should offer the same API; investigation is required to determine whether this should be strict backwards compatibility where the API is exactly the same (byte code signature equality), or whether this can be relaxed to some degree without necessarily enabling the introduction of new enhancements that would blur the notion of one unit of release. This may imply, at a minimum, that a public class present in a release-specific directory should also be present in the root, though it need not be present in an earlier release directory. The run-time system will not verify this property, but tooling can and should detect such API compatibility issues, and a library method may also be provided to perform such varification (for example on java.util.jar.JarFile).

Ultimately, this mechanism enables library and framework 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, breaking the chicken-and-egg cycle so that a library can be "Java 9-ready" without actually requiring Java 9.

Details

The following components of the JDK will be modified in order to support multi-release JAR files.

Compatibility

By default the behaviour of java.util.jar.JarFile and the jar scheme protocol handlers will remain the same. It is necessary to opt-in to construct a JarFile pointing to a MRJAR for version selection of entries. Likewise it is necessary to opt-in for jar URLs (see next section for details).

JarFile instances created by the runtime for class loading will opt-in and create instances that are configured to select entries according to the version of the running Java platform. Such as JarFile instance is referred to as being runtime versioned.

Class loader resources

A resource URL, produced by a class loader, identifying a resource in a MRJAR will refer directly to a versioned entry (if present). For example for a versioned resource, foo/baz/resource.txt:

URL r = loader.getResource("foo/baz/resource.txt");

the URL ‘r’ may be:

jar:file:/mrjar.jar!/META-INF/versions/9/foo/baz/resource.txt

rather than:

jar:file:/mrjar.jar!/foo/baz/resource.txt

This approach is considered the least disruptive option. Changing the structure of resources URLs is not without risk (e.g. a new scheme or an appended fragment). Legacy code may process URL characters directly, rather than parsing the URL and correctly extracting the components. While such URL process is incorrect it was considered preferable to not breaking such code.

Alternatives

A common approach is to use a static reflective check to determine if an API feature is present or not and accordingly select an appropriate class that respectively depends on that feature or not. The reflective cost is incurred at class initialization and not every time the dependent feature is used. A Java platform release is selected for compilation with the source and target flags set to a lower release to generate class files compatible with that lower release. This approach is often augmented with tools such as Animal Sniffer to check for API incompatibilities, where in addition to enforcing API compatibility code can be annotated to state whether it depends on a later Java platform release. There are a number of limitations with this approach:

  1. The reflective checks need to be carefully maintained.

  2. It is not possible to utilize newer language features.

  3. If a platform release API feature is removed (perhaps an internal API) then dependent code will fail to compile.

"Fat" class files were considered, where a class may have one or more methods targeted to different Java platform versions. This was deemed too complicated in terms of the language and runtime features required to support such method declarations and dynamic selection.

Method handles (invokedynamic) cannot be used because of the need to maintain binary compatibility.

Risks and Assumptions

It is anticipated that the production of MRJARs is primarily compatible with existing popular build tools and therefore IDEs that support such tools, but the developer experience could be improved with enhancements.

The source layout and building of an MRJAR file can be supported by Maven using a multi-module project. For example, see this example Maven project that can produce a, currently rudimentary, MRJAR file. There would be a sub-project for the root and specific Java platform releases, and a sub-project to assemble the aforementioned sub-projects into an MVJAR. The assembly process could be enhanced, perhaps using a specific Maven plugin, leveraging the same features as the jar tool to enforce backwards compatibility.

The design and implementation of the runtime processing of MRJARs currently assumes that a runtime uses the URL class loader or a custom class loader leverages JarFile to obtain platform-specific class files. Runtimes whose class loaders use ZipFile to load classes will not be MRJAR aware. Popular application frameworks and tools, such as Jetty, Tomcat, and Maven, etc., need to be checked for compatibility.

Dependences

The enhanced JAR-file format under consideration for the Java Platform Module System will need to take multi-release JAR metadata into account.

JEP 247 (Compile for Older Platform Versions), which supports compiling against older versions of the platform libraries, may aid build tools in the production of multi-release JAR files.