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
.
This turned out to be a problem in the actual code generation as discussed in the related GitHub issue.