2016-11-23 4 views
1

Я учусь Byte Бадди, и я пытаюсь сделать следующее:Ошибки при переопределении метода с 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() { ... } 
    } 
    

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

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

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

+0

Спасибо за ваш отзыв, Рафаэль! Но я немного смущен, потому что я надеялся сделать что-то похожее на то, как работает Mockito: т. Е. Сначала определить класс «Mock», а затем настроить поведение. Другими словами, существует ли способ избежать загрузки подкласса перед заменой метода? –

+0

В этом случае вам нужно пройти иерархию классов «Бар» и найти первый класс, объявляющий метод. Это то, что мы делаем в Мокито. В этом методе мы затем добавляем код, похожий на: 'if (this instanceof SomeClass)', чтобы решить, следует ли отправлять код. Если вы явно определите подклассы, я бы рекомендовал вам применить некоторый метод method (any()). (SuperMethodCall.INSTANCE), что позволяет вам позже переопределить любой метод, если у вас есть такая возможность. –

+0

Удивительный, спасибо большое! Я использовал несколько иной подход для поддержки интерфейса Foo, вызывая исключение для всех вызовов супер-методов: '.method (ElementMatchers.any()). Intercept (ExceptionMethod.throwing (RuntimeException.class)); и он отлично работает! –