“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;
}
...
}
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
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>
Enter fullscreen mode Exit fullscreen mode
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
...
Enter fullscreen mode Exit fullscreen mode
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
│ └── ...
├── ...
...
Enter fullscreen mode Exit fullscreen mode
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”. ↩
暂无评论内容