I'm learning Byte Buddy and I'm trying to do the following:
- create a subclass from a given class or interface
- then replace a method in the subclass
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;
}
}