JavaRush/Java блог/Java Developer/Динамические прокси (Dynamic Proxies) в Java
Автор
Jesse Haniel
Главный архитектор программного обеспечения в Tribunal de Justiça da Paraíba

Динамические прокси (Dynamic Proxies) в Java

Статья из группы Java Developer
участников
Привет! Сегодня мы рассмотрим достаточно важную и интересную тему — создание динамических прокси-классов в Java. Она не слишком простая, поэтому попробуем разобраться с ней на примерах :) Итак, самый важный вопрос: что такое динамические прокси и для чего они нужны? Прокси-класс — это некоторая «надстройка» над оригинальным классом, которая позволяет нам при необходимости изменить его поведение. Что значит «изменить поведение» и как это работает? Рассмотрим простой пример. Допустим, у нас есть интерфейс Person и простой класс Man, реализующий этот интерфейс
public interface Person {

   public void introduce(String name);

   public void sayAge(int age);

   public void sayFrom(String city, String country);
}

public class Man implements Person {

   private String name;
   private int age;
   private String city;
   private String country;

   public Man(String name, int age, String city, String country) {
       this.name = name;
       this.age = age;
       this.city = city;
       this.country = country;
   }

   @Override
   public void introduce(String name) {

       System.out.println("Меня зовут " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("Мне " + this.age + " лет");
   }

   @Override
   public void sayFrom(String city, String country) {

       System.out.println("Я из города " + this.city + ", " + this.country);
   }

   //..геттеры, сеттеры, и т.д.
}
У нашего класса Man есть 3 метода: представиться, назвать свой возраст, и сказать, откуда ты родом. Представим, что этот класс мы получили в составе готовой JAR-библиотеки и не можем просто взять и переписать его код. Тем не менее, нам нужно изменить его поведение. К примеру, мы не знаем, какой именно метод будет вызван у нашего объекта, а потому хотим, чтобы при вызове любого из них человек сначала говорил «Привет!» (никто не любит невежливых). Динамические прокси - 1Как же нам в такой ситуации поступить? Нам понадобятся несколько вещей:
  1. InvocationHandler

Что это такое? Можно перевести дословно — «перехватчик вызовов». Это довольно точно опишет его предназначение. InvocationHandler — это специальный интерфейс, который позволяет перехватить любые вызовы методов к нашему объекту и добавить нужное нам дополнительное поведение. Нам необходимо сделать собственный перехватчик — то есть, создать класс и реализовать этот интерфейс. Это довольно просто:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

private Person person;

public PersonInvocationHandler(Person person) {
   this.person = person;
}

 @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("Привет!");
       return null;
   }
}
Нам нужно реализовать всего один метод интерфейса — invoke(). Он, собственно, и делает то что нам нужно — перехватывает все вызовы методов к нашему объекту и добавляет необходимое поведение (здесь мы внутри метода invoke() выводим в консоль «Привет!»).
  1. Оригинальный объект и его прокси.
Создадим оригинальный объект Man и «надстройку» (прокси) для него:
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный объект
       Man vasia = new Man("Вася", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального объекта
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный объект
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего объекта vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси объекта один из методов нашего оригинального объекта
       proxyVasia.introduce(vasia.getName());

   }
}
Выглядит не очень просто! Я специально написал к каждой строке кода комментарий: давай разберемся подробнее, что там происходит.

В первой строке мы просто делаем оригинальный объект, для которого будем создавать прокси. Следующие две строки могут вызвать у тебя затруднение:
//Получаем загрузчик класса у оригинального объекта
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

//Получаем все интерфейсы, которые реализует оригинальный объект
Class[] interfaces = vasia.getClass().getInterfaces();
Но на самом деле ничего особенного здесь не происходит :) Для создания прокси нам нужен ClassLoader (загрузчик классов) оригинального объекта и список всех интерфейсов, которые реализует наш оригинальный класс (то есть Man). Если ты не знаешь что такое ClassLoader, можешь почитать эту статью о загрузке классов в JVM или эту на Хабре, но пока не особо с этим заморачивайся. Просто запомни, что мы получаем немного дополнительной информации, которая потом будет нужна для создания прокси-объекта. В четвертой строке мы используем специальный класс Proxy и его статический метод newProxyInstance():
//Создаем прокси нашего объекта vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Этот метод как раз создает наш прокси-объект. В метод мы передаем ту информацию об оригинальном классе, которую получили на прошлом шаге (его ClassLoader и список его интерфейсов), а также объект созданного нами ранее перехватчика — InvocationHandler’a. Главное — не забудь передать перехватчику наш оригинальный объект vasia, иначе ему нечего будет «перехватывать» :) Что же у нас в итоге получилось? У нас теперь есть прокси-объект vasiaProxy. Он может вызывать любые методы интерфейса Person. Почему? Потому что мы передали ему список всех интерфейсов — вот здесь:
//Получаем все интерфейсы, которые реализует оригинальный объект
Class[] interfaces = vasia.getClass().getInterfaces();

