«1. Обзор

В этой статье мы рассмотрим библиотеку cglib (библиотека генерации кода). Это библиотека инструментовки байтов, используемая во многих средах Java, таких как Hibernate или Spring. Инструментарий байт-кода позволяет манипулировать или создавать классы после этапа компиляции программы.

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

Чтобы использовать cglib в своем проекте, просто добавьте зависимость от Maven (последнюю версию можно найти здесь):

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.4</version>
</dependency>

3. Cglib

Классы в Java загружаются динамически в время выполнения. Cglib использует эту особенность языка Java, чтобы сделать возможным добавление новых классов в уже запущенную программу Java.

Hibernate использует cglib для генерации динамических прокси. Например, он не вернет полный объект, хранящийся в базе данных, но вернет инструментальную версию сохраненного класса, который лениво загружает значения из базы данных по запросу.

Популярные мок-фреймворки, такие как Mockito, используют cglib для мок-методов. Макет — это инструментированный класс, в котором методы заменены пустыми реализациями.

Мы рассмотрим наиболее полезные конструкции из cglib.

4. Реализация прокси с помощью cglib

Допустим, у нас есть класс PersonService с двумя методами:

public class PersonService {
    public String sayHello(String name) {
        return "Hello " + name;
    }

    public Integer lengthOfName(String name) {
        return name.length();
    }
}

Обратите внимание, что первый метод возвращает строку, а второй — целое число.

4.1. Возврат того же значения

Мы хотим создать простой прокси-класс, который будет перехватывать вызов метода sayHello(). Класс Enhancer позволяет нам создать прокси путем динамического расширения класса PersonService с помощью метода setSuperclass() из класса Enhancer:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(PersonService.class);
enhancer.setCallback((FixedValue) () -> "Hello Tom!");
PersonService proxy = (PersonService) enhancer.create();

String res = proxy.sayHello(null);

assertEquals("Hello Tom!", res);

FixedValue — это интерфейс обратного вызова, который просто возвращает значение из прокси-метода. Выполнение метода sayHello() на прокси-сервере вернуло значение, указанное в методе прокси.

4.2. Возвращаемое значение в зависимости от сигнатуры метода

Первая версия нашего прокси имеет некоторые недостатки, потому что мы не можем решить, какой метод прокси должен перехватывать и какой метод должен вызываться из суперкласса. Мы можем использовать интерфейс MethodInterceptor для перехвата всех вызовов прокси-сервера и решить, хотите ли вы сделать конкретный вызов или выполнить метод из суперкласса:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(PersonService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    if (method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
        return "Hello Tom!";
    } else {
        return proxy.invokeSuper(obj, args);
    }
});

PersonService proxy = (PersonService) enhancer.create();

assertEquals("Hello Tom!", proxy.sayHello(null));
int lengthOfName = proxy.lengthOfName("Mary");
 
assertEquals(4, lengthOfName);

В этом примере мы перехватываем все вызовы, когда сигнатура метода не из класса Object, что означает, что методы toString() или hashCode() не будут перехвачены. Кроме того, мы перехватываем только те методы из PersonService, которые возвращают String. Вызов метода lengthOfName() не будет перехвачен, поскольку его возвращаемый тип — целое число.

5. Bean Creator

Еще одна полезная конструкция из библиотеки cglib — это класс BeanGenerator. Это позволяет нам динамически создавать bean-компоненты и добавлять поля вместе с методами установки и получения. Он может использоваться инструментами генерации кода для создания простых объектов POJO:

BeanGenerator beanGenerator = new BeanGenerator();

beanGenerator.addProperty("name", String.class);
Object myBean = beanGenerator.create();
Method setter = myBean.getClass().getMethod("setName", String.class);
setter.invoke(myBean, "some string value set by a cglib");

Method getter = myBean.getClass().getMethod("getName");
assertEquals("some string value set by a cglib", getter.invoke(myBean));

6. Создание миксина

Миксин — это конструкция, которая позволяет объединять несколько объектов в один. Мы можем включить поведение пары классов и представить это поведение как один класс или интерфейс. Миксины cglib позволяют объединять несколько объектов в один объект. Однако для этого все объекты, включенные в миксин, должны поддерживаться интерфейсами.

Допустим, мы хотим создать миксин из двух интерфейсов. Нам нужно определить оба интерфейса и их реализации:

public interface Interface1 {
    String first();
}

public interface Interface2 {
    String second();
}

public class Class1 implements Interface1 {
    @Override
    public String first() {
        return "first behaviour";
    }
}

public class Class2 implements Interface2 {
    @Override
    public String second() {
        return "second behaviour";
    }
}

Чтобы скомпоновать реализации Interface1 и Interface2, нам нужно создать интерфейс, который расширяет их оба:

public interface MixinInterface extends Interface1, Interface2 { }

Используя метод create() из класса Класс Mixin мы можем включить поведение Class1 и Class2 в MixinInterface:

Mixin mixin = Mixin.create(
  new Class[]{ Interface1.class, Interface2.class, MixinInterface.class },
  new Object[]{ new Class1(), new Class2() }
);
MixinInterface mixinDelegate = (MixinInterface) mixin;

assertEquals("first behaviour", mixinDelegate.first());
assertEquals("second behaviour", mixinDelegate.second());

Вызов методов в mixinDelegate вызовет реализации из Class1 и Class2.

7. Заключение

В этой статье мы рассмотрели cglib и ее наиболее полезные конструкции. Мы создали прокси, используя класс Enhancer. Мы использовали BeanCreator и, наконец, создали Mixin, который включает поведение других классов.

«Cglib широко используется фреймворком Spring. Одним из примеров использования прокси-сервера cglib в Spring является добавление ограничений безопасности к вызовам методов. Вместо того, чтобы вызывать метод напрямую, безопасность Spring сначала проверит (через прокси), проходит ли указанная проверка безопасности, и делегирует фактическому методу, только если эта проверка прошла успешно. В этой статье мы увидели, как создать такой прокси для своих целей.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.