Dynamic Proxy

Java Collections
2 уровень , 7 лекция
Открыта

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

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

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

В 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, да?

— Ага.

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

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

Комментарии (133)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
{Java_Shark} Уровень 36
22 января 2025
++
Viter Уровень 38
9 декабря 2023
Я возможно что то не так понял, но что делать если оригинальный класс к которому у нас нет прямого доступа не имплементирует никаких интерфейсов или в его интерфейсах нет нужных нам методов? Разве в большенстве случаев это не будет именно так?
partiec Уровень 33
9 октября 2023
Тема сегодняшней лекции - корнуэльские пикси. То есть динамические прокси
Михаил Уровень 32
19 июня 2023
Achtung!!!! Данный материал был подан таким образjм, чтобы вы разрушили себе мозг вы сразу обратились к правильному материалу (спасибо Kurama от 14.12.2022) Перечитывать смысла нет - сразу лучше идти туда - там есть ответ на ClassCastException. От себя добавлю, что если унаследоваться от класса предка, который имплементирует, а наследник - нет, то тоже будет ClassCastException. Наследник обязан имплементировать. Прокси класс должен быть типа имплементируемого интерфейса.
PashaNPlay Уровень 41
3 октября 2023
Благодарю, действительно толковая статья, всем рекомендую ее изучить, даже если вам удалось все понять из лекции.
Мирослав Уровень 37 Expert
7 января 2025
Вроде тут - Данный материал был подан таким образjм, чтобы вы разрушили себе мозг вы сразу обратились к правильному материалу все сказал на 100% только тут - Перечитывать смысла нет - сразу лучше идти туда хочу поделиться : после просмотра добавочка для полности прочел 1 раз , но другими глазами! спс за доп!!!
Андрей Уровень 42
18 мая 2023
только начал отходить от структур данных, поданных через одно место,теперь это... 5 раз пытался прочесть. раза 4 смотрел кино про эту дичь. вообще никак не воспринимается. на каком-то моменте просто перестаю соображать и следить че происходит из-за постоянных отсылок к предыдущим шагам... а у кого-то же воспалилась картинка с этим всем, кто-то смог придумать.. пздц
Popka Уровень 24
22 июля 2023
Все нормально. Прокси - это паттерн. Т.е. это уже готовый костыль написанный опытными разрабами в процессе эволюции языка. Пытаться понять его сейчас глупо. Нужно просто его заучить. А понимание придет с опытом
dface Уровень 51
5 апреля 2023
Нужно упомянуть, что при создании прокси, объект ОБЯЗАН реализовывать интерфейс, действия которого и будут перехватываться нашим прокси. Иначе не работает. Т.е. прокси можно создать только у объекта, который РЕАЛИЗУЕТ ИНТЕРФЕЙС. Если у класса объекта есть методы написанные в классе, то они перехватываться не будут.
Rolik Уровень 41
10 мая 2023
Да, без реализации интерфейса никак, даже если он на не нужен в коде. Иначе конструктор создания proxy объекта не сложится. Вообше, рефлексия странная штука. Кто и зачем ее применяет на практике хз... Это как со стримами, они есть и они функциональнее циклов, но все равно большинство использует для сортировки циклы с условными операторами. Может и ошибаюсь. Поправьте.
Daniel Уровень 51
10 марта 2023
Лучшее объяснение из всех найденных мной приводится через AOP (аспектно ориентированное программирование). В отличии от ООП инкапсуляция происходит не на уровне объектов (все информация, связанная с машиной, и методы для работы с ней должны быть инкапсулированы в соответствующий класс), а на уровне аспектов (функций, которые выполняются). Выделяется основной аспект, он же бизнес-логика (полезная работа нашего класса), и сквозные аспекты (работа, которая никак не связана с нашим классом, но должна выполняться вместе с его бизнес-логикой). К примеру нам нужно логировать каждое действие, которое происходит с нашей машиной. Логирование никакого отношения к поведению машины (бизнес-логике) не имеет, но делать его все равно нужно. Добавить этот функционал мы можем через наследование (создать класс ЛогирующаясяМашина), через декоратор (через соответствующий паттерн) или через динамические прокси, про которые нам тут рассказывают. Первые два варианта не очень подходят для больших программ, потому что нам нужно будет сделать наследника и продублировать логику логирования для каждого класса/интерфейса. Это в разы увеличит и усложнит код и вообще не выход. А вот при помощи динамеческих прокси мы можем один раз написать этот класс (вся сквозная логика будет находиться в одном месте) и во всей программе при необходимости работать с объектами именно через эти прокси, которые будут перехватывать вызовы методов основного объекта, выполнять заложенную в них сквозную логику и потом выполнять вызванный метод основного класса. В сухом остатке мы получаем, что все эти танцы с бубном нам нужны для того чтобы разделить основную и служебную логику в нашей программе и не нарушать SRP из SOLID.
Антон Веснин Уровень 38 Expert
8 марта 2023
Exception in thread "main" java.lang.ClassCastException: class jdk.proxy1.$Proxy0 cannot be cast to class java.io.Reader (jdk.proxy1.$Proxy0 is in module jdk.proxy1 of loader 'app'; java.io.Reader is in module java.base of loader 'bootstrap') at Main.main(Main.java:21)
Vladimir Shmakov Уровень 37
8 марта 2023
Exception in thread -cannot be cast to class java.io.Reader . Proxy.newProxyInstance работает только с интерфейсом, или я не прав? - Returns a proxy instance for the specified interfaces that dispatches method invocations to the specified invocation handler.
Константин Уровень 51
15 февраля 2023
А теперь, чтобы закрепить пройденный материал, напишем приложение для выращивания картофеля на Марсе.