Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining the same class twice using a ClassLoader

I am working a project which involves reading all of the classes in a given JAR file, modifying some of these classes (at the bytecode level, using ASM), and creating a ClassLoader of these modified classes. Currently, after modifying the classes, I create a custom instance of the ClassLoader class which makes defineMethod(String, byte[]) visible.

Here is the code for the custom ClassLoader (it is a nested class):

private static class ExposingClassLoader extends ClassLoader {

    private ExposingClassLoader(JarFile jar) throws MalformedURLException {
        super(new URLClassLoader(new URL[] { new URL("jar:" + new File(jar.getName()).toURI().toURL() + "!/") }));
    }

    private void defineClass(String name, byte[] data) {
        defineClass(name, data, 0, data.length);
    }
}

You may have noticed that I made the parent class loader a URLClassLoader. When I construct the custom class loader, I make the URLClassLoader refer to the JAR that contains the unmodified class files. After modification of some of the class files, I iterate through them and invoke defineMethod(String, byte[]) for each one. Some of the modified classes depend on each other, that is, class A may be the superclass of class B, or class C may implement class D, or class E may contain references to class F, et cetera.

For my tests, I am using two classes. The first is named fn and the second is named fi, and fi extends fn and contains multiple fields of type fi. Both of these classes are present in the JAR file that is used to construct the ClassLoader, the only difference is that they are unmodified. For some reason, if I define class fn it works fine (which it should not? The class fn already exists in the JAR file). However, if I try to define fi, which, again, refers to fn multiple times, I get this exception:

Exception in thread "main" java.lang.LinkageError: loader (instance of Injector$ExposingClassLoader): attempted  duplicate class definition for name: "fn"

On the line:

defineClass(name, data, 0, data.length);

And I am not sure how to deal with this. The easiest way to do this would be to "unload" the classes that are contained in the JAR, but only those that I have modified. That way, when I define my modified classes, they will not already be loaded in the ClassLoader. However, I have looked around and I have yet to find a clean way to do it. Some have said that I need separate (multiple) class loaders, and that would not work for my project unless there was a way to perhaps combine these multiple class loaders into one, like a wrapper.

How can I redefine a class in a ClassLoader?

like image 508
Martin Tuskevicius Avatar asked Nov 02 '22 06:11

Martin Tuskevicius


1 Answers

I found the solution to my problem. Basically, rather than overriding the behavior of defineClass I should have overriden findClass. Here is the code that works.

public class ModifiableClassLoader extends ClassLoader {

    private final Map<String, byte[]> definitions;

    public ModifiableClassLoader(JarFile jar, Map<String, byte[]> definitions) throws MalformedURLException {
        super(new URLClassLoader(new URL[] { new URL("jar:" + new File(jar.getName()).toURI().toURL() + "!/") }));
        this.definitions = definitions;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classBytes = definitions.remove(name);
        if (classBytes != null) {
            return defineClass(name, classBytes, 0, classBytes.length);
        }
        return super.findClass(name);
    }
}

It wrote it based on an answer to another Stack Overflow question which I cannot find right now. I must have overlooked it before.

like image 50
Martin Tuskevicius Avatar answered Nov 13 '22 17:11

Martin Tuskevicius