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 .