Рубрики
Без рубрики

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

Быстрый и практический пример использования ByteBuddy – инструмента для создания классов во время выполнения.

Автор оригинала: baeldung.

1. Обзор

Проще говоря, ByteBuddy -это библиотека для динамического создания классов Java во время выполнения.

В этой статье мы будем использовать фреймворк для управления существующими классами, создания новых классов по требованию и даже перехвата вызовов методов.

2. Зависимости

Давайте сначала добавим зависимость в наш проект. Для проектов на основе Maven нам необходимо добавить эту зависимость в ваш pom.xml :


    net.bytebuddy
    byte-buddy
    1.10.22

Для проекта на основе Gradle нам нужно добавить тот же артефакт в наш файл build.gradle :

compile net.bytebuddy:byte-buddy:1.10.22

Последнюю версию можно найти на Maven Central .

3. Создание класса Java во время выполнения

Давайте начнем с создания динамического класса путем подклассов существующего класса. Мы рассмотрим классический проект Hello World .

В этом примере мы создаем тип ( Класс ), который является подклассом Object.class и переопределить метод toString() :

DynamicType.Unloaded unloadedType = new ByteBuddy()
  .subclass(Object.class)
  .method(ElementMatchers.isToString())
  .intercept(FixedValue.value("Hello World ByteBuddy!"))
  .make();

То, что мы только что сделали, – это создали экземпляр ByteBuddy. Затем мы использовали API подкласса () для расширения Object.class , и мы выбрали toString() суперкласса ( Object.class ) использование ElementMatchers .

Наконец, с помощью метода intercept() мы предоставили нашу реализацию toString() и вернули фиксированное значение.

Метод make() запускает генерацию нового класса.

На данный момент наш класс уже создан, но еще не загружен в JVM. Он представлен экземпляром динамического типа .Выгруженный , который является двоичной формой сгенерированного типа.

Поэтому нам нужно загрузить сгенерированный класс в JVM, прежде чем мы сможем его использовать:

Class dynamicType = unloadedType.load(getClass()
  .getClassLoader())
  .getLoaded();

Теперь мы можем создать экземпляр динамического типа и вызвать на нем метод toString() :

assertEquals(
  dynamicType.newInstance().toString(), "Hello World ByteBuddy!");

Обратите внимание, что вызов динамического типа.toString() не будет работать, так как это вызовет только toString() реализацию ByteBuddy.class .

new Instance () – это метод отражения Java, который создает новый экземпляр типа, представленного этим объектом ByteBuddy ; аналогично использованию ключевого слова new с конструктором no-arg.

|| new Instance () || – это метод отражения Java, который создает новый экземпляр типа, представленного этим объектом || ByteBuddy||; аналогично использованию ключевого слова || new || с конструктором no-arg.

|| new Instance () || – это метод отражения Java, который создает новый экземпляр типа, представленного этим объектом || ByteBuddy||; аналогично использованию ключевого слова || new || с конструктором no-arg.

В нашем предыдущем примере мы возвращаем фиксированное значение из метода toString () .

На самом деле приложения требуют более сложной логики, чем эта. Одним из эффективных способов облегчения и предоставления настраиваемой логики динамическим типам является делегирование вызовов методов.

Давайте создадим динамический тип, который подклассы Foo.class который имеет метод sayhelloto() :

public String sayHelloFoo() { 
    return "Hello in Foo!"; 
}

Кроме того, давайте создадим другой класс Bar со статическим say Hello Bar() с той же сигнатурой и типом возврата, что и sayHelloFoo() :

public static String sayHelloBar() { 
    return "Holla in Bar!"; 
}

Теперь давайте делегируем все вызовы sayhelloto() в say Hello Bar() с помощью ByteBuddy ‘sDSL. Это позволяет нам предоставлять пользовательскую логику, написанную на чистом 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());

Вызов say Hello Foo() вызовет say Hello Bar() соответственно.

Как ByteBuddy знает, какой метод в Bar.class чтобы вызвать? Он выбирает соответствующий метод в соответствии с сигнатурой метода, типом возвращаемого значения, именем метода и аннотациями.

Методы sayhelloto() и say Hello Var() не имеют одного и того же имени, но имеют одинаковую сигнатуру метода и тип возвращаемого значения.

Если в имеется более одного вызываемого метода Bar.class с соответствующей подписью и типом возвращаемого значения мы можем использовать @BindingPriority аннотацию для устранения неоднозначности.

@BindingPriority принимает целочисленный аргумент – чем выше целочисленное значение, тем выше приоритет вызова конкретной реализации. Таким образом, say Hello Bar() будет предпочтительнее swaybar() в приведенном ниже фрагменте кода:

@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, , который возвращает Строку и имеет модификатор public access.

Как и в предыдущих примерах, мы реализовали наш метод, перехватывая вызовы к нему и делегируя их Bar.class , который мы создали ранее в этом уроке.

6. Переопределение существующего класса

Хотя мы работали с динамически созданными классами, мы можем работать и с уже загруженными классами. Это можно сделать, переопределив (или перебазировав) существующие классы и используя Byte Buddy Agent для перезагрузки их в JVM.

Во-первых, давайте добавим Byte Buddy Agent к вашему pom.xml :


    net.bytebuddy
    byte-buddy-agent
    1.7.1

Последнюю версию можно найти здесь .

Теперь давайте переопределим метод sayhelloto () , который мы создали в 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 .