Building Java 6-8 Libraries for JPMS in Gradle
Find out how to use Gradle to build Java 6-8 libraries that support JPMS (Java Platform Module System) by providing Java 9 module-info.class
.
Introduction
If you need introduction to JPMS itself, check out this nice overview.
This post is primarily targeted at Java library maintainers.
Any such maintainer has to make a choice of which JDK to target:
- Targeting the newest JDKs (JDK 11, or just released JDK 12) provides the developers and users with access to new APIs and more functionality.
- However, it prevents the library to be used by all those users who are stuck on older JDKs.
- And those older JDKs are still very popular, taking ~95% share in 2018, and predicted to take ~90% in 2019. Especially the popularity of JDK 8 (> 80% share) makes it a de-facto standard for now.
So the latter is rightly a deciding factor for many library maintainers. For example, vavr 1.0 was intended to target JDK 11, but will ultimately target JDK 8.
Still, it’s advisable to add some support for JPMS in the hope that it will see wide adoption in future (I’d say 5+ years from now).Stephen Colebourne describes three options here:
- Do nothing (not recommended).
- Minimum: add an
Automatic-Module-Name
entry in yourMANIFEST.MF
file. - Optimum: add a
module-info.class
targeting JDK 9+ while providing all the remaining classes targeting JDK 6-8*.
Here, we’ll delve into how to achieve option 3 (the optimum).
* I write about JDK 6-8 (and not e.g. JDK 5-8) because, in JDK 11, javac
‘s --release
option is limited to range 6-11.
Justification
Before we delve into “how”, though, let’s skim over “why”.
Why is it worth bothering with JPMS at all? Primarily because JPMS:
- provides strong encapsulation,
- prevents introducing split packages,
- ensures faster class loading.
To sum up, JPMS is really cool (more here), and it’s in our best interest to encourage its adoption!
So I encourage the maintainers of Java 6-8 libraries to make the most of JPMS:
- for themselves, by compiling
module-info.java
against the JDK 6-8 classes of its module and against other modules, - for their users, by providing
module-info.class
for the library to work well on module-path.
Possible Behavior
Location of module-info.java
There are two places where module-info.java
can be located:
- with all the other classes, in
src/main/java
, - in a separate “source set”, e.g. in
src/main/java9
.
I prefer option 1, because it just seems more natural.
Location of module-info.class
There are two places where module-info.class
can end up:
- in the root output directory, with all the other classes,
- in
META-INF/versions/9
(Multi-Release JAR, AKA MRJAR)
Having read a post on MRJARs by Cédric Champeau, I’m rather suspicious of MRJARs, and so I prefer option 1.
Note, however, that Gunnar Morling reports having had some issues with option 1. On the other hand, I hope that 1.5 years from the release of JDK 9, all major libraries are already patched to properly handle module-info.class
.
Example Libraries per Build Tool
This section contains a few examples of libraries that provide module-info.class
while targeting JDK 6-8.
Ant
- Lombok (JDK 6 main + JDK 9
module-info.class
)
Maven
- ThreeTen-extra (JDK 8 main + JDK 9
module-info.class
) - Google Gson – not released yet (JDK 6 main + JDK 9
module-info.class
) - SLF4J – not released yet (JDK 6 main + JDK 9
module-info.class
inMETA-INF/versions/9
)
Note that Maven Compiler Plugin provides an example of how to provide such support.
Gradle
I haven’t found any popular libraries that provide such support using Gradle (please comment if you know any). I only know of vavr trying to do this (#2230).
Existing Approaches in Gradle
ModiTect
ModiTect (by Gunnar Morling) and its Gradle plugin (by Serban Iordache) have some really cool features. In essence, ModiTect generates module-info.class
without the use of javac
, based on a special notation or directly from module-info.java
.
However, in case of direct generation from module-info.java
, ModiTect effectively duplicates what javac
does while introducing issues of its own (e.g. #90). That’s why I feel it’s not the best tool here.
Badass Jar plugin
Serban Iordache also created a Gradle plugin that lets one “seamlessly create modular jars that target a Java release before 9”.
It looks quite nice, however:
- in order to build the proper JAR and validate
module-info.java
, the Gradle build has to be run twice, - it doesn’t use
javac
‘s--release
option, which guarantees that only the right APIs are referenced, - it doesn’t use
javac
to compilemodule-info.java
.
Again, I feel it’s not the right tool here.
JpmsGradlePlugin
This is my most recent find: JpmsGradlePlugin by Axel Howind.
The plugin does some nice things (e.g. excluding module-info.java
from javadoc
task), however:
- it too doesn’t use
javac
‘s--release
option, - it doesn’t support Java modularity fully (e.g. module patching),
- it doesn’t feel mature enough (code hard to follow, non-standard behavior like calling
javac
directly).
Proposed Approach in Gradle
Gradle Script
Initially, I wanted to do this by adding a custom source set. However, it turned out that such an approach would introduce unnecessary configurations and tasks, while what we really need is only one extra task, “hooked” properly into the build lifecycle.
As a result, I came up with the following:
- Configure
compileJava
to:- exclude
module-info.java
, - use
--release 6/7/8
option.
- exclude
- Add a new
JavaCompile
task namedcompileModuleInfoJava
and configure it to:- include only
module-info.java
, - use
--release 9
option, - use the classpath of
compileJava
as--module-path
*, - use the destination directory of
compileJava
*, - depend on
compileJava
*.
- include only
- Configure
classes
task to depend oncompileModuleInfoJava
.
The above, expressed as a Gradle script in Groovy DSL, can be found in this Stack Overflow answer of mine.
* These three steps are necessary for compileModuleInfoJava
to see classes compiled by compileJava
. Otherwise, javac
wouldn’t be able to compile module-info.java
due to unresolved references. Note that in such configuration, every class is compiled just once (unlike with the recommended Maven Compiler Plugin configuration).
Unfortunately, such configuration:
- is not easily reusable across repositories,
- doesn’t support Java modularity fully.
Gradle Modules Plugin
Finally, there’s a plugin (Gradle Modules Plugin) that adds full support for JPMS to Gradle (created by the authors of Java 9 Modularity, Sander Mak and Paul Bekker).
This plugin only lacks support for the scenario described in this post. Therefore, I decided to:
- file a feature request with this plugin: #72
- provide a Pull Request with a complete implementation of #72 (as a “proof of concept”): #73
I tried hard to make these high-quality contributions. The initial feedback was very welcome (even Mark Reinhold liked this!). Thank you!
Now, I’m patiently waiting for further feedback (and potential improvement requests) before the PR can be (hopefully) merged.
Summary
In this post, I’ve shown how to build Java 6-8 libraries with Gradle so that module-info.java
is compiled to JDK 9 format (JPMS support), while all the other classes are compiled to JDK 6-8 format.
I’ve also recommended to use Gradle Modules Plugin for such configuration (as soon as my PR gets merged and a new plugin version gets released).
Published on Java Code Geeks with permission by Tomasz Linkowski, partner at our JCG program. See the original article here: Building Java 6-8 Libraries for JPMS in Gradle Opinions expressed by Java Code Geeks contributors are their own. |
Gradle Modules Plugin v1.5.0, which includes my PR, has been released! 😃
https://github.com/java9-modularity/gradle-modules-plugin/releases/tag/v1.5.0