Ответ 1
Я не знаю, как вы создаете свой загрузчик классов, но предположив, что у вас уже есть один, вы можете заменить
Class<?> targetClass = Class.forName(className);
с
Class<?> targetClass = yourClassLoader.loadClass(className);
Я пытаюсь динамически создавать экземпляр лямбды, используя классы, которые недоступны во время компиляции, поскольку они генерируются во время выполнения или еще не известны во время компиляции.
Это работает с использованием следующего кода
// 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());
}
Потенциальный вызов может выглядеть так:
@FunctionalInterface
interface MyLambda {
double call(double d);
}
public void foo() {
lookupLambda(MyLambda.class, "java.lang.Math", "sin", double.class, double.class);
}
В экспериментальной установке это работает хорошо. Однако в реальном коде lambda class
загружается с использованием другого ClassLoader
, чем остальная часть приложения, т.е. class
целевого метода.
Это приводит к исключению во время выполнения, поскольку, как кажется, используется ClassLoader
целевого метода class
для загрузки lambda class
. Вот интересная часть 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
Как я могу это исправить? Есть ли способ указать, какой ClassLoader
использовать для каждого класса? Есть ли другой способ динамического создания экземпляров лямбда, который не сталкивается с этой проблемой?
Любая помощь высоко ценится.
Edit: Вот небольшой пример, который должен показать проблему
1. Основной класс
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. Сгенерированный класс
Это предполагает, что класс уже скомпилирован в файл .class
и что он находится где-то вне области системного загрузчика классов.
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));
}
}
Для меня это приводит к следующему 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
Я не знаю, как вы создаете свой загрузчик классов, но предположив, что у вас уже есть один, вы можете заменить
Class<?> targetClass = Class.forName(className);
с
Class<?> targetClass = yourClassLoader.loadClass(className);
Не совсем понятно, почему вы выбрали путь, который вы опубликовали, и, в частности, почему именно он выходит за пределы тестовой среды. Но если бы я понял вас правильно, то достичь этой цели можно было бы, используя старый добрый Динамический класс прокси:
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());
}
}
(Вы даже можете отложить инициализацию callMethod
в обработчике вызовов до точки, в которой invoke
вызывается в первый раз. Этот код следует рассматривать только как эскиз, показывающий, что может быть жизнеспособным путь к решению)