— Привет, Амиго.

— Здорово, Риша.

— Сегодня я расскажу тебе новую и очень интересную тему — динамические прокси.

В Java есть несколько способов изменить функциональность нужного класса…

Способ первый. Наследование

Самый простой способ изменить поведение некоторого класса — это создать новый класс, унаследовать его от оригинального (базового) и переопределить его методы. Затем, вместо объектов оригинального класса использовать объекты класса наследника. Пример:

Reader reader = new UserCustomReader();

Способ второй. Использование класса-обертки (Wrapper).

Примером такого класса является BufferedReader. Во-первых, он унаследован от Reader, то есть может быть использован вместо него. Во-вторых, он переадресует все вызовы к оригинальному объекту Reader, который обязательно нужно передать в конструкторе объекту BufferedReader. Пример:

Reader readerOriginal = new UserCustomReader();
Reader reader = new BufferedReader(readerOriginal);

Способ третий. Создание динамического прокси (Proxy).

Dynamic Proxy - 1

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

Это делается очень просто:

Reader reader = (Reader)Proxy.newProxyInstance();

— А вот это уже что-то новенькое!

— Но, нам ведь не нужен просто объект без методов. Надо чтобы у этого объекта были методы, и они делали то, что нам нужно. Для этого в Java используется специальный интерфейс InvocationHandler, с помощью которого можно перехватывать все вызовы методов, обращенные к proxy-объекту. proxy-объект можно создать только используя интерфейсы.

Invoke – стандартное название для метода/класса, основная задача которого просто вызвать какой-то метод.

Handler – стандартное название для класса, который обрабатывает какое-то событие. Например, обработчик клика мышки будет называться MouseClickHandler, и т.д.

У интерфейса InvocationHandler есть единственный метод invoke, в который направляются все вызовы, обращенные к proxy-объекту. Пример:

Код
Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler());
reader.close();
class CustomInvocationHandler implements InvocationHandler
{
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  System.out.println("yes!");
  return null;
 }
}

При вызове метода reader.close(), вызовется метод invoke, и на экран будет выведена надпись “yes!”

— Т.е. мы объявили класс CustomInvocationHandler, в нем реализовали интерфейс InvocationHandler и его метод invoke. Метод invoke при вызове выводит на экран строку “yes!”- Затем мы создали объект типа CustomInvocationHandler и передали его в метод newProxyInstance при создании объекта-proxy.

— Да, все верно.

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

– в таком методе можно проверять права текущего пользователя, обрабатывать ошибки, логировать ошибки и многое другое.

Вот пример, где метод invoke еще и вызывает методы оригинального объекта:

Код
Reader original = new UserCustomReader();

Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler(original));
reader.close();
class CustomInvocationHandler implements InvocationHandler
{
 private Reader readerOriginal;

 CustomInvocationHandler(Reader readerOriginal)
 {
  this.readerOriginal = readerOriginal;
 }

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  if (method.getName().equals("close"))
  {
   System.out.println("Reader closed!");
  }

  // это вызов метода close у объекта readerOriginal
  // имя метода и описание его параметров хранится в переменной method
  return method.invoke(readerOriginal, args);
 }
}

В данном примере есть две особенности.

Во-первых, в конструктор передается «оригинальный» объект Reader, ссылка на который сохраняется внутри CustomInvocationHandler.

Во-вторых, в методе invoke мы снова вызываем этот же метод, но уже у «оригинального» объекта.

— Ага. Т.е. вот эта последняя строчка и есть вызов того же самого метода, но уже у оригинального объекта:

return method.invoke(readerOriginal, args);

— Ага.

— Не сказал бы, что слишком очевидно, но все же понятно. Вроде бы.

— Отлично. Тогда вот еще что. В метод newProxyInstance нужно передавать еще немного служебной информации для создания proxy-объекта. Но, т.к. мы не создаем монструозные прокси-объекты, то эту информацию легко получить из самого оригинального класса.

Вот тебе пример:

Код
Reader original = new UserCustomReader();

ClassLoader classLoader = original.getClass().getClassLoader();
Class<?>[] interfaces = original.getClass().getInterfaces();
CustomInvocationHandler invocationHandler = new CustomInvocationHandler(original);

Reader reader = (Reader)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
class CustomInvocationHandler implements InvocationHandler
{
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  return null;
 }
}

— Ага. ClassLoader и список интерфейсов. Это что-то из Reflection, да?

— Ага.

— Ясно. Что ж, думаю, я смогу создать примитивный простенький прокси объект, если это когда-нибудь мне понадобится.

— А ты спроси у Диего