11

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
4
  • 3
    There's no such thing as a "lambda instance", it's just a class that implements some interface. Any reflection approach targeting the appropriate interface is applicable. Commented Jan 24, 2018 at 19:19
  • You can create custom ClassLoader, also remember that class laoder goes through the hierarchy and loads. Commented Jan 24, 2018 at 19:22
  • 1
    Can you convert this into a minimal reproducible example ? In a quick test, this throws an AbstractMethodError for me... Commented Jan 25, 2018 at 0:56
  • Sorry there was a typo in the lookupLambda method, it has to be methodType.unwrap() instead of methodType.generic() to work. Commented Jan 25, 2018 at 1:21

2 Answers 2

2

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);
Sign up to request clarification or add additional context in comments.

5 Comments

The class is already loaded but it seems that sun.misc.Unsafe.defineAnonymousClass is trying to load it again using the other ClassLoader which then fails.
A class is defined by both its bytecode, and the class loader. I am not sure but wouldn't the other class loader think that its loading a different class?
I guess so. However the standard ClassLoader cannot find it because of the way it is generated which is the reason it was being loaded using a different ClassLoader in the first place.
@MartinS which class is dynamically generated? Is it the functional interface (MyLambda) or is it the class with the method that you want to call (i.e. "java.lang.Math"/"sin")?
I generate the functional interface
2

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)

1 Comment

thanks, I will give this a try when I have some time.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.