1. Обзор
Проще говоря, ByteBuddy - это библиотека для динамического создания классов Java во время выполнения.
В этой статье мы собираемся использовать среду для управления существующими классами, создания новых классов по требованию и даже перехвата вызовов методов.
2. зависимости
Давайте сначала добавим зависимость к нашему проекту. Для проектов на основе Maven нам нужно добавить эту зависимость в наш pom.xml :
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.7.1</version>
</dependency>
Для проекта на основе Gradle нам нужно добавить тот же артефакт в наш файл build.gradle :
compile net.bytebuddy:byte-buddy:1.7.1
Самую последнюю версию можно найти на Maven Central .
3. Создание класса Java во время выполнения
Давайте начнем с создания динамического класса путем создания подкласса существующего класса. Мы посмотрим на классический Hello World проект.
В этом примере мы создаем тип ( Class ), который является подклассом Object.class и переопределяем метод toString ()
DynamicType.Unloaded unloadedType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.isToString())
.intercept(FixedValue.value("Hello World ByteBuddy!"))
.make();
Мы только что создали экземпляр ByteBuddy. Затем мы использовали API subclass () для расширения Object.class и выбрали toString () суперкласса ( Object.class ) с помощью ElementMatchers__.
Наконец, с помощью метода intercept () мы предоставили нашу реализацию toString () и вернули фиксированное значение.
Метод make () запускает генерацию нового класса.
На данный момент наш класс уже создан, но еще не загружен в JVM. Он представлен экземпляром DynamicType.Unloaded , который является двоичной формой сгенерированного типа.
Поэтому нам нужно загрузить сгенерированный класс в JVM, прежде чем мы сможем его использовать:
Class<?> dynamicType = unloadedType.load(getClass()
.getClassLoader())
.getLoaded();
Теперь мы можем создать экземпляр dynamicType и вызвать для него метод toString () :
assertEquals(
dynamicType.newInstance().toString(), "Hello World ByteBuddy!");
Обратите внимание, что вызов dynamicType.toString () не будет работать, поскольку это вызовет только реализацию toString () ByteBuddy.class .
NewInstance () - это метод отражения Java, который создает новый экземпляр типа, представленного этим объектом ByteBuddy ; аналогично использованию ключевого слова new с конструктором без аргументов.
Пока что мы смогли переопределить только метод в суперклассе нашего динамического типа и вернуть собственное фиксированное значение. В следующих разделах мы рассмотрим определение нашего метода с помощью пользовательской логики.
4. Делегирование методов и пользовательская логика
В нашем предыдущем примере мы возвращаем фиксированное значение из метода toString () .
В действительности приложения требуют более сложной логики, чем эта. Одним из эффективных способов облегчения и предоставления настраиваемой логики динамическим типам является делегирование вызовов методов.
Давайте создадим динамический тип, который подклассов Foo.class , который имеет метод sayHelloFoo () :
public String sayHelloFoo() {
return "Hello in Foo!";
}
Кроме того, давайте создадим еще один класс Bar со статическим sayHelloBar () с той же сигнатурой и типом возврата, что и sayHelloFoo () :
public static String sayHelloBar() {
return "Holla in Bar!";
}
Теперь давайте делегируем все вызовы sayHelloFoo () в sayHelloBar () , используя DSL ByteBuddy ‘. Это позволяет нам предоставлять пользовательскую логику, написанную на чистом Java, нашему вновь созданному классу во время выполнения:
String r = new ByteBuddy()
.subclass(Foo.class)
.method(named("sayHelloFoo")
.and(isDeclaredBy(Foo.class)
.and(returns(String.class))))
.intercept(MethodDelegation.to(Bar.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance()
.sayHelloFoo();
assertEquals(r, Bar.sayHelloBar());
Вызов sayHelloFoo () вызовет sayHelloBar () соответственно.
-
Как ByteBuddy знает, какой метод в Bar.class вызывать? ** Он выбирает подходящий метод в соответствии с сигнатурой метода, типом возвращаемого значения, именем метода и аннотациями.
Методы sayHelloFoo () и sayHelloBar () не имеют одинакового имени, но имеют одинаковую сигнатуру метода и тип возвращаемого значения.
Если в Bar.class имеется более одного вызываемого метода с совпадающими сигнатурой и типом возвращаемого значения, мы можем использовать аннотацию @ BindingPriority для устранения неоднозначности.
@ BindingPriority принимает целочисленный аргумент - чем выше целочисленное значение, тем выше приоритет вызова конкретной реализации.
Таким образом, sayHelloBar () будет предпочтительнее sayBar () в приведенном ниже фрагменте кода:
@BindingPriority(3)
public static String sayHelloBar() {
return "Holla in Bar!";
}
@BindingPriority(2)
public static String sayBar() {
return "bar";
}
5. Определение метода и поля
Мы смогли переопределить методы, объявленные в суперклассе наших динамических типов. Давайте пойдем дальше, добавив новый метод (и поле) в наш класс.
Мы будем использовать отражение Java для вызова динамически создаваемого метода:
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.name("MyClassName")
.defineMethod("custom", String.class, Modifier.PUBLIC)
.intercept(MethodDelegation.to(Bar.class))
.defineField("x", String.class, Modifier.PUBLIC)
.make()
.load(
getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
Method m = type.getDeclaredMethod("custom", null);
assertEquals(m.invoke(type.newInstance()), Bar.sayHelloBar());
assertNotNull(type.getDeclaredField("x"));
Мы создали класс с именем MyClassName , который является подклассом Object.class . Затем мы определяем метод custom, , который возвращает String и имеет модификатор доступа public .
Как и в предыдущих примерах, мы реализовали наш метод, перехватывая вызовы к нему и делегируя их Bar.class , который мы создали ранее в этом уроке.
6. Переопределение существующего класса
Хотя мы работали с динамически создаваемыми классами, мы можем работать и с уже загруженными классами. Это может быть сделано путем переопределения (или перебазирования) существующих классов и использования ByteBuddyAgent для их перезагрузки в JVM.
Во-первых, давайте добавим ByteBuddyAgent в ваш pom.xml:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.7.1</version>
</dependency>
Последней версией может быть foundound здесь .
Теперь давайте переопределим метод sayHelloFoo () , который мы создали ранее в Foo.class :
ByteBuddyAgent.install();
new ByteBuddy()
.redefine(Foo.class)
.method(named("sayHelloFoo"))
.intercept(FixedValue.value("Hello Foo Redefined"))
.make()
.load(
Foo.class.getClassLoader(),
ClassReloadingStrategy.fromInstalledAgent());
Foo f = new Foo();
assertEquals(f.sayHelloFoo(), "Hello Foo Redefined");
7. Заключение
В этом сложном руководстве мы подробно рассмотрели возможности библиотеки ByteBuddy и ее использование для эффективного создания динамических классов.
Http://bytebuddy.net/#/tutorial[documentation]предлагает подробное объяснение внутренней работы и других аспектов библиотеки
И, как всегда, полные фрагменты кода для этого руководства можно найти по адресу over на Github .