2016-11-23 60 views
1

我学习字节好友和我试图做到以下几点:错误而重新定义与ByteBuddy的方法:“重新定义类失败:尝试添加一个方法”

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

注意,子类是在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尝试重新定义,因为少数虚拟机实际上支持它(如有兴趣的话可以在某些时候合并到HotSpot中,参见JEP 159)。

+0

谢谢你的回应,Raphael!但是我有点困惑,因为我希望能够做到类似于Mockito的工作方式:即首先定义一个“模拟”类,然后定制行为。换句话说,在替换方法之前是否有避免加载子类的方法? –

+0

在这种情况下,您需要遍历Bar的类层次结构并找到声明该方法的第一个类。这就是我们在Mockito中所做的。在这个方法中,我们添加类似于'if(this instanceof SomeClass)'的代码来决定是否应该分派代码。如果你明确地定义了子类,我会推荐你​​应用一些'method(any())。instrument(SuperMethodCall.INSTANCE)'什么让你稍后重新定义任何方法,如果你有机会的话。 –

+0

太棒了,非常感谢!我使用了一种稍微不同的方法来支持'Foo'接口,通过抛出所有超级方法调用的异常:'.method(ElementMatchers.any()).intercept(ExceptionMethod.throwing(RuntimeException.class));''它效果很好! –