//Создаем прокси нашего объекта vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Теперь он «в курсе» всех методов интерфейса Person. Кроме того, мы передали нашему прокси объект PersonInvocationHandler, настроенный на работу с объектом vasia:
//Создаем прокси нашего объекта vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Теперь, если мы вызовем у прокси-объекта любой метод интерфейса Person, наш перехватчик «словит» этот вызов и выполнит вместо него свой метод invoke(). Давай попробуем запустить метод main()! Вывод в консоль: Привет! Отлично! Мы видим, что вместо настоящего метода Person.introduce() вызван метод invoke() нашего PersonInvocationHandler():
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Привет!");
   return null;
}
И в консоль было выведено «Привет!» Но это не совсем то поведение, которое мы хотели получить :/ По нашей задумке сначала должно быть выведено «Привет!», а после — сработать сам метод, который мы вызываем. Иными словами, вот этот вызов метода:
proxyVasia.introduce(vasia.getName());
должен выводить в консоль «Привет! Меня зовут Вася», а не просто «Привет!» Как же нам добиться этого? Ничего сложного: просто придется немного похимичить над нашим перехватчиком и методом invoke() :) Обрати внимание, какие аргументы передаются в этот метод:
public Object invoke(Object proxy, Method method, Object[] args)
У метода invoke() есть доступ к методу, вместо которого он вызывается, и ко всем его аргументам (Method method, Object[] args). Иными словами, если мы вызываем метод proxyVasia.introduce(vasia.getName()), и вместо метода introduce() вызывается метод invoke(), внутри этого метода у нас есть доступ и к оригинальному методу introduce(), и к его аргументу! В результате мы можем сделать вот так:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

   private Person person;

   public PersonInvocationHandler(Person person) {

       this.person = person;
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Привет!");
       return method.invoke(person, args);
   }
}
Теперь мы добавили в метод invoke() вызов оригинального метода. Если мы попробуем сейчас запустить код из нашего предыдущего примера:
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный объект
       Man vasia = new Man("Вася", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального объекта
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный объект
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего объекта vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси объекта один из методов нашего оригинального объекта
       proxyVasia.introduce(vasia.getName());
   }
}
то увидим, что теперь все работает как надо :) Вывод в консоль: Привет! Меня зовут Вася Где это может тебе понадобиться? На самом деле, много где. Паттерн проектирования «динамический прокси» активно используется в популярных технологиях...а я, кстати, и забыл тебе сказать, что Dynamic Proxy — это паттерн! Поздравляю, ты выучил еще один! :) Динамические прокси - 2Так вот, он активно используется в популярных технологиях и фреймворках, связанных с безопасностью. Представь, что у тебя есть 20 методов, которые могут выполнять только залогиненные пользователи твоей программы. С помощью изученных приемов ты легко сможешь добавить в эти 20 методов проверку того, ввел ли пользователь логин и пароль, не дублируя код проверки отдельно в каждом методе. Или, к примеру, если ты хочешь создать журнал, куда будут записываться все действия пользователей, это также легко сделать с использованием прокси. Можно даже сейчас: просто допиши в пример код, чтобы название метода выводилось в консоль при вызове invoke(), и ты получишь простенький журнал логов нашей программы :) В завершение лекции, обрати внимание на одно важное ограничение. Создание прокси объекта происходит на уровне интерфейсов, а не классов. Прокси создается для интерфейса. Взгляни на этот код:
//Создаем прокси нашего объекта vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Здесь мы создаем прокси именно для интерфейса Person. Если попробуем создать прокси для класса, то есть поменяем тип ссылки и попытаемся сделать приведение к классу Man, у нас ничего не выйдет.
Man proxyVasia = (Man) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

