When migrating applications, we had to deal with the scenario that the dependent libraries might not all be migrated to Java 9. When dealing with libraries, you'll need to tackle the opposite problem. The applications consuming your library may not all be Java 9. You'll have to support Java 8 (or perhaps even older versions of Java in some cases). How do you, as a library author, create library distributions for all those versions? Before Java 9, you used to have two options:
- You could create separate JARs for each Java version
- In your library code, you could use reflection to do a feature check. For example, you could reflectively access a platform API that was introduced in Java 8. If it works, you are in Java 8. If not, drop down to Java 7, and so on.
Both these options are tedious. There is a new alternative with Java 9, with a feature called multi-release JARs. The concept is simple. You create a special JAR file called a multi-release JAR that contains classes for all versions of Java you are targeting.
Here's how it works. Multi-release JARs have a special structure that holds the classes within it:
Here's what you'll find in a multi-release JAR file, corresponding to the numbering in the diagram:
- There's a root META-INF folder with a MANIFEST.MF file that contains the following line:
Multi-Release: true
This tells the platform that this is a multi-release JAR and thus needs to be treated differently
- The JAR root also contains a default version of the compiled classes, just like any other JAR. Remember, this JAR targets multiple Java versions and it could hold multiple target versions of the same class. The classes at the root folder are the default base versions that could potentially apply to multiple Java versions
- There's a folder called versions inside META-INF. To target multiple runtimes, the JAR packages classes into sub-folders here. There's one folder for each Java version you want to target. Each such folder contains classes that have been specifically compiled for that release version. So, if the JAR is used in that version of the Java platform, the classes in the version folder override the classes in the multirelease folder and are picked up instead. If the JAR is used in a platform version that does not have classes in the META-INF folder, or the class needed doesn't exist in the version folder, the runtime falls back to the contents of the multirelease folder.
Notice that the default versions of the classes are in the root location in the JAR file. This is why you can use the JAR file with older versions of Java too. To older Java versions, a multi-release JAR file looks just like an ordinary JAR file--the root location is all the platform looks at, and the versions folder is ignored!
Let's try creating a simple multi-release JAR. The 11-migrating-application/04-multirelease-jars folder contains an extremely simple library. It's called mylib and it has a class with a method that prints the contents of a list passed to it.
We'd like to create a multi-release JAR for this library targeting two different versions of Java:
- The base version of the library targets all pre-Java 9 versions. It contains code that performs a for loop and prints the contents of the list as follows:
public class PrintList { public void print(List<?> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } }
- The Java 9 specific version of this library has two changes--it declares itself as a Java 9 module with module-info.java and it uses forEach and a function reference to print the contents of the list, as follows:
public class PrintList { public void print(List<?> list) { list.forEach(System.out::println); } }
The two versions of the library are in two separate folders. Since there will be two separate versions of the same class, it helps to separate them this way.
Here's the structure of the code:
The first step to making a multi-release JAR is to add the MANIFEST.MF file that declares it. Add this file at the root of the project with a single line, shown next. Make sure you match the statement exactly without any extra spaces:
Multi-Release: true
Now, we'll create the folders that hold the compiled classes. We'll create a folder called out and have two subfolders--base for the base classes and 9 for the Java 9 version, as shown here:
$ mkdir out
$ mkdir out/base
$ mkdir out/9
Next, we will compile the classes into these two folders by setting the right release versions. The --release parameter to the javac command lets you target specific Java versions for your compiled classes:
$ javac --release 7 -d out/base base/src/packt/mylib/PrintList.java
The preceding command compiles the PrintList.java class with target release 7, and places the complied output in the out/base directory.
Next, we'll compile the Java 9 version as follows:
$ javac --release 9 -d out/9 java9/src/packt.mylib/module-info.java java9/src/packt.mylib/packt/mylib/PrintList.java
There are two Java files this time--PrintList.java and module-info.java. The complied classes go to the out/9 directory.
Now that we have the compiled classes, it's time to create a multi-release JAR. Let's first create a JAR file with the base version classes. We also supply the MANIFEST.MF file to be included in the JAR:
$ jar -cf mylib.jar MANIFEST.MF -C out/base .
The -c option tells the jar tool to create a new JAR, and f option is used to specify the JAR file name (here, mylib.jar). The -C option changes the directory the tool is looking for to out.base and lets it compile classes there (as specified by ".").
This creates the JAR file and adds the base classes to it. Next, let's add the Java 9 classes:
$ jar -uf mylib.jar --release 9 -C out/9 .
The -u options tells the jar tool to update the JAR rather than create one. We are targeting release 9 this time, and including compiled classes in the out/9 directory.
Here are the contents of the JAR file that's generated. This is the structure we have already seen: