使用来自多个类加载器的参数通过反射创建一个类的新实例



你好,我试图通过反射创建一个类的新实例:

在下面的例子中:

  1. 这是Arg1的子类中的一个方法
  2. Data是存储相互关联的类引用的对象
public <T extends Arg2, S extends Arg1> Foo
getFoo(@NotNull Data<T, S> data) {
Class<?>[] classes = new Class<?>[]{data.getArg1(), data.getArg2()};
T entity = getArg2(data);
try {
Class<? extends Foo> clazz = data.getFoo();
Constructor<? extends Foo> constructor = clazz.getDeclaredConstructor(classes);
constructor.setAccessible(true);
Object[] objects = new Object[]{this, entity};
return constructor.newInstance(objects);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException |
ClassNotFoundException e) {
throw new RuntimeException(e);
}
}

当提供的参数来自相同的类装入器时,此代码有效,但当参数来自不同的类装入器时,此代码失败。因此,多个Class Loaders作为参数会导致方法失败。

是否有任何方法可以让Java从多个类加载器接受我的参数?

编辑:我有多个类加载器的原因是由于我使用自定义URLClassLoader将针对此应用程序API编译的外部jar文件加载到应用程序中。

至于最小的可复制的例子,我不能在这个时候提供一个例子,因为这是我不拥有的私有代码。代码的所有者必须明确允许我上传如此大量的代码(这是一堆类,本质上是整个应用程序的基石)。不过,我可以也会采纳所有的建议,并将这篇文章转发给业主,让他们批准。

任何帮助都是非常感激的:)

编辑2:

下面是带有调试信息的代码:
public <T extends Arg2, S extends Arg1> Foo
getFoo(@NotNull Data<T, S> data) {
Class<?>[] classes = new Class<?>[]{data.getArg1(), data.getArg2()};
T entity = getArg2(data);
try {
Class<? extends Foo> clazz = data.getFoo();
System.out.println(clazz.getClassLoader());
Constructor<? extends Foo> constructor = clazz.getDeclaredConstructor(classes);
constructor.setAccessible(true);
System.out.println(this.getClass().getClassLoader());
System.out.println(entity.getClassLoader());
Object[] objects = new Object[]{this, entity};
return constructor.newInstance(objects);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException |
ClassNotFoundException e) {
throw new RuntimeException(e);
}
}

给定Arg1, Arg2和Foo是类,它们是基础应用程序的一部分,输出如下:

jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa
jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa
jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa

假设Arg1和Foo是来自相同外部Jar文件的类,Arg2仍然是基础应用程序的一部分:

Class Loader for a single jar file
Joined Class Loader for all jar files
jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa
java.lang.IllegalArgumentException: argument type mismatch
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na]
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[na:na]
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[na:na]

注意:这是仅有的两个用例

不能在ClassLoaders中使用不同的对象。从appClassLoader加载的Foo类≠从另一个ClassLoader实例加载的Foo类请参阅以下代码示例:

import com.google.gson.Gson;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws Exception {
var customClassLoader = new CustomClassLoader();

//////////////////////    normal reflection   //////////////////////
Test.class.getMethod("foo", Foo.class).invoke(null, new Foo()); // equivalent to foo(new Foo);

//////////////////////    instantiating a new Foo obj from a custom class loader   //////////////////////
var foo = customClassLoader.findClass(Foo.class.getName()).getDeclaredConstructor().newInstance();

try {
//////////////////////    calling foo by passing a Foo obj from different ClassLoader   //////////////////////
Test.class.getMethod("foo", Foo.class).invoke(null, foo); // yields java.lang.IllegalArgumentException: argument type mismatch!
} catch (IllegalArgumentException e) {
System.err.println(e);
}

//////////////////////    workaround, using gson to serialize the obj   //////////////////////
var gson = new Gson();
Foo serializedFoo = gson.fromJson(gson.toJson(foo), Foo.class);
Test.class.getMethod("foo", Foo.class).invoke(null, serializedFoo); // no exception
}
public static void foo(Foo foo) {
System.out.println("Test#foo: " + foo.getClass().getClassLoader().getName());
}
public static class Foo {
}
public static class CustomClassLoader extends ClassLoader {
public CustomClassLoader() {
super("custom", getSystemClassLoader());
}
@Override
public Class<?> findClass(String name) throws ClassFormatError {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name.replace('.', File.separatorChar) + ".class");
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue;
try {
while ((nextValue = inputStream.read()) != -1) byteStream.write(nextValue);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
var data = byteStream.toByteArray();
return defineClass(name, data, 0, data.length);
}
}
}

根据您的使用情况,这可能不是最有效的解决方案,但它总是有效的。更好的解决方案是使用反射,但是您将被迫使用反射来执行所有操作。像这样:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws Exception {
var customClassLoader = new CustomClassLoader();
Test.class.getMethod("foo", Object.class).invoke(null, new Foo());
var foo = customClassLoader.findClass(Foo.class.getName()).getDeclaredConstructor().newInstance();
Test.class.getMethod("foo", Object.class).invoke(null, foo);
}
public static void foo(Object foo) throws Exception {
if (foo.getClass().getClassLoader() instanceof CustomClassLoader) {
foo.getClass().getMethod("sayFoo").invoke(foo);
} else {
((Foo) foo).sayFoo();
}
}
public static class Foo {
public void sayFoo() {
System.out.println("Foo");
}
}
public static class CustomClassLoader extends ClassLoader {
public CustomClassLoader() {
super("custom", getSystemClassLoader());
}
@Override
public Class<?> findClass(String name) throws ClassFormatError {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name.replace('.', File.separatorChar) + ".class");
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue;
try {
while ((nextValue = inputStream.read()) != -1) byteStream.write(nextValue);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
var data = byteStream.toByteArray();
return defineClass(name, data, 0, data.length);
}
}
}

两个来自不同类加载器的同名类在运行时被认为是不同的类。[1]

参数类型mismatch"错误是说你传入了一个"Arg2"实例,但构造函数需要一个"Arg2">

您需要找到一种方法将正确类的参数(包括从正确的类装入器)传递给构造函数。你在这里没有包括足够的背景,让我说你如何实现这一点。GL

进一步阅读

[1] https://kepler-project.org/developers/teams/framework/design-docs/trade-studies/custom-class-loading/using-multiple-classloaders.html

如果同一类的不同版本由不同的类加载器加载,那么使用这些不同类加载器的模块不能交换该类型的对象。

最新更新