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

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

1. Вступление

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

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

Динамические прокси позволяют одному отдельному классу с одним единственным методом обслуживать несколько вызовов методов для произвольных классов с произвольным числом методов. Динамический прокси-сервер можно рассматривать как своего родаFacade, но он может претендовать на роль реализации любого интерфейса. Под прикрытиемit routes all method invocations to a single handler - метод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 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, а также некоторые их возможные варианты использования.

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