Перейти к основному содержимому

Руководство по Byte Buddy

· 5 мин. чтения

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 .