java - 使用 ByteBuddy : "class redefinition failed: attempted to add a method" 重新定义方法时出错

标签 java instrumentation byte-buddy

我正在学习 Byte Buddy 并且正在尝试执行以下操作:

  • 从给定的类或接口(interface)创建子类
  • 然后替换子类中的一个方法

请注意,子类在 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 接口(interface)被子类化时,另外两个测试失败。

我读到我应该在一个单独的类中委托(delegate)新方法,这是我使用 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 接口(interface)和Bar 类是:

public interface Foo {

    public String sayHello();

}

public class Bar {

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

}

最佳答案

问题是您首先创建了 Bar 的子类,然后加载它,但后来重新定义它以添加方法 sayHello。您的类(class)演变如下:

  1. 子类创建

    class Bar$ByteBuddy extends Bar {
      Bar$ByteBuddy() { ... }
    }
    
  2. 子类的重新定义

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

HotSpot VM 和大多数其他虚拟机不允许在类加载后添加方法。您可以通过在第一次定义它之前将方法添加到子类来解决这个问题,即设置:

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

这样,重定义时方法已经存在,字节好友可以简单地替换其字节码,而不需要添加方法。请注意,Byte Buddy 尝试重新定义,因为一些虚拟机确实支持它(即动态代码演化虚拟机,希望在某个时候合并到 HotSpot 中,请参阅 JEP 159)。

关于java - 使用 ByteBuddy : "class redefinition failed: attempted to add a method" 重新定义方法时出错,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40774684/

相关文章:

java - 从 XML 元素读取属性

android - 运行单元测试时运行 Junit5 和 Instrumentation Tests

java - Java中如何获取Instrumentation实例

java - 使用不带 -javaagent 参数的 ByteBuddy Java 代理

java - 在 Byte Buddy 中禁用标识符验证

java - 在 Strategy 设计模式中使用抽象类代替接口(interface)

java - 为什么在使用 Collections.sort() 而不是使用 TreeSet.add(new Object()) 时出现编译时错误

Java 的 Scanner 类 : using left- and right buttons with Bash

android - Robotium 示例

java - 如何使用注释 @MyFormat ("%td.%<tm.%<tY"格式化 getBirthday()