It’s never easy to decide which Java version to require for your project. On the one hand, you want to give users the freedom of choice, so it would be nice to support several major versions, not just the newest one. On the other hand, you’re dying to use the newest language features and APIs. From Java 9 on, there’s a new JVM feature, multi-release JARs (MR-JARs), that helps you reconcile these opposing forces—at least, under some circumstances.
MR-JARs allow you to ship bytecode for different Java versions in the same artifact. You can then rely on the JVM to load the classes that you compiled for the most recent version it supports. Starting with a project that runs successfully on your minimally required version, you can selectively improve it on newer JVMs by using more resilient and performant APIs—without being forced to raise your project’s baseline.
With all of that out of the way, let’s explore this handy new feature. We’ll start with creating a simple MR-JAR before looking at how it’s structured internally. We’ll end with some recommendations for when and how to use MR-JARs.
To prepare for an MR-JAR, you need to split source files by the Java version they target, compile each set of sources for the corresponding version, and place the resulting .class
files into separate folders. When packaging them with jar
, you add the baseline class files as usual (directly or with -C
; check section 4.5.1) and use the new option --release ${release}
for each other bytecode set.
Let’s look at an example. Say you need to detect the currently running JVM’s major version. Java 9 offers a nice API for that, so you no longer have to parse a system property. (Section 6.5.1 gives a glimpse of it, but the details aren’t important here.) By deploying an MR-JAR, you can use that API if you’re running on Java 9 or later.
The hypothetical app has two classes, Main
and DetectVersion
; and the goal is to have two variants of DetectVersion
, one for Java 8 and earlier and another for Java 9 and later. These two variants need to have the exact same fully qualified name (which can make it challenging to work with them in your IDE)—assume you place them into two parallel source folders, src/main/java
and src/main/java-9
.
Figure E.1 shows how to organize the sources, and listing E.1 shows how to compile and package them into an MR-JAR. Note the two compilation steps and the separate output folders. The end result is shown in figure E.2.
javac --release 8 ①
-d classes
src/main/java/org/codefx/detect/*.java
javac --release 9 ②
-d classes-9
src/main/java-9/module-info.java
src/main/java-9/org/codefx/detect/DetectVersion.java
jar --create ③
--file target/detect.jar ③
-C classes . ③
--release 9 ④
-C classes-9 . ④
This simple example creates two variants of DetectVersion
, one for the minimally required Java 8 and another for Java 9. Formalizing that to the general case of creating a feature with several classes for several versions is surprisingly complex and tedious, so I’ll spare you the formal version. Instead, section E.3 leaves you with a rule of thumb.
How does an MR-JAR work? It’s pretty straightforward: it stores version-unspecific class files in its root (as usual) and version-specific files in META-INF/versions/${version}
.
In addition to the folders in META-INF/versions
, an MR-JAR can also be recognized by looking at the plaintext file META-INF/MANIFEST.MF
: in MR-JARs, the manifest has an entry Multi-Release: true
.
Now that you know how to create MR-JARs and how they work, I want to give you some recommendations for how to make the most out of them. More precisely, I’ll give you tips on these topics:
src/main/java
, not src/main/java-X
.src/main/java-X
. (Note that if you’re offering a feature that only works on a newer Java version, having a class that only throws errors stating “Operation not supported before Java X”
counts as complete. My recommendation is to not leave it out, leading to an uninformative NoClassDefFoundError
.)These aren’t technical requirements; nothing stops you from targeting Java 11 and putting half of the code in src/main/java
and the other half, or even all of it, in src/main/java-11
. But that will only cause confusion.
By sticking to the guidelines, you keep the source tree’s layout as simple as possible. Any human or tool looking into it sees a fully functioning project that targets the required JVM version. Version-dependent source trees then selectively enhance that code for newer versions.
How do you verify whether you got it right? As I said earlier, a formal description is complex, so here’s that rule of thumb I promised. To determine whether your particular layout works, mentally (or actually) undertake the following steps:
If that works, you got it right.
Of course, your tools also have to work with the source layout you choose. Unfortunately, at the time of writing, IDEs and most build tools don’t have good support for this layout, and you might be forced to compromise. As an alternative solution, consider creating separate projects for each Java version.
--release
.META-INF/versions
.Once again, these aren’t technical requirements, but they guarantee that everybody looking into the JAR’s root sees a fully functioning project compiled for the required JVM version with selective enhancements for newer JVMs in META-INF/versions
.
How do MR-JARs help you solve the dilemma of picking the required Java version? First, and to state the obvious, preparing a MR-JAR adds quite a bit of complexity:
Also, MR-JARs aren’t a good fit for using convenient new language features. As you’ve seen, you need two variants of the involved source files, and there’s no good argument on the basis of convenience if you have to keep a source file with the inconvenient variant. Language features will also quickly pervade a code base, leading to a lot of duplicate classes. This isn’t a good idea.
APIs, on the other hand, are the sweet spot for MR-JARs. Java 9 introduced a number of new APIs that solve existing use cases with more resilience and/or better performance:
Runtime.Version
instead of parsing system properties (see section 6.5.1)Throwable
(this book doesn’t cover that API, but developers of your logging framework are already using it)If you want to use a newer API on a newer Java release, all you need to do is encapsulate your direct calls to it in a dedicated wrapper class and then implement two variants of it: one using the old API, another using the new one. If you’ve accepted the complexities outlined before, then this is straightforward.
3.144.93.141