Xavier Coulon
Xavier Coulon

Reputation: 1600

Error while redefining a method with ByteBuddy: "class redefinition failed: attempted to add a method"

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

Answers (1)

Rafael Winterhalter
Rafael Winterhalter

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:

  1. Subclass creation

    class Bar$ByteBuddy extends Bar {
      Bar$ByteBuddy() { ... }
    }
    
  2. 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

Related Questions