Java Multi-Release JARs

“Adding Support to Java InvocationHandler Implementations for Interface Default Methods” describes how to implement an InvocationHandler to invoke default interface methods. This mechanism is critical to the FluentNode implementation described in “Java Interface Facades”. The first article also notes that the MethodHandles.Lookup method used with Java 8 would not work with Java 9 which means the whole API will not work on Java 9 and subsequent JVMs.

This article describes the Java 9-specific solution, refactoring the InvocationHandler implementation to separate and compartmentalize the Java 8 and Java 9-specific solution logic, and introduces “JEP 238: Multi-Release JAR Files” to deliver a Java 8 and Java 9 (and later) solutions simultaneously in the same JAR.

Theory of Operation

As described in JEP 238, multi-release JARs provide a means to provide alternate versions of classes that can take advantage of specific platform features. Alternate classes are stored in the JAR within the hierarchy described by /META-INF/versions/${java.specification.version}/. (The implementation hierarchy is shown in detail at the end of this article.) For archivers and class-loaders that are not multi-release aware (e.g., Java 8), these additional classes are ignore. However, for Java 9 and subsequent environments, these additional classes are loaded if the version is less than or equal to the JVM’s ${java.specification.version} (with the latest taking precedence).

The class with the Java 8-specific code will be refactored to implement a super-interface with a single method embodying that code. That existing code base will be compiled as-is to create a JAR suitable for Java 8 JVMs. In addition, a Java 9 version of the super-interface will be compiled for a Java 9 environment and saved to the JAR beneath the /META-INF/versions/9/ hierarchy.

Implementation

DefaultInvocationHandler.invoke(Object,Method,Object[]) is re-factored to implement DefaultInterfaceMethodInvocationHandler:

DefaultInvocationHandler
@NoArgsConstructor @ToString
public class DefaultInvocationHandler implements DefaultInterfaceMethodInvocationHandler {
    ...
    @Override
    public Object invoke(Object proxy, Method method, Object[] argv) throws Throwable {
        Object result = null;
        Class<?> declarer = method.getDeclaringClass();

        if (method.isDefault()) {
            result = DefaultInterfaceMethodInvocationHandler.super.invoke(proxy, method, argv);
        } else if (declarer.equals(Object.class)) {
            result = method.invoke(this, argv);
        } else {
            result = invokeMethod(this, true, method.getName(), argv, method.getParameterTypes());
        }

        return result;
    }
    ...
}

With the Java 8 implementation:

DefaultInterfaceMethodInvocationHandler (Java 8)
public interface DefaultInterfaceMethodInvocationHandler extends InvocationHandler {
    @Override
    default Object invoke(Object proxy, Method method, Object[] argv) throws Throwable {
        Constructor<MethodHandles.Lookup> constructor =
            MethodHandles.Lookup.class.getDeclaredConstructor(Class.class);

        constructor.setAccessible(true);

        Class<?> declarer = method.getDeclaringClass();
        Object result =
            constructor.newInstance(declarer)
            .in(declarer)
            .unreflectSpecial(method, declarer)
            .bindTo(proxy)
            .invokeWithArguments(argv);

        return result;
    }
}

While the Java 9 implementation is:

DefaultInterfaceMethodInvocationHandler (Java 9)
public interface DefaultInterfaceMethodInvocationHandler extends InvocationHandler {
    @Override
    default Object invoke(Object proxy, Method method, Object[] argv) throws Throwable {
        Class<?> declarer = method.getDeclaringClass();
        Object result =
            MethodHandles.lookup()
            .findSpecial(declarer, method.getName(),
                         methodType(method.getReturnType(), method.getParameterTypes()), declarer)
            .bindTo(proxy)
            .invokeWithArguments(argv);

        return result;
    }
}

The Java 9 alternative is saved to ${basedir}/src/main/java9/ball/lang/reflect/DefaultInterfaceMethodInvocationHandler.java within the Maven project. Compiling with the maven-compiler-plugin is straightforward as the following profile demonstrates the incremental configuration.1

Parent POM: Java 9 Profile
<project ...>
  ...
  <profiles>
  ...
    <profile>
      <id>[+] src/main/java9</id>
      <activation>
        <file><exists>${basedir}/src/main/java9</exists></file>
      </activation>
      <build>
        <pluginManagement>
          <plugins>
            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-compiler-plugin</artifactId>
              <executions>
                <execution>
                  <id>jdk9</id>
                  <goals>
                    <goal>compile</goal>
                  </goals>
                  <configuration>
                    <release>9</release>
                    <compileSourceRoots>
                      <compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
                    </compileSourceRoots>
                    <multiReleaseOutput>true</multiReleaseOutput>
                  </configuration>
                </execution>
              </executions>
            </plugin>
          </plugins>
        </pluginManagement>
      </build>
    </profile>
  ...
  </profiles>
  ...
</project>

To complete the JAR’s configuration, the Multi-Release flag must be set in the JAR’s manifest.

META-INF/MANIFEST.MF
Manifest-Version: 1.0
...
Multi-Release: true
...

The resulting JAR hierarchy is depicted below.

Muti-Release JAR Hierarchy
ball-util.jar
├── META-INF
│   ├── MANIFEST.MF
│   ├── ...
│   └── versions
│       └── 9
│           └── ball
│               └── lang
│                   └── reflect
│                       └── DefaultInterfaceMethodInvocationHandler.class
└── ball
    ├── ...
    ├── lang
    │   ├── ...
    │   └── reflect
    │       ├── DefaultInterfaceMethodInvocationHandler.class
    │       ├── DefaultInvocationHandler.class
    │       └── ...
    ├── ...
    ...

Summary

Multi-release JARs provide a solution for a single JAR to support a range of Java platform versions. Developers should be aware that the solution does present some challenges: The compiled versioned class files cannot be run outside the JAR (making testing difficult) and class-loader implementation details will effect resource discovery and loading2 (to name a few). However, as illustrated with this use-case, it is a powerful tool for creating JARs that support a wide range of Java platforms.

[1] The Parent POM has similar profiles for Java 10 through 14.

[2] Please see the discussion in “JEP 238: Multi-Release JAR Files”.