Руководство по Байту Бадди

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 .