Ошибка при переопределении метода с помощью 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
, Ваш класс развивается следующим образом:
Создание подкласса
class Bar$ByteBuddy extends Bar { Bar$ByteBuddy() { ... } }
Переопределение подкласса
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).