Hello I am trying to create a new instance of a class via reflection:
In the example below the following applies:
- This is a method in a subclass of Arg1
- Data is an object which stores class references which are associated with each other
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); } }
This code works when the provided arguments are from the same class loader, yet the code fails when the arguments are from different class loaders. As such, multiple Class Loaders as arguments causes the failure of the method.
Is there any way I can get Java to accept my Arguments from multiple class loaders?
Edit: The reason I have multiple class loaders is due to the fact that I load external jar files which were compiled against this applications API into the application using a custom URLClassLoader
.
As to the minimal reproducible example, I cannot at this time provide an example as this is private code which I do not own. The owner of the code would have to give me express permission to upload such an extensive chunk of the code (It’s a bunch of classes that are essentially the cornerstone to the entire application). I can and will run any and all suggestions though and will forward this post to the owner for their approval.
Any help is much appreciated 🙂
Edit 2:
Here the code with debug messages:
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); } }
Given that Arg1, Arg2 and Foo are Classes which are part of the base application, the output is as follows:
jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa jdk.internal.loader.ClassLoaders$AppClassLoader@1d44bcfa
Given that Arg1 and Foo are Classes which are from the same external Jar File and Arg2 remains a class that is part of the base application:
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]
Note: These are the only two use cases
Advertisement
Answer
You cannot use different objects across ClassLoaders
. Class Foo
loaded from app
ClassLoader
≠Foo
loaded from another ClassLoader
instance. Please see the following code sample:
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); } } }
Depending on you usage, this might not be the most efficient solution, but one that will always work. A better solution would be using reflection, but you will be forced to do everything with reflection. Something like this:
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); } } }