«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 для всех get операций и выдает исключение 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.