Tips & Tricks

Creating Multi-Release JAR Files in IntelliJ IDEA

One of the interesting features of Java 9 is support for Multi-Release JAR Files.  What does this mean?  In the past, library developers had three options when it came to supporting newer versions of Java:

  1. Provide two (or more!) different JAR files, one for each version of Java they wanted to support. These might come with version numbers like “1.2-java-5” and “1.2-java-1.3”.
  2. Tie each one of your releases to a specific version of Java, forcing the users to either upgrade their version of Java or be stuck on an old version of the library.  E.g. “version 5.0 onwards requires Java 8”.
  3. Stick to the lowest common denominator release for users. For many library developers, this means they are still compiled against Java 6 and cannot move to using Java 8 features like lambdas and streams until practically all their users have moved to 8 already.

None of these approaches is particularly fun for the library developers or their users.  They either involve a lot of work, or they alienate/confuse users, or the libraries don’t get to make use of new features (and therefore don’t provide much incentive for their users to upgrade their version of Java either).

As of Java 9, there’s an alternative.  Now, a library developer can release a single JAR file that:

  • If you’re running it on Java 9, uses Java 9 features and functionality
  • If you’re running it on a version before Java 9, you get the pre-Java-9 implementation.

This works for Java 9 onwards – so these multi-release JAR files will support a Java 9 version, a Java 10 (or 18.3), 11, 12 version etc… but anything before Java 9 is lumped into “pre-Java 9”.  This is a little bit sad since obviously Java 8 had some nice features that you might want if you’re running Java 8, but the pre-Java-9 support for a library might target 6 onwards like a lot of libraries still do.  The reason for this split, of course, is that Java 8 itself can’t decide what to do differently when it runs a multi-release JAR file because the functionality only became available in Java 9.

In this blog post I’m going to show how to create a multi-release JAR file in IntelliJ IDEA.  I highly recommend that you do not use the IDE for creating production-ready builds of your application, I expect most people are using Maven, Gradle, Ant or some other build tool.  However, I wanted to experiment with multi-release JAR files and managed to build them using IntelliJ IDEA, and hope that showing this process helps people to understand how to build multi-release JAR files and how they work.

The Example

I’m going to create a really simple application that just outputs the current stack trace (you’ll see why I’ve chosen this example later).  My project consists of a Main class, an interface to define what I might be interested in about the stack, StackInfo, and an implementation of this interface, StackParser:

01BasicStructure

Project Structure

If you look at the specification, you’ll see that what you need is an output structure that looks something like this:

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

Basically, you have a standard JAR file with all the classes from your application in the root like usual, and an additional “versions” folder in META-INF with specific implementations for each of the additional supported versions of Java (in this case, just Java 9).  This “9” folder only needs to contain the class files for those classes with specific Java 9 features.  If a class is not in there (e.g. C.class) the default version will be used.

If I want parts of my application to be compiled against Java 9 and the “default” application to be compiled against Java 8, one way I can do this in IntelliJ IDEA is to set up a different IntelliJ IDEA module to contain just the Java 9 code:

02Java9Module-2

Java Version Settings & Dependencies

In my project structure, I’m going to set Java 8 as my default, since this is what I want the application to be compiled against in the normal case.

03Java8Default

If I look at the settings for my root project, I should see this is using the default SDK, Java 8.

04RootJava8

Now I need to go into my java9 module and make sure this is set to compile against JDK9.

05java9JDK9

I have also added a dependency upon the root module in this java9 module.  The reason for this is that the root project contains all the project code, whereas the java9 module only contains classes that need to be compiled against Java 9. These classes may need to refer to other classes in the application, so we’ll depend upon the root project to get access to these other classes.

Using Java 9 Features

Now I create a Java 9 implementation of the StackParser .

06Java9StackParser

