Press "Enter" to skip to content

Writing JDK8-Compatible Libraries with Java Platform Module System Support

If you maintain a Java library, you’ve probably felt the disconnect between supporting Java 8 and providing support for the Java Platform Module System.

A common trade-off is adding an Automatic-Module-Name entry to a manifest. While convenient, it’s inferior to a proper JPMS setup (automatic modules are not supported by jlink).

The good news is that you can have both with a bit of extra work.

Why not Automatic-Module-Name?

We can add:

Automatic-Module-Name: com.example.mylib

to our manifest. That helps module naming, but automatic modules are inferior to standard ones.

Automatic modules:

  • are not supported by jlink
  • don’t give the same control and guarantees as a real module descriptor
  • derive exports and readability automatically (can be unstable if your dependencies change)

A real module-info.class is the clean solution.

One Jar, Two Worlds

The key enabler is the Multi-Release JAR format (JEP 238), introduced in Java 9. A Multi-Release JAR can contain version-specific class files in META-INF/versions/{version}/.

In Java 8, the JVM simply ignores the directory and uses the root classes. On Java 9+, it picks up the version-specific entries – including module-info.class.

This means we can compile our library with Java 8, then inject a module-info.class under META-INF/versions/9/. Such a JAR works as a plain library on Java 8 and as a proper named module on Java 9+.

Moditect

The Moditect Maven plugin lets you add a module-info file to your JAR without requiring the project to compile with Java 9+. It compiles the module descriptor separately and injects it into the JAR as a Multi-Release entry.

Let’s say we have a simple utility library compiled with Java 8:

package com.pivovarit.utils;

public final class StringUtils {

    private StringUtils() {
    }

    public static String reverse(String input) {
        return input == null ? null : new StringBuilder(input).reverse().toString();
    }

    public static boolean isPalindrome(String input) {
        if (input == null) {
            return false;
        }
        String reversed = reverse(input);
        return input.equalsIgnoreCase(reversed);
    }
}

The Maven build uses the maven-compiler-plugin targeting Java 8, and the moditect-maven-plugin to inject a module descriptor:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.14.0</version>
            <configuration>
                <release>8</release>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.moditect</groupId>
            <artifactId>moditect-maven-plugin</artifactId>
            <version>1.2.2.Final</version>
            <executions>
                <execution>
                    <id>add-module-infos</id>
                    <phase>package</phase>
                    <goals>
                        <goal>add-module-info</goal>
                    </goals>
                    <configuration>
                        <jvmVersion>9</jvmVersion>
                        <module>
                            <moduleInfoSource>
                                module com.pivovarit.utils {
                                    exports com.pivovarit.utils;
                                }
                            </moduleInfoSource>
                        </module>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
        </plugin>
    </plugins>
</build>

The <jvmVersion>9</jvmVersion> setting is what makes the magic happen. It tells Moditect to place the compiled module-info.class under META-INF/versions/9/ instead of the JAR root, producing a Multi-Release JAR.

After building, the resulting JAR has the following structure:

And the manifest contains:

Multi-Release: true

On Java 8, the JVM sees a regular JAR with StringUtils.class. On Java 9+, the JVM recognizes the Multi-Release marker and picks up module-info.class, making the library a proper named module (com.pivovarit.utils) with explicitly declared exports.

The only downside is that we don’t get dedicated IDE support because our module-info is effectively just a string in the Maven plugin configuration.

This is precisely how modularity support for Vavr was delivered in 1.0.0.

Summary

In this article, we saw that supporting Java 8 and JPMS doesn’t have to be a compromise.

By combining:

  • Java 8 compilation
  • Moditect
  • Multi-Release JAR packaging

We managed to ship a single polymorphic jar with a no-compromise Java Platform Module System support.

The above example is available on GitHub.




If you enjoyed the content, consider supporting the site: