3

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;
    }

}
Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
Xavier Coulon
  • 1,580
  • 10
  • 15

1 Answers1

1

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).

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • Thanks for your response, Raphael! But I'm a bit confused, because I was hoping to be able to do something similar to the way Mockito works: ie, first define a "Mock" class and then customize the behaviour. In other words is there a way to avoid loading the subclass before replacing the method ? – Xavier Coulon Nov 24 '16 at 09:25
  • In this case, you need to traverse the class hierarchy of `Bar` and find the first class that declares the method. This is what we do in Mockito. Within this method, we then add code similar to: `if (this instanceof SomeClass)` to decide whether the code should be dispatched. If you define the subclasses explicitly, I would however recommend you to apply some `method(any()).instrument(SuperMethodCall.INSTANCE)` what allows you to later redefine any method, if you have the chance. – Rafael Winterhalter Nov 24 '16 at 11:45
  • Awesome, thanks a lot! I used a slightly different approach to support the `Foo` interface as well, by throwing an exception for all super method calls: `.method(ElementMatchers.any()).intercept(ExceptionMethod.throwing(RuntimeException.class));` and it works great! – Xavier Coulon Nov 24 '16 at 12:43