"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
@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;
}
...
}
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;
}
}
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;
}
}
<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>
Manifest-Version: 1.0
...
Multi-Release: true
...
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". ↩
Top comments (0)