Skip to content
Advertisement

Creating a New Instance of a class via Reflection with Arguments from multiple Class Loaders

Hello I am trying to create a new instance of a class via reflection:

In the example below the following applies:

  1. This is a method in a subclass of Arg1
  2. 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);
        }
    }

}
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement