MethodHandle.Lookup for classes loaded outside of default ClassLoader

Viewed 52

I'm using the stuff of java.lang.invoke to create lambdas out of methods obtained by reflection. When the method is called frequently, it improves performances.

It works quite well, until I discovered that it no longer works when the method comes from a class loaded by a ClassLoader different than the default one.

What have I missed ? What do I need to change in order to make it work ?

IN case there is an importance, I'm on Java 17.

First of all, the class I'm going to load externally:

public class TestFunc {
  public static String testFunc (String str) {
    return str.toUpperCase();
  }
}

Then, the main example code:

import java.lang.reflect.*;
import java.lang.invoke.*;
import java.util.*;
import java.util.function.*;
import java.net.*;
import java.io.*;

public class TestLambda {

  public static <I> I createLambda (Object obj, Executable method, Class<I> interfaceClass, Class<?> returnType, String name, Class<?>... mArgs) throws ReflectiveOperationException {
    try {
      method.setAccessible(true);
      boolean isConstructor = method instanceof Constructor, isStatic = isConstructor || Modifier.isStatic(method.getModifiers()), isBound = obj!=null&&!isStatic;
      var cls = method.getDeclaringClass();
      var pt = method.getParameterTypes();
      var rt = isConstructor? cls : ((Method)method) .getReturnType();
      var lookup = MethodHandles.lookup();
      if (Modifier.isPrivate(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) lookup = MethodHandles.privateLookupIn(cls, lookup);
      var mh = isConstructor? lookup.unreflectConstructor((Constructor)method) : lookup.unreflect((Method)method);
      var  interfaceMT = isBound? MethodType.methodType(interfaceClass, cls) : MethodType.methodType(interfaceClass);
      var implementationMT = MethodType.methodType(returnType, mArgs);
      var calledMT = isStatic || isBound? MethodType.methodType(rt, pt) : MethodType.methodType(rt, cls, pt);
      var callSite = LambdaMetafactory.metafactory(lookup, name, interfaceMT, implementationMT, mh, calledMT);
      return (I) (isBound?
        callSite.getTarget() .invoke(obj) :
        callSite.getTarget() .invoke() );
    } catch (Throwable t) { 
      throw new ReflectiveOperationException(t);
    }
  }
  
  public static <T,R> Function<T,R> createFunction (Object obj, Executable method, Class<T> argType, Class<R> returnType) throws ReflectiveOperationException {
    return (Function<T,R>) createLambda(obj, method, Function.class, Object.class, "apply", Object.class);
  }
  
  public static void main (String[] args) throws Throwable {
    // Example usage with a trivial case where it works well
    Method toUpperCaseMethod = String.class.getMethod("toUpperCase");
    Function<String, String> toUpperCaseFunction = createFunction(null, toUpperCaseMethod, String.class, String.class);
    System.out.println(toUpperCaseFunction.apply("Hello, world!"));
    
    // Example 2 to show that calling the external method via standars reflection API works as well
    ClassLoader classLoader = new URLClassLoader( new URL[]{ new File("test.jar").toURL() }, TestLambda.class.getClassLoader());
    Class theClass = classLoader.loadClass("TestFunc");
    Method method = theClass.getMethod("testFunc", String.class);
    System.out.println(method.invoke(null, "This is working"));
    
    // Example 3 where it doesn't work
    Function<String,String> func = createFunction(null, method, String.class, String.class);
    System.out.println(func.apply("This doesn't work"));
  }

}

Result :

HELLO, WORLD!
THIS IS WORKING
Exception in thread "main" java.lang.NoClassDefFoundError: TestFunc
    at TestLambda.main(TestLambda.java:51)
Caused by: java.lang.ClassNotFoundException: TestFunc
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    at jdk.compiler/com.sun.tools.javac.launcher.Main$MemoryClassLoader.loadClass(Main.java:600)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    at TestLambda.main(TestLambda.java:51)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:419)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:192)
    at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:132)
1 Answers

I have found a solution. It works when using MethodHandleProxies.asInterfaceInstance.

Performance wise, I presume it's one order slower than the LambdaMetafactory, because it then pass via JDK Proxy.

I still don't understand why it doesn't work with LambdaMetafactory, and what it changes when it's done via a proxy. Does someone has any idea ? Feel free to post the explanation in another answer if you have it.

The code becomes as follows:

public static <I> I createLambda (Object obj, Executable method, Class<I> interfaceClass, Class<?> returnType, String name, Class<?>... mArgs) throws ReflectiveOperationException {
  try {
    method.setAccessible(true);
    boolean isConstructor = method instanceof Constructor, isStatic = isConstructor || Modifier.isStatic(method.getModifiers()), isBound = obj!=null&&!isStatic;
    var cls = method.getDeclaringClass();
    var targetClassLoader = cls.getClassLoader();
    var pt = method.getParameterTypes();
    var rt = isConstructor? cls : ((Method)method) .getReturnType();
    var lookup = MethodHandles.lookup();
    if (Modifier.isPrivate(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) lookup = MethodHandles.privateLookupIn(cls, lookup);
    var mh = isConstructor? lookup.unreflectConstructor((Constructor)method) : lookup.unreflect((Method)method);
    if (targetClassLoader!=null && targetClassLoader!=TestLambda.class.getClassLoader()) {
      return (I) MethodHandleProxies.asInterfaceInstance(interfaceClass, mh);
    }
    else {
      var  interfaceMT = isBound? MethodType.methodType(interfaceClass, cls) : MethodType.methodType(interfaceClass);
      var implementationMT = MethodType.methodType(returnType, mArgs);
      var calledMT = isStatic || isBound? MethodType.methodType(rt, pt) : MethodType.methodType(rt, cls, pt);
              var callSite = LambdaMetafactory.metafactory(lookup, name, interfaceMT, implementationMT, mh, calledMT);
              return (I) (isBound?
                callSite.getTarget() .invoke(obj) :
        callSite.getTarget() .invoke() );
      }
  } catch (Throwable t) { 
    throw t instanceof ReflectiveOperationException r? r : new ReflectiveOperationException(t);
  }
}
Related