Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically create lambda in Java using different classloaders

Tags:

java

lambda

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
like image 796
MartinS Avatar asked Jan 24 '18 19:01

MartinS


2 Answers

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)

like image 146
Marco13 Avatar answered Oct 08 '22 19:10

Marco13


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);
like image 21
Thomas Kläger Avatar answered Oct 08 '22 19:10

Thomas Kläger