I am trying to dynamically create a lambda instance using classes that are not available at compile-time as they are generated during runtime or not known yet at compile time.
This works using the following code
// lambdaClass = class of the lambda interface
// className = class containing the target method
// methodName = name of the target method
private static <T> T lookupLambda(Class<T> lambdaClass, String className, String methodName, Class<?> returnType,
Class<?> argumentType) throws Throwable {
MethodType lambdaType = MethodType.methodType(lambdaClass);
MethodType methodType = MethodType.methodType(returnType, argumentType);
MethodHandles.Lookup lookup = MethodHandles.lookup();
Class<?> targetClass = Class.forName(className);
MethodHandle handle = lookup.findStatic(targetClass, methodName, methodType);
CallSite callSite = LambdaMetafactory
.metafactory(lookup, "call", lambdaType, methodType.unwrap(), handle, methodType);
MethodHandle methodHandle = callSite.getTarget();
return lambdaClass.cast(methodHandle.invoke());
}
A potential call could look like this
@FunctionalInterface
interface MyLambda {
double call(double d);
}
public void foo() {
lookupLambda(MyLambda.class, "java.lang.Math", "sin", double.class, double.class);
}
In a experimental setup this works well. However in the actual code the lambda class
is loaded using a different ClassLoader
than the rest of the application i.e. the class
of the target method.
This leads to an exception at runtime as it seems to use the ClassLoader
of the target method class
to load the lambda class
. Here is the interesting part of the stacktrace:
Caused by: java.lang.NoClassDefFoundError: GeneratedPackage.GeneratedClass$GeneratedInterface
at sun.misc.Unsafe.defineAnonymousClass(Native Method)
at java.lang.invoke.InnerClassLambdaMetafactory.spinInnerClass(InnerClassLambdaMetafactory.java:326)
at java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:194)
at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:304)
at my.project.MyClass.lookupLambda(MyClass.java:765)
at
... 9 more
Caused by: java.lang.ClassNotFoundException: GeneratedPackage.GeneratedClass$GeneratedInterface
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 15 more
How can I fix this? Is there a way to specify which ClassLoader
to use for each class? Is there another way of dynamically creating lambda instances that does not run into this issue?
Any help is highly appreciated.
Edit: Here a small executable example that should show the problem
1. The main class
import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
private static <T> T lookupLambda(Class<T> lambdaClass, String className, String methodName, Class<?> returnType,
Class<?> argumentType) throws Throwable {
MethodType lambdaType = MethodType.methodType(lambdaClass);
MethodType methodType = MethodType.methodType(returnType, argumentType);
MethodHandles.Lookup lookup = MethodHandles.lookup();
Class<?> targetClass = Class.forName(className);
MethodHandle handle = lookup.findStatic(targetClass, methodName, methodType);
CallSite callSite = LambdaMetafactory
.metafactory(lookup, "call", lambdaType, methodType.unwrap(), handle, methodType);
MethodHandle methodHandle = callSite.getTarget();
return lambdaClass.cast(methodHandle.invoke());
}
public static void main(String[] args) throws Throwable {
URL resourcesUrl = new URL("file:/home/pathToGeneratedClassFile/");
ClassLoader classLoader = new URLClassLoader(new URL[] { resourcesUrl }, Thread.currentThread().getContextClassLoader());
Class<?> generatedClass = classLoader.loadClass("GeneratedClass");
Class<?> generatedLambdaClass = classLoader.loadClass("GeneratedClass$GeneratedLambda");
Constructor constructor = generatedClass.getConstructor(generatedLambdaClass);
Object instance = constructor
.newInstance(lookupLambda(generatedLambdaClass, "java.lang.Math", "sin", double.class, double.class));
Method method = generatedClass.getDeclaredMethod("test");
method.invoke(instance);
}
}
2. The generated class
This assumes that the class has already been compiled to a .class
file and that it is somewhere outside of the scope of the system classloader.
import javax.annotation.Generated;
@Generated("This class is generated and loaded using a different classloader")
public final class GeneratedClass {
@FunctionalInterface
public interface GeneratedLambda {
double call(double d);
}
private final GeneratedLambda lambda;
public GeneratedClass(GeneratedLambda lambda) {
this.lambda = lambda;
}
public void test() {
System.out.println(lambda.call(3));
}
}
For me this results in the following stacktrace
Exception in thread "main" java.lang.NoClassDefFoundError: GeneratedClass$GeneratedLambda
at sun.misc.Unsafe.defineAnonymousClass(Native Method)
at java.lang.invoke.InnerClassLambdaMetafactory.spinInnerClass(InnerClassLambdaMetafactory.java:326)
at java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:194)
at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:304)
at Test.lookupLambda(Test.java:21)
at Test.main(Test.java:36)
Caused by: java.lang.ClassNotFoundException: GeneratedClass$GeneratedLambda
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 6 more
It's not entirely clear why you chose the path that you posted, and particularly, why exactly it fails outside the test environment. But if I understood you correctly, then it should be possible to achieve this goal by using a good old Dynamic Proxy Class:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@FunctionalInterface
interface MyLambda
{
double call(double d);
}
public class DynamicLambdaTest
{
public static void main(String[] args) throws Throwable
{
MyLambda x = lookupLambda(
MyLambda.class, "java.lang.Math", "sin",
double.class, double.class);
System.out.println(x.call(Math.toRadians(45)));
}
private static <T> T lookupLambda(Class<T> lambdaClass, String className,
String methodName, Class<?> returnType, Class<?> argumentType)
throws Throwable
{
Object proxy = Proxy.newProxyInstance(lambdaClass.getClassLoader(),
new Class[] { lambdaClass },
new LambdaProxy(lambdaClass, className, methodName, argumentType));
@SuppressWarnings("unchecked")
T lambda = (T)proxy;
return (T)lambda;
}
}
class LambdaProxy implements InvocationHandler {
// The object method handling is based on
// docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
private static Method hashCodeMethod;
private static Method equalsMethod;
private static Method toStringMethod;
static
{
try
{
hashCodeMethod =
Object.class.getMethod("hashCode", (Class<?>[]) null);
equalsMethod =
Object.class.getMethod("equals", new Class[] { Object.class });
toStringMethod =
Object.class.getMethod("toString", (Class<?>[]) null);
}
catch (NoSuchMethodException e)
{
throw new NoSuchMethodError(e.getMessage());
}
}
private Class<?> lambdaClass;
private Method callMethod;
public LambdaProxy(Class<?> lambdaClass, String className,
String methodName, Class<?> argumentType) {
this.lambdaClass = lambdaClass;
try
{
Class<?> c = Class.forName(className);
this.callMethod = c.getDeclaredMethod(methodName, argumentType);
}
catch (ClassNotFoundException
| NoSuchMethodException
| SecurityException e)
{
e.printStackTrace();
}
}
@Override
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable
{
Class<?> declaringClass = m.getDeclaringClass();
if (declaringClass == Object.class)
{
if (m.equals(hashCodeMethod))
{
return proxyHashCode(proxy);
}
else if (m.equals(equalsMethod))
{
return proxyEquals(proxy, args[0]);
}
else if (m.equals(toStringMethod))
{
return proxyToString(proxy);
}
else
{
throw new InternalError(
"unexpected Object method dispatched: " + m);
}
}
if (declaringClass == lambdaClass)
{
return callMethod.invoke(null, args);
}
throw new Exception("Whoopsie");
}
private int proxyHashCode(Object proxy) {
return System.identityHashCode(proxy);
}
private boolean proxyEquals(Object proxy, Object other) {
return (proxy == other);
}
private String proxyToString(Object proxy) {
return proxy.getClass().getName() + '@' +
Integer.toHexString(proxy.hashCode());
}
}
(You could even defer the initialization of the callMethod
in the invocation handler up to the point where invoke
is called for the first time. The code above should only be considered as a sketch showing what might be a viable path to the solution)
I don't know how you create your classloader, but assuming you already have one then you can replace
Class<?> targetClass = Class.forName(className);
with
Class<?> targetClass = yourClassLoader.loadClass(className);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With