0

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

4
  • 1
    Please edit your question to include the full complete error message you get. Also add a minimal reproducible example of your problem to your question to make it easier to reproduce the issue. Also add a detailed description where your different class loaders are coming from and why you don't have just one. Commented Nov 21, 2022 at 21:29
  • @Progman I made the requested changes :) Commented Nov 22, 2022 at 6:20
  • If the code is so private and special that you cannot share a reproducer, why are you asking about it in public? It is like wishing to eat the cake, but keep it at the same time. Commented Nov 29, 2022 at 20:08
  • Why do you have the generics flipped? So in this method you have <T extends Arg2, S extends Arg1> but in Foo you have them in the correct order <S extends Arg1, T extends Arg2>? Also Foo is a parametrized class, so adding <?, ?> will increase the readability. However, your provided code literally does not help at all, because clearly the issue with initializing the objects in the first place, which you give less attention. Commented Dec 1, 2022 at 12:05

2 Answers 2

1
+50

You cannot use different objects across ClassLoaders. Class Foo loaded from app ClassLoaderFoo 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);
        }
    }

}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you this resolved my issue. Had not thought of reflecting into methods instead of constructor
0

Two classes with the same name from different class loaders are considered to be different classes at runtime. [1]

The "argument type mismatch" error is saying that you are passing in an "Arg2" instance from one class loader, but the constructor needs an "Arg2" instance from the other class loader.

You will need to find a way to pass arguments of the correct class, including from the correct class loader, to the constructor. You haven't included enough context here for me to say how you might achieve that. GL

Further reading

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

If different versions of the same class are loaded by different class loaders, then modules that use those different class loaders cannot exchange objects of that type.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.