Статьи

Реализация динамических прокси — сравнение

Иногда возникает необходимость перехватить вызовы определенных методов, чтобы каждый раз вызывать перехваченный метод для выполнения собственной логики. Если вы не находитесь в мире CDI Java EE и не хотите использовать AOP-инфраструктуры, такие как aspectj, у вас есть простая и похожая эффективная альтернатива.

Начиная с версии 1.5, JDK поставляется с классом java.lang.reflect.Proxy, который позволяет вам создавать динамический прокси для данного интерфейса. InvocationHandler, который находится за динамически созданным классом, вызывается каждый раз, когда приложение вызывает метод на прокси. Следовательно, вы можете динамически контролировать, какой код будет выполняться до вызова кода какой-либо платформы или библиотеки.

Наряду с реализацией JDK Proxy, такие как javassist или cglib, предлагают аналогичные функциональные возможности. Здесь вы можете даже создать подкласс существующего класса и решить, какие методы вы хотите переслать в реализацию суперкласса и какие методы вы хотите перехватить. Это, конечно, связано с бременем другой библиотеки, от которой зависит ваш проект, и, возможно, ее придется время от времени обновлять, в то время как реализация JDK Proxy уже включена в среду выполнения.

Итак, давайте поближе познакомимся с этими тремя альтернативами. Чтобы сравнить прокси javassist и cglib с реализацией JDK, нам нужен интерфейс, который реализуется простым классом, потому что механизм JDK поддерживает только интерфейсы и не имеет подклассов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public interface IExample {
    void setName(String name);
}
 
public class Example implements IExample {
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

Чтобы делегировать вызовы метода на прокси-объекту какому-либо реальному объекту, мы создаем экземпляр класса Example выше и вызываем его в InvocationHandler через последнюю объявленную переменную:

1
2
3
4
5
6
7
8
final Example example = new Example();
InvocationHandler invocationHandler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(example, args);
    }
};
return (IExample) Proxy.newProxyInstance(JavaProxy.class.getClassLoader(), new Class[]{IExample.class}, invocationHandler);

Как видно из примера кода, создание прокси довольно просто: вызовите статический метод newProxyInstance () и предоставьте ClassLoader, массив интерфейсов, которые должны быть реализованы прокси, а также экземпляр интерфейса InvocationHandler. Наша реализация направлена ​​ради демонстрации только того примера, который мы создали ранее. Но в реальной жизни вы, конечно, можете выполнять более сложные операции, которые оценивают, например, имя метода или его аргументы.

Теперь посмотрим, как это делается с помощью javassist:

01
02
03
04
05
06
07
08
09
10
11
12
ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(Example.class);
Class aClass = factory.createClass();
final IExample newInstance = (IExample) aClass.newInstance();
MethodHandler methodHandler = new MethodHandler() {
    @Override
    public Object invoke(Object self, Method overridden, Method proceed, Object[] args) throws Throwable {
        return proceed.invoke(newInstance, args);
    }
};
((ProxyObject)newInstance).setHandler(methodHandler);
return newInstance;

Здесь у нас есть ProxyFactory, которая хочет знать, для какого класса она должна создавать подкласс. Затем мы позволяем ProxyFactory создать целый класс, который можно использовать столько раз, сколько необходимо. Здесь MethodHandler является аналогом InvocationHandler, который вызывается для каждого вызова метода экземпляра. Здесь мы снова пересылаем вызов экземпляру Example, который мы создали ранее.

И последнее, но не менее важное: давайте посмотрим на прокси-сервер cglib:

1
2
3
4
5
6
7
8
final Example example = new Example();
IExample exampleProxy = (IExample) Enhancer.create(IExample.class, new MethodInterceptor() {
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        return method.invoke(example, args);
    }
});
return exampleProxy;

В мире cglib у нас есть класс Enhancer, который мы можем использовать для реализации данного интерфейса с экземпляром MethodInterceptor. Реализация метода обратного вызова выглядит очень похоже на ту, что была в примере с javassist. Мы просто пересылаем вызов метода через API отражения в уже существующий экземпляр Example.

Теперь, когда мы увидели три разных реализации, мы также хотим оценить их поведение во время выполнения. Поэтому мы пишем простой модульный тест, который измеряет время выполнения каждой из этих реализаций:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Test
public void testPerformance() {
    final IExample example = JavaProxy.createExample();
    long measure = TimeMeasurement.measure(new TimeMeasurement.Execution() {
        @Override
        public void execute() {
            for (long i = 0; i < JavassistProxyTest.NUMBER_OF_ITERATIONS; i++) {
                example.setName("name");
            }
        }
    });
    System.out.println("Proxy: "+measure+" ms");
}

Мы выбираем огромное количество итераций, чтобы подчеркнуть JVM и позволить компилятору HotSpot создавать собственный код для часто выполняемых отрывков. Следующая диаграмма показывает среднее время выполнения трех реализаций:

2014-01-14_proxies_chart

Чтобы показать влияние реализации Proxy на диаграмме, также показано время выполнения стандартного вызова метода для объекта Example («No proxy»). Прежде всего, мы можем отметить, что реализации прокси примерно в 10 раз медленнее, чем простой вызов самого метода. Но мы также замечаем разницу между тремя решениями прокси. Класс JDK Proxy на удивление почти так же быстр, как и реализация cglib. Только javassist вытягивает примерно вдвое больше времени cglib.

Вывод: прокси-серверы времени выполнения просты в использовании, и у вас есть другой способ сделать это. Прокси JDK поддерживает только прокси для интерфейсов, тогда как javassist и cglib позволяют создавать подклассы для существующих классов. Поведение прокси во время выполнения примерно в 10 раз медленнее, чем при обычном вызове метода. Три решения также отличаются с точки зрения времени выполнения.