Search code examples
javaclassloadernoclassdeffounderrorbytecodebyte-buddy

How to load this class in the correct way?


Alright, so I have a class

class A{
    public D d = new B$0();

    public void foo(){
        B$0 b;
        try{
            b = (B$0)this.d;
        }catch(ClassCastException e){
             this.d = d.move(this); //actual implementation uses a CAS to make sure it's only replaced once
             throw new RepeatThisMethodException();
        }
        //do something with b here
    }
}

Where RepeatThisMethodException is handled by some code further up.

abstract class D{
    public abstract D move(Object o);
}

and

class B$0 extends D{
    public static D moveThis(A a){
        throw new Error();
    }

    public D move(Object o){
        return moveThis((A)o);
    }|
}

I now create a new class

class B$1 extends D{
    public D move(Object o){
        return B$0.moveThis((A)o);
    }
}

And load it using ByteBuddy.

    DynamicType.Builder builder = byteBuddy
            .subclass(D.class)
            .name("B$1")
            ;

    DynamicType.Unloaded newClass = builder.make();
    byte[] rawBytecode = newClass.getBytes();
    byte[] finishedBytecode = MyASMVisitor.addMethods(rawBytecode);

    Class b0 = Class.forName("B$0");
    ClassLoadingStrategy.Default.INJECTION.load(b0.getClassLoader(),
            Collections.singletonMap(newClass.getTypeDescription(), finishedBytecode));

(Note that I'm using B$0.class.getClassloader() to load B$1.)

The bytecode for that move method MyASMVisitor adds looks like this:

public Method move:"(Ljava/lang/Object;)LD;"
    stack 1 locals 2
{
        aload_1;
        checkcast   class A;
        invokestatic    Method B$0.moveThis:"(LA;)LD;";
        areturn;
}

Now that B$1 is loaded, I re-instrument B$0 s.t. it can handle the new class.

class B$0 extends D{
    public static D moveThis(A a){
        if(a.d instanceof B$1) throw new RepeatThisMethodException();
        if(a.d instanceof B$0) return new B$1();
        throw new Error();
    }

    public D move(Object o){
        return moveThis((A)o);
    }|
}

and reload it using

private void redefineClass(String classname, byte[] bytecode) {
    Class clazz;
    try{
        clazz = Class.forName(classname);
    }catch(ClassNotFoundException e){
        throw new RuntimeException(e);
    }

    ClassReloadingStrategy s = ClassReloadingStrategy.fromInstalledAgent();
    s.load(clazz.getClassLoader(),
            Collections.singletonMap((TypeDescription)new TypeDescription.ForLoadedType(clazz), bytecode));
}

So B$0 gets reloaded by B$0.class.getClassLoader().

Now that B$1 exists and can be handled, I let A know it should use the new class from now on.

class A{
    public D d = new B$1();

    public void foo(){
        B$1 b;
        try{
            b = (B$1)this.d;
        }catch(ClassCastException e){
             this.d = d.move(this); //actual implementation uses a CAS to make sure it's only replaced once
             throw new RepeatThisMethodException();
        }
        //do something with b here
    }
}

And reload it using the same redefineClass method (so A gets reloaded by A.class.getClassLoader()).

In effect, new instances of A will use B$1 right from the get-go, while existing instances will call b.move(this), which in turn will call B$0.moveThis((A)o) (where it cannot use this.

This seems to work, for now.

The problem now is that we need to update ALL classes that ever use a version of B obviously, we cannot reload them all simultaneously, so some are going to be earlier and some are going to be late.

Let's say we have a class G that uses A a and consequently, its a.d.

A is already reloaded, G isn't yet. So some methods on A (or any other already reloaded client of A) may have triggered the move already while G still tries to cast to B$0.

That's fine.

If G uses A a and fails to cast a.d to the version it expects, it will call a.d.move(a) which in turn calls B$0.moveThis((A)a).

In that case,

if(a.d instanceof B$1) throw new RepeatThisMethodException();

in our handling code in B$0 ensures that G cannot make progress until its bytecode has been reloaded and it knows about B$1.

Or it WOULD, if B$1 would be able to call B$0.moveThis.

Instead, we get

Exception in thread "MyT1" java.lang.NoClassDefFoundError: A
    at B$1.move(Unknown Source)

Alright, that is unfortunate. Let's see if we can circumvent this error by moving the cast of Object o to B$0.moveThis ...

Exception in thread "MyT1" java.lang.NoClassDefFoundError: B$0
    at B$1.move(Unknown Source)

Nope, doesn't look like.

How do I load B$1 s.t. it has access to at least B$0 and both B$0 and A (and whatever clients of A, eventually) have access to it?

Note

Any kind of solution needs to support upcasting.

E.g. say I have D :> B :> C and we use B b = new C() (or pass an instance of C to a method expecting a B or ...), then b.move(b) must still call C$0.moveThis((C)b).

Update (Thanks, Holger)

The issues seems unrelated to the redefinition of existing classes.

    Class b0 = Class.forName("B$0");
    ClassLoadingStrategy.Default.INJECTION.load(b0.getClassLoader(),
            Collections.singletonMap(newClass.getTypeDescription(), finishedBytecode));

    try {
        Class c = Class.forName("B$1");
        Object instance = c.newInstance();
        c.getMethod("move", Object.class).invoke(instance, new Object());
    } catch (Exception e) {
        e.printStackTrace();
    }

which calls B$1.move() before any other class has been reloaded, is in fact enough to trigger the NoClassDefFoundError.

Update

When I print clazz.getClassLoader() for reloading classes and b0.getClassLoader() for new classes, I always get the same instance of sun.misc.Launcher$AppClassLoader.


Solution

  • This turned out to be a problem in the actual code generation as discussed in the related GitHub issue.