1. Обзор
Проще говоря, ByteBuddy — это библиотека для динамического создания классов Java во время выполнения.
В этой статье мы собираемся использовать инфраструктуру для управления существующими классами, создания новых классов по запросу и даже для перехвата вызовов методов.
2. Зависимости
Давайте сначала добавим зависимость в наш проект. Для проектов на основе Maven нам нужно добавить эту зависимость в наш pom.xml
:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.11.20</version>
</dependency>
Для проекта на основе Gradle нам нужно добавить тот же артефакт в наш файл build.gradle
:
compile net.bytebuddy:byte-buddy:1.11.20
Последнюю версию можно найти на 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,
который возвращает строку
и имеет модификатор общего
доступа.
Как и в предыдущих примерах, мы реализовали наш метод, перехватив его вызовы и делегировав их классу Bar.class
, который мы создали ранее в этом руководстве.
6. Переопределение существующего класса
Хотя мы работали с динамически созданными классами, мы можем работать и с уже загруженными классами. Это можно сделать, переопределив (или перебазировав) существующие классы и используя ByteBuddyAgent
для перезагрузки их в JVM.
Во-первых, давайте добавим ByteBuddyAgent
в наш pom.xml
:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.7.1</version>
</dependency>
Последнюю версию можно найти здесь .
Теперь давайте переопределим метод 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
и способы ее использования для эффективного создания динамических классов.
Его документация предлагает подробное объяснение внутренней работы и других аспектов библиотеки.
И, как всегда, полные фрагменты кода для этого руководства можно найти на Github .