Reputation: 1600
I'm learning Byte Buddy and I'm trying to do the following:
Note that the subclass is 'loaded' in a ClassLoader
before one of its method (sayHello
) is redefined. It fails with the following error message:
java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method
at sun.instrument.InstrumentationImpl.redefineClasses0(Native Method)
at sun.instrument.InstrumentationImpl.redefineClasses(InstrumentationImpl.java:170)
at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy$1.apply(ClassReloadingStrategy.java:293)
at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:173)
...
Below is the code for a set of JUnit tests. The first test, shouldReplaceMethodFromClass
, passes as the Bar
class is not subclassed before redefining its method. The two other tests fail when the given Bar
class or Foo
interface is subclassed.
I read that I should delegate the new method in a separate class, which is what I do using the CustomInterceptor
class, and I also installed the ByteBuddy agent at the test startup and used to load the subclass, but even with that, I'm still missing something, and I can't see what :(
Anyone has an idea ?
public class ByteBuddyReplaceMethodInClassTest {
private File classDir;
private ByteBuddy bytebuddy;
@BeforeClass
public static void setupByteBuddyAgent() {
ByteBuddyAgent.install();
}
@Before
public void setupTest() throws IOException {
this.classDir = Files.createTempDirectory("test").toFile();
this.bytebuddy = new ByteBuddy().with(Implementation.Context.Disabled.Factory.INSTANCE);
}
@Test
public void shouldReplaceMethodFromClass()
throws InstantiationException, IllegalAccessException, Exception {
// given
final Class<? extends Bar> modifiedClass = replaceMethodInClass(Bar.class,
ClassFileLocator.ForClassLoader.of(Bar.class.getClassLoader()));
// when
final String hello = modifiedClass.newInstance().sayHello();
// then
assertThat(hello).isEqualTo("Hello!");
}
@Test
public void shouldReplaceMethodFromSubclass()
throws InstantiationException, IllegalAccessException, Exception {
// given
final Class<? extends Bar> modifiedClass = replaceMethodInClass(createSubclass(Bar.class),
new ClassFileLocator.ForFolder(this.classDir));
// when
final String hello = modifiedClass.newInstance().sayHello();
// then
assertThat(hello).isEqualTo("Hello!");
}
@Test
public void shouldReplaceMethodFromInterface()
throws InstantiationException, IllegalAccessException, Exception {
// given
final Class<? extends Foo> modifiedClass = replaceMethodInClass(createSubclass(Foo.class),
new ClassFileLocator.ForFolder(this.classDir));
// when
final String hello = modifiedClass.newInstance().sayHello();
// then
assertThat(hello).isEqualTo("Hello!");
}
@SuppressWarnings("unchecked")
private <T> Class<T> createSubclass(final Class<T> baseClass) {
final Builder<T> subclass =
this.bytebuddy.subclass(baseClass);
final Loaded<T> loaded =
subclass.make().load(ByteBuddyReplaceMethodInClassTest.class.getClassLoader(),
ClassReloadingStrategy.fromInstalledAgent());
try {
loaded.saveIn(this.classDir);
return (Class<T>) loaded.getLoaded();
} catch (IOException e) {
throw new RuntimeException("Failed to save subclass in a temporary directory", e);
}
}
private <T> Class<? extends T> replaceMethodInClass(final Class<T> subclass,
final ClassFileLocator classFileLocator) throws IOException {
final Builder<? extends T> rebasedClassBuilder =
this.bytebuddy.redefine(subclass, classFileLocator);
return rebasedClassBuilder.method(ElementMatchers.named("sayHello"))
.intercept(MethodDelegation.to(CustomInterceptor.class)).make()
.load(ByteBuddyReplaceMethodInClassTest.class.getClassLoader(),
ClassReloadingStrategy.fromInstalledAgent())
.getLoaded();
}
static class CustomInterceptor {
public static String intercept() {
return "Hello!";
}
}
}
The Foo
interface and Bar
class are:
public interface Foo {
public String sayHello();
}
and
public class Bar {
public String sayHello() throws Exception {
return null;
}
}
Upvotes: 3
Views: 9024
Reputation: 44032
The problem is that you first create a subclass of Bar
, then load it but later redefine it to add the method sayHello
. Your class evolves as follows:
Subclass creation
class Bar$ByteBuddy extends Bar {
Bar$ByteBuddy() { ... }
}
Redefinition of subclass
class Bar$ByteBuddy extends Bar {
Bar$ByteBuddy() { ... }
String sayHello() { ... }
}
The HotSpot VM and most other virtual machines do not allow adding methods after class loading. You can fix this by adding the method to the subclass before defining it for the first time, i.e. setting:
DynamicType.Loaded<T> loaded = bytebuddy.subclass(baseClass)
.method(ElementMatchers.named("sayHello"))
.intercept(SuperMethodCall.INSTANCE) // or StubMethod.INSTANCE
.make()
This way, the method already exists when redefining and Byte Buddy can simply replace its byte code instead of needing to add the method. Note that Byte Buddy attempts the redefinition as some few VMs do actually support it (namly the dynmic code evolution VM which hopefully gets merged into HotSpot at some point, see JEP 159).
Upvotes: 1