2016-11-23 93 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));''它效果很好! –