The JEP-238 Multi-Release JAR feature, introduced with Java 9, allows a single JAR to contain multiple implementations that work with different versions of the JDK. As noted in my last post, This is especially important given the pace of changes to the JDK. Unfortunately, neither IDEs nor build tools are currently providing much in the way of support.
The actual construction of the JAR is straightforward. Classes can be added under a path, indicating to the run time system when they should replace classes in the main JAR. If, for example, there is a class named com.meterware.simplestub.ClassUtils
, it is found normally in the jar at com/meterware/simplestub/ClassUtils.class
. If another version of it exists at META-INF/versions/9/com/meterware/simplestub/ClassUtils.class
, that latter version can be used instead when the JAR is run in Java 9 or later, but will be ignored under Java 8 or earlier. The two implementations can be built under different versions of Java.
This, of course, violates a number of long-established conventions of the build process. Each JAR is usually built from a single source tree, possibly including some code generated during the build, and generally is built by a single compiler. Class names are assumed to be unique. MR JARs require something a bit different.
It seems that it isn’t trivial to build them. Tools have long held the assumption that a single version of javac should suffice, and that each jar should ideally be built from a single source tree (plus generated code). These are very good assumptions, which have rationalized builds for a great many Java projects, but MR Jars call them into question.
To get around this, a number of approaches have been developed:
- A multi-module approach. This uses multiple modules: a parent POM, a base module, a module for each JDK version, and an assembly module which puts the whole thing together. It is very clean, in terms of the individual POMs, and will work well with IDEs. It seems incredibly clumsy, however, and would be problematic to scale, especially in a project that is already producing multiple modules, and doesn’t support running a single set of unit tests against each JDK version, although one could create separate tests and somehow keep them in sync.
- A somewhat simplified version of the above, which adds just one additional module for the later JDK, and the copies those classes back into the original project. To accomplish this, however, it must build the main project, then build the second project against it, and finally rebuild the original project to pull in the additional classes.
- Use of the ant plugin to compile the JDK 9 code. This is much simpler, allowing all of the code to be in a single project. In this approach, the JDK 9 code is treated very much as a special case. You cannot, for example, run unit tests against it; they are only run against the main code. Further, it counts on being run under Java 9, in order that when it compiles the additional code with the ant plugin, the Java 9 version will be used. As long as the main code can be compiled with Java 9, this can work. It becomes a problem, however, when the main code uses an API which has been removed in later versions. Still, it is reasonably clean and simple, and I have used it in the pfl-basic module of the PFL library.
- A promising approach that uses a build extension. This extension actually detects the presence of additional source directories, and compiles them with the appropriate –release switch. It follows very much in the Maven philosophy of convention over configuration. It lacks customizations for unit testing, but in principle it should be possible to add them.
I should note a basic problem with the --release
switch, supported by Java 9 and later. It is intended to simplify things by allowing a single compiler to build against multiple public APIs; unfortunately, there are some edge cases that are not supported, including access to the jdk.unsupported
module and its equivalent in earlier releases. Programs that use them including SimpleStub, need to use the actual compilers for each version, as they will simply not compile, otherwise.
One thing that all of these approaches lack is the ability to run unit tests against the additional classes. Since I depend heavily on unit tests to verify my code, I find that a serious handicap. As it happens, though, I have found an approach which appears to work, and which I will describe in my next blog post.