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 .

EDIT (May 4, 2019): TL;DR → Use Gradle Modules Plugin for that.

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 your MANIFEST.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:

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,

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 in META-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

EDIT (May 4, 2019): Currently, Gradle Modules Plugin supports precisely what I needed.

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,

, 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,

‘s option, which guarantees that only the right APIs are referenced, it doesn’t use javac to compile module-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,

‘s 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. Add a new JavaCompile task named compileModuleInfoJava and configure it to: include only module-info.java ,

, use --release 9 option,

option, use the classpath of compileJava as --module-path * ,

as , use the destination directory of compileJava * ,

, depend on compileJava *. Configure classes task to depend on compileModuleInfoJava .

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 Bakker).

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).

EDIT (Apr 4, 2019): Gradle Modules Plugin v1.5.0, which includes my PR, has been released! 😃