1

I need to define a ClassLoader that dynamically reloads certain classes from their respective classfiles. The reason for this is that the classes are changed since runtime started.

Currently I have a classloader than can load one class from its file. What I need further is load all classes that this one classes depends on and that are in the list of changed classes.

Simply explained, I have a ClassLoader class that overrides the loadClass method to load the changed classfile when the required class is in the list. Otherwise, it loads the class using the default (parent) classloader.

Is it possible to also (re)load the classes that are in the class definition from the primarily loaded class using the same classloader?

So, a simple example

Class A {
    static method() {
        B b = new B();
        C c = new C();
        // do something with B and C
    }
}
Class B {
    public X x = new X();
}

When I load class A with MyCustomClassLoader, I hand it a list of classes, containing A and C f.e. because I know the classfiles (the bin/A.class) of these classes have been changed. So, it should manually load the file from class A, and when loading B and C, it should check if they are in the list of changed classes. When they are not, they can be loaded using the default classloader, otherwise it will do the same as with A, recursively (so also checking classes that B depends on).

I have the loading from file working, but not the recursion. I also know that this method does not override the Java new operator globally. But I only need to run one changed class, but with it's dependencies also changed.

EDIT: Note that this is not achievable using Instrumentation. The documentation for it cites the following, both for retransformClasses() as for redefineClasses().

The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions.

So I made a working agent to change method bodies, but only when I noticed it crashed on method creation or deletion, I found this piece of text.

Steven Roose
  • 2,731
  • 4
  • 29
  • 46
  • I'm not sure how feasible it'd be to outright replace a class on the fly. Loading one, sure...because the JVM hasn't incorporated it into anything yet. But once it's loaded, assumptions are made, and a JITter would generate code based on those assumptions. The big question is, what should happen to an existing instance of the old class? – cHao Nov 21 '13 at 18:22
  • Well, it is not possible to replace the runtime class definition, that's for sure. But I'm only trying to redefine the class within the one classloader. This will not change behaviour of the `new` operator elsewhere in the runtime environment, but will only change the behaviour of the instances of that class created with my own classloader. – Steven Roose Nov 21 '13 at 19:37
  • It *is* possible, see my answer below. However, see caveats about existing instances of the previously defined class. – brettw Nov 22 '13 at 01:31
  • It is possible to replace runtime method implementation (bodies), but not the definition (signature) of classes or methods using Instrumentation. – Steven Roose Nov 22 '13 at 02:45

2 Answers2

2

I finally got it working. I made the following class:

I believe the key here was the resolveClass() method. Although the documentation doesn't provide any useful information, I believe this method ensures that after loading a class, all used classes are loaded as well.

It requires you to make a new class every time one of the changed classes changes again or a new class changed.

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Set;

/**
 * This class provides dynamic loading in the sense that the given class and 
 * other changed classes are loaded from disk, not from cache.
 * Ensures most recent version is used for those classes.
 */
public class DynamicClassLoader extends ClassLoader{

    private Set<String> classes;

    public DynamicClassLoader(Set<String> changedClasses) {
        this.classes = changedClasses;
    }

    /**
     * Dynamically loads the class from it's binary file. 
     * Classes this class depends on will only be loaded dynamically when 
     * they are in list of changed classes given at construction.
     * @throws ClassNotFoundException 
     */
    public Class<?> dynamicallyLoadClass(String name) 
        throws ClassNotFoundException {
        classes.add(name);
        return loadClass(name);
    }

    /**
     * Finds the class dynamically, contrary to Class.forname which can use 
     * cached copies.
     * This means it forces a reload of the class data.
     * Source: http://stackoverflow.com/questions/3971534/
     */
    @Override
    protected Class<?> findClass(String s) throws ClassNotFoundException {
        try {
            byte[] bytes;
            bytes = loadClassData(s);
            return defineClass(s, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException{
        return loadClass(name, true);
    }

    /**
     * Overridden to not check the parent's loadClass method first.
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
        throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(name);
        if(clazz != null)
            return clazz;

        if(classes.contains(name)) {
            clazz = findClass(name);
            resolveClass(clazz);
            return clazz;
        }
        return super.loadClass(name, resolve);
    }

    /**
     * Load class data from byte code.
     * 
     * @param className
     * @return
     * @throws IOException
     */
    protected byte[] loadClassData(String className) throws IOException {
        File f = new File("bin/" 
            + className.replaceAll("\\.", "/") + ".class");
        int size = (int) f.length();
        byte buff[] = new byte[size];
        FileInputStream fis = new FileInputStream(f);
        DataInputStream dis = new DataInputStream(fis);
        dis.readFully(buff);
        dis.close();
        return buff;
    }
}
Steven Roose
  • 2,731
  • 4
  • 29
  • 46
0

I would strongly recommend looking at using java.lang.instrument for what you are trying to accomplish, instead of juggling ClassLoaders. Specifically, by implementing the ClassFileTransformer interface, registering an agent, and using the redefineClasses() and/or retransformClasses() methods of the Instrumentation instance passed into your agent during registration.

Note that when you redefine a class, existing instances of the previous class definition are not affected. There is no way around this regardless of using custom ClassLoaders or instrumentation.

This page in particular you will find very informative about how to use instrumentation to accomplish what you are trying to do.

brettw
  • 10,664
  • 2
  • 42
  • 59
  • I tried it with an agent and it worked partially. Then I read this line in the Instrumentation javadoc: "The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions." I need support for method creation and deletion as well. – Steven Roose Nov 22 '13 at 02:30
  • That was my best shot. I don't think what you are trying to accomplish is possible in that case, neither through instrumentation nor class loaders. – brettw Nov 22 '13 at 02:35
  • Well, I finally got it working (right before I saw your comment). You are right that it is impossible to replace the runtime's definition of the class, but you can reload the class to get a class object with which you can create new instances of the new class. – Steven Roose Nov 22 '13 at 02:41