Перейти к основному содержимому

Динамические прокси в Java

· 4 мин. чтения

1. Введение

Эта статья посвящена динамическим прокси-серверам Java — одному из основных механизмов прокси-серверов, доступных нам в этом языке.

Проще говоря, прокси — это фронты или обертки, которые передают вызов функции через свои собственные средства (обычно на реальные методы) — потенциально добавляя некоторую функциональность.

Динамические прокси позволяют одному классу с одним единственным методом обслуживать несколько вызовов методов к произвольным классам с произвольным количеством методов. Динамический прокси можно рассматривать как своего рода Фасад , но такой, который может претендовать на реализацию любого интерфейса. Под прикрытием он направляет все вызовы методов одному обработчику — методу invoke() .

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

Эта функция встроена в стандартный JDK, поэтому никаких дополнительных зависимостей не требуется.

2. Обработчик вызова

Давайте создадим простой прокси, который на самом деле ничего не делает, кроме как печатает, какой метод был запрошен для вызова, и возвращает жестко запрограммированное число.

Во-первых, нам нужно создать подтип java.lang.reflect.InvocationHandler :

public class DynamicInvocationHandler implements InvocationHandler {

private static Logger LOGGER = LoggerFactory.getLogger(
DynamicInvocationHandler.class);

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
LOGGER.info("Invoked method: {}", method.getName());

return 42;
}
}

Здесь мы определили простой прокси, который записывает, какой метод был вызван, и возвращает 42.

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

Экземпляр прокси, обслуживаемый только что определенным обработчиком вызова, создается с помощью вызова фабричного метода класса java.lang.reflect.Proxy :

Map proxyInstance = (Map) Proxy.newProxyInstance(
DynamicProxyTest.class.getClassLoader(),
new Class[] { Map.class },
new DynamicInvocationHandler());

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

proxyInstance.put("hello", "world");

Как и ожидалось, сообщение о вызове метода put() распечатывается в файле журнала.

4. Обработчик вызовов через лямбда-выражения

Поскольку InvocationHandler — это функциональный интерфейс, можно определить встроенный обработчик с помощью лямбда-выражения:

Map proxyInstance = (Map) Proxy.newProxyInstance(
DynamicProxyTest.class.getClassLoader(),
new Class[] { Map.class },
(proxy, method, methodArgs) -> {
if (method.getName().equals("get")) {
return 42;
} else {
throw new UnsupportedOperationException(
"Unsupported method: " + method.getName());
}
});

Здесь мы определили обработчик, который возвращает 42 для всех операций получения и выдает исключение UnsupportedOperationException для всего остального.

Он вызывается точно так же:

(int) proxyInstance.get("hello"); // 42
proxyInstance.put("hello", "world"); // exception

5. Пример временного динамического прокси

Давайте рассмотрим один потенциальный реальный сценарий для динамических прокси.

Предположим, мы хотим записать, сколько времени требуется нашим функциям для выполнения. В связи с этим мы сначала определяем обработчик, способный оборачивать «настоящий» объект, отслеживать информацию о времени и отражающий вызов:

public class TimingDynamicInvocationHandler implements InvocationHandler {

private static Logger LOGGER = LoggerFactory.getLogger(
TimingDynamicInvocationHandler.class);

private final Map<String, Method> methods = new HashMap<>();

private Object target;

public TimingDynamicInvocationHandler(Object target) {
this.target = target;

for(Method method: target.getClass().getDeclaredMethods()) {
this.methods.put(method.getName(), method);
}
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
long start = System.nanoTime();
Object result = methods.get(method.getName()).invoke(target, args);
long elapsed = System.nanoTime() - start;

LOGGER.info("Executing {} finished in {} ns", method.getName(),
elapsed);

return result;
}
}

Впоследствии этот прокси можно использовать на различных типах объектов:

Map mapProxyInstance = (Map) Proxy.newProxyInstance(
DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class },
new TimingDynamicInvocationHandler(new HashMap<>()));

mapProxyInstance.put("hello", "world");

CharSequence csProxyInstance = (CharSequence) Proxy.newProxyInstance(
DynamicProxyTest.class.getClassLoader(),
new Class[] { CharSequence.class },
new TimingDynamicInvocationHandler("Hello World"));

csProxyInstance.length()

Здесь мы проксировали карту и последовательность символов (String).

Вызовы прокси-методов будут делегированы обернутому объекту, а также будут создавать операторы регистрации:

Executing put finished in 19153 ns 
Executing get finished in 8891 ns
Executing charAt finished in 11152 ns
Executing length finished in 10087 ns

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

В этом кратком руководстве мы рассмотрели динамические прокси-серверы Java, а также некоторые из возможных вариантов их использования.

Как всегда, код в примерах можно найти на GitHub .