Now you see why I chose this example – in Java 9 there’s a new StackWalking API which makes it much easier (and usually more efficient) to get information about the stack. The specification for Multi-Release JAR Files states “Every version of the library should offer the same API” – this means that really you should only use Java 9 for implementation details, not for providing a different API to the user.  The fact that you’re using Java 9 features will be invisible to the user.  Whether library developers will follow this or not remains to be seen.  But for our example, we’re going to have exactly the same API for both the Java 8 and Java 8 versions (both StackParser classes implement StackInfo ) but the Java 9 version uses Java 9 features in its implementation.

public class StackParser implements StackInfo {

    @Override
    public String getStackCount() {
        return "Java 9: " + StackWalker.getInstance()
                                       .walk(Stream::count);
    }

    @Override
    public String getStack() {
        return StackWalker.getInstance()
                          .walk(frames -> frames.map(Object::toString)
                                                .collect(joining("\n")));
    }
}

For comparison, our Java 8 version uses currentThread().getStackTrace() :

public class StackParser implements StackInfo {
    @Override
    public String getStackCount() {
        return "Java 8: " + Thread.currentThread()
                                  .getStackTrace().length;
    }

    @Override
    public String getStack() {
        return Arrays.stream(Thread.currentThread()
                                   .getStackTrace())
                     .map(element -> element.toString())
                     .collect(Collectors.joining("\n"));
    }
}

Note that these two classes are called the same thing and are in the same package.

Compiling

Remember that there’s a very specific structure to the classes in the JAR file, so we’re going to use this structure for our compiler output.  We ultimately want something that looks like:

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

Our root project can be compiled to pretty much wherever we like, as long as we know where that is.  I have my project set up like this:

07RootOutput

My root module compile output path is set to

[project home]/artifacts/classes/production/root

The java9 module needs special care:

08Java9Output

I have the java9 module output path set to

[project home]/artifacts/classes/production/root/META-INF/versions/9

Now when you build the whole project, everything should compile as you expect, and you should see the output in the artifacts directory:

09ProjectCompileOutput

When I open up the StackParser class in the root, I see it was compiled with Java 8:

10CompileWith8

And if I open the one in the 9 folder, I can see it was compiled with Java 9.

Creating the JAR file

Next, we’re going to tell IntelliJ IDEA how to assemble the JAR file.  In the artifacts section of the Project Structure dialog, we’re going to create a new artifact.  I click on the “+” at the top of the artifacts window and select JAR -> empty.

11NewJAR

I’m going to change the name to “multi-release”, then right-click on “root” in available elements and select Put Into Output Root.

12PutRootIntoOutput

I do the same thing with the java9 module as well. I’m going to change the destination folder of the JAR file because I’d prefer to have it in a different location, but that’s not important to the process (as long as you remember where it’s going to be output).  I set my output path to

[project home]/artifacts/jar

Then I click on multi-release.jar and press the “Create Manifest” button.

13CreateManifest

I’m going to select the root module as the location for this ([project home]/root), and IntelliJ IDEA creates a META-INF folder here with a MANIFEST.MF file.

Now I can press OK to save all these settings.

Next up, I go into the MANIFEST.MF file and make a couple of changes:

Manifest-Version: 1.0
Main-Class: com.mechanitis.demo.multi.Main
Multi-Release: true

This last line is the most important one.

Finally, from the Build menu, I choose Build Artifacts… and select “Build” under multi-release.jar. I should now see the JAR in my chosen output directory

14JarFileOutput

Running Under Different JVMs

Finally, let’s see this JAR file in action.  Firstly, I have a terminal set up to run Java 8.  When I run the jar file from here, I get the Java 8 implementation:

15RnWithJava8

Then in a second terminal set up with Java 9, running exactly the same JAR file gives me the Java 9 implementation.

15RnWithJava9

Summary

In this blog post we’ve looked at: what a multi-release JAR file is and why it might be useful; how to create an IntelliJ IDEA project that can have Java 8 and Java 9 implementations of the same functionality; how to use IntelliJ IDEA to create a multi-release JAR file; what happens when you run multi-release JAR files using different Java versions.

The code for this example is on GitHub.

More information on Multi-Release JAR Files:

image description