Ошибка при переопределении метода с помощью ByteBuddy: "Не удалось переопределить класс: попытка добавить метод"

Я изучаю Byte Buddy и пытаюсь сделать следующее:

  • создать подкласс из данного класса или интерфейса
  • затем заменить метод в подклассе

Обратите внимание, что подкласс "загружен" в ClassLoader перед одним из его методов (sayHello) переопределено. Сбой со следующим сообщением об ошибке:

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

Ниже приведен код для набора тестов JUnit. Первый тест, shouldReplaceMethodFromClassпроходит как Bar Класс не разделяется на подклассы до переопределения его метода. Два других теста не пройдены, когда данный Bar класс или Foo интерфейс подкласс.

Я прочитал, что должен делегировать новый метод в отдельный класс, что я и делаю, используя CustomInterceptor класс, и я также установил агент ByteBuddy при запуске теста и использовал для загрузки подкласса, но даже при этом я все еще что-то упускаю, и я не вижу, что:(

У кого-нибудь есть идея?

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


}

Foo интерфейс и Bar класс являются:

public interface Foo {

    public String sayHello();

}

а также

public class Bar {

    public String sayHello() throws Exception {
      return null;
    }

}

1 ответ

Решение

Проблема в том, что вы сначала создаете подкласс Bar, затем загрузить его, но позже переопределить его, чтобы добавить метод sayHello, Ваш класс развивается следующим образом:

  1. Создание подкласса

    class Bar$ByteBuddy extends Bar {
      Bar$ByteBuddy() { ... }
    }
    
  2. Переопределение подкласса

    class Bar$ByteBuddy extends Bar {
      Bar$ByteBuddy() { ... }
      String sayHello() { ... }
    }
    

Виртуальная машина HotSpot и большинство других виртуальных машин не позволяют добавлять методы после загрузки класса. Вы можете исправить это, добавив метод к подклассу перед его первым определением, т.е. установив:

DynamicType.Loaded<T> loaded = bytebuddy.subclass(baseClass)
  .method(ElementMatchers.named("sayHello"))
  .intercept(SuperMethodCall.INSTANCE) // or StubMethod.INSTANCE
  .make()

Таким образом, метод уже существует при переопределении, и Byte Buddy может просто заменить свой байт-код вместо необходимости добавлять метод. Обратите внимание, что Byte Buddy пытается переопределить, так как некоторые виртуальные машины действительно поддерживают его (в частности, виртуальная машина эволюции динмического кода, которая, как мы надеемся, в какой-то момент будет объединена с HotSpot, см. JEP 159).

Другие вопросы по тегам