proxyVasia.introduce(vasia.getName());
Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to Man Наличие интерфейса — обязательное требование. Прокси работает на уровне интерфейсов. На этом на сегодня все :) В качестве дополнительного материала по теме прокси могу порекомендовать тебе отличное видео, и также неплохую статью. Ну, а теперь было бы неплохо решить несколько задач! :) До встречи!
Комментарии (96)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Евгений Усанов
Уровень 43
12 ноября 2023, 11:03
"У нас теперь есть прокси-объект vasiaProxy. Он может вызывать любые методы интерфейса Person. Почему?" Тут опечатка, правильно "У нас теперь есть прокси-объект proxyVasia" , а не vasiaProxy
PashaNPlay
Уровень 36
3 октября 2023, 21:16
Только в конце статьи обратил внимание на то, что ее автор иностранец. А тема зашла на ура, стоит отдельно поблагодарить переводчика!
Suzuya Jūzō
Уровень 44
28 июня 2023, 14:51
спасибо!
muradmur
Уровень 1
28 июня 2023, 01:36
крутой материал, спасибо
Андрей
Уровень 42
11 мая 2023, 12:55
спасибо
Mr. Implex
Уровень 34
14 декабря 2022, 22:45
Либо тут ошибка либо я не понял(хотя у себя проверил) В классе Main вызов proxyVasia.introduce(vasia.getName()); и я заметил и при проверке IDE ругнулась, что метода гет в классе Man НЕТ! И точно также при вызове любого другого метода. Объясните кто этот момент. Я пока вижу два решения , чтобы этот пример работал. Дописать return'ы в setName и тд. Или создать гетеры в классе Мan, но по условию переписывать класс у нас якобы нет возможности.
29 декабря 2022, 14:05
Посмотрите внимательно на класс Man (41 строка).
[M210] Java Developer
10 октября 2022, 15:58
т.е в итоге получаем простой враппер? Я же просто могу создать свой класс, реализующий такой же интерфейс, передать в конструкторе "Васю", и дергать в нем все его методы, добавляя перед методами свой код? Тоже самое же, только менее читабельный)) Да и вызывать приходится не getName, а какой то invoke, а потом думай, куда этот invoke тебя приведет)
Mike
Уровень 25
12 октября 2022, 13:31
Я могу ошибаться, но суть прокси, в том, что ты не контролируешь каждый метод и не контролируешь его поведение, ты его только проксируешь, добавляя какой то свой код. В случае же с враппером, ты делая обертку по сути вручную всем управляешь. Т.е. у тебя допустим уже есть некий скомпиленный код, который ты не можешь уже поменять и есть уже готовая реализация под него, возможно тоже чья-то, и чтобы допустим не лезть глобально в код, можно воспользоваться паттерном прокси, проксируя целиком вызовы, и выполняя какой то дополнительный код только в своем прокси.
[M210] Java Developer
14 октября 2022, 05:11
Все равно, мне кажется, я этим не буду пользоваться. Просто не вижу смысла)) Надо будет почитать про этот прокси побольше инфы
Виктор
Уровень 46
8 ноября 2022, 10:48
а если у тебя 100500 методов, перед каждым будешь "дергать"? Я согласен с ответом пользователя Mike
[M210] Java Developer
11 ноября 2022, 16:05
если у тебя 100500 методов и тебе нужно в каждый чтото добавить, то тут стоит задуматься, а все ли ты делаешь правильно?))) И может тогда стоит задуматься о смене архитектуры проги?
Виктор
Уровень 46
12 ноября 2022, 12:08
Задуматься стоит. И вот я думаю, что, как минимум можно говорить о том, что программу на проекте ты пишешь не один и не всегда есть возможность что-то поменять так как хочешь. Это не говоря уже о том объяснении, которое давалось в JR (с логином пользователей)
Серега Батенин
Уровень 34
9 января 2023, 12:13
А еще как я понял идет уже первое затрагивание такой темы, как рефлексия, а с помощью нее можно получать доступ к приватным полям и методам класса, к которым в иной ситуации нет возможности получить никакого доступа
Dmitriy Software Developer
3 июля 2023, 09:00
Ты если будешь работать java разрабом, то ты в вероятностью 95% придешь на легаси проект c миллионом строк кода и библиотеками, и тебе там не то что сменять архитектуру не дадут(это невозможно), но ты даже ветку main никогда не увидишь.
Viter
Уровень 38
30 ноября 2023, 12:52
А рифлексию мы вместо этого не можем использовать вместе с обетками?
proxylunae
Уровень 45
26 августа 2022, 12:57
proxy Vasya)))) Жирная отсылка на Эрго Прокси?) 🤔🤔
ArturZ
Уровень 42
28 июля 2022, 08:38
Спасибо автору, все стало понятно!
28 июня 2022, 13:58
Я правильно понимаю, что метод invoke может перехватывать только методы, которые есть в реализованных классом интерфейсах? Собственные методы класса он не видит?
On1k
Уровень 45
29 июня 2022, 19:43
если класс имплементирует интерфейс, то в интерфейсе не появляются методы, которые вы добавите в свой класс. А так как перехватчик приводится к типу интерфейса, то он не знает о существовании методов класса. Думаю, что вы правы, но это лишь мое скромное мнение)