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.mylibto 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: trueOn 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.



