Привет! Сегодня мы рассмотрим достаточно важную и интересную тему: создание динамических прокси-классов в Java. Она не слишком простая, поэтому попробуем разобраться с ней на примерах :) Если нужен короткий ответ: динамический прокси это объект-надстройка, который Java создает прямо во время работы программы и который перехватывает вызовы методов оригинального объекта. Собирается он из двух частей: своего класса, реализующего
InvocationHandler, и вызова
Proxy.newProxyInstance(). Нужен он там, где к чужому классу надо добавить поведение, не трогая его код.
Кратко
- Динамический прокси создается во время работы программы и перехватывает вызовы методов оригинального объекта, поэтому чужой класс можно обвесить своим поведением, не переписывая его.
- Нужны две вещи: собственный класс, реализующий
InvocationHandler, и вызов статического метода Proxy.newProxyInstance(). - Все вызовы к прокси приходят в один-единственный метод
invoke(Object proxy, Method method, Object[] args). - Чтобы после вашей вставки отработал настоящий метод, внутри
invoke() вызывают method.invoke(оригинальныйОбъект, args). Без этой строки оригинал просто не выполнится. Proxy.newProxyInstance() принимает три вещи: загрузчик классов, массив интерфейсов и сам перехватчик.- Главное ограничение: прокси создается только для интерфейсов. Привести его к классу нельзя, получите
ClassCastException.
Что такое динамический прокси
Итак, самый важный вопрос: что такое динамические прокси и для чего они нужны?
Прокси-класс это некоторая «надстройка» над оригинальным классом, которая позволяет нам при необходимости изменить его поведение.
Что значит «изменить поведение» и как это работает? Рассмотрим простой пример.
Допустим, у нас есть интерфейс
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-библиотеки и не можем просто взять и переписать его код.
Тем не менее, нам нужно изменить его поведение. К примеру, мы не знаем, какой именно метод будет вызван у нашего объекта, а потому хотим, чтобы при вызове любого из них человек сначала говорил «Привет!» (никто не любит невежливых).
![Розовая неоновая вывеска в виде облачка с надписью hello на черном фоне]()
Как же нам в такой ситуации поступить?
Как создать динамический прокси
Нам понадобятся несколько вещей:
Шаг 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() выводим в консоль «Привет!»).
Шаг 2. Оригинальный объект и его прокси
Создадим оригинальный объект
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, иначе ему нечего будет «перехватывать» :) Если собрать все три аргумента вместе, получится так.
| Аргумент |
Что это |
Откуда взять в примере |
ClassLoader loader |
загрузчик, которым будет загружен сгенерированный класс прокси |
vasia.getClass().getClassLoader() |
Class<?>[] interfaces |
интерфейсы, которые прокси станет реализовывать. Перехватывать он сможет только их методы |
vasia.getClass().getInterfaces() |
InvocationHandler h |
перехватчик, в который попадут все вызовы. Оригинальный объект передают именно ему |
new PersonInvocationHandler(vasia) |
Что же у нас в итоге получилось?
У нас теперь есть прокси-объект
proxyVasia. Он может вызывать
любые методы интерфейса 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;
}
И в консоль было выведено «Привет!»
Как вызвать оригинальный метод из invoke()
Но это не совсем то поведение, которое мы хотели получить :/
По нашей задумке сначала должно быть выведено «Привет!», а после сработать сам метод, который мы вызываем.
Иными словами, вот этот вызов метода:
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 это реализация паттерна Proxy средствами самой Java!
Поздравляю, ты выучил еще один! :)
![Рука держит связку разноцветных воздушных шаров с лентами]()
Так вот, он активно используется в популярных технологиях и фреймворках, связанных с безопасностью.
Представь, что у тебя есть 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: class jdk.proxy1.$Proxy0 cannot be cast to class Man (jdk.proxy1.$Proxy0 is in module jdk.proxy1 of loader 'app'; Man is in unnamed module of loader 'app')
Само имя пакета документация не фиксирует, так что в разных версиях оно отличается: до Java 9 прокси-классы попадали в
com.sun.proxy, сейчас в отдельный модуль вида
jdk.proxy1. Суть сообщения от этого не меняется.
Наличие интерфейса это обязательное требование. Прокси работает на уровне интерфейсов.
На этом на сегодня все :)
В качестве дополнительного материала по теме прокси могу порекомендовать тебе отличное
видео, и также неплохую
статью.
Ну, а теперь было бы неплохо решить несколько задач! :)
До встречи!
Вопросы и ответы
Что такое динамический прокси в Java?
Это объект, который Java создает на лету, во время работы программы, и который подставляется вместо оригинального. Снаружи он выглядит как обычная реализация нужного интерфейса, но каждый вызов метода он переадресует вашему перехватчику. Так к чужому классу добавляют поведение, не имея доступа к его исходникам.
Чем динамический прокси отличается от обычного класса-обертки?
Обертку вы пишете руками, и в ней придется продублировать все методы интерфейса: три метода это три метода, двадцать это двадцать. Динамический прокси генерируется сам, а вся логика умещается в одном методе
invoke(), каким бы большим ни был интерфейс. Плата за это, потеря проверок на этапе компиляции и работа через рефлексию.
Что делает метод invoke() и какие у него аргументы?
В
invoke() попадает любой вызов метода прокси. Аргументов три: сам прокси-объект, объект
Method того метода, который пытались вызвать, и массив его аргументов. Благодаря второму и третьему аргументу перехватчик может не только добавить свое поведение, но и запустить оригинальный метод.
Как вызвать из invoke() оригинальный метод?
Через рефлексию:
return method.invoke(person, args);, где
person это оригинальный объект, переданный перехватчику в конструкторе. Если этой строки нет, а
invoke() возвращает
null, оригинальный метод просто не отработает, и в консоли вы увидите только свою вставку.
Почему прокси можно создать только для интерфейса?
Потому что
Proxy.newProxyInstance() генерирует класс, который реализует переданные интерфейсы, а наследником вашего класса он не является. Поэтому привести результат к
Man не выйдет, будет
ClassCastException, а к
Person выйдет. Если интерфейса у класса нет, штатный механизм не подойдет.
Где на практике применяются динамические прокси?
Везде, где одно и то же поведение надо навесить сразу на много методов. Проверка того, залогинен ли пользователь, запись действий в журнал, замер времени выполнения, кеширование, открытие и закрытие транзакции. На этом механизме держится изрядная часть популярных фреймворков, поэтому понимать его полезно даже до того, как вы возьметесь за них.
Читайте также
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
путьконтракт интерфейса. Если вызвать метод без аргумента, будет ошибка компиляции, изза того что мы не реализовали абстрактный метод: public void introduce(String name);Отличия и сходства паттернов Adapter, Decorator и Proxy:Различаются они тем, какой интерфейс предоставляют и что делают с функциональностью оригинального класса: Прокси оборачивает некоторый класс и предоставляет такой же интерфейс. Цель -- "притвориться" оригинальным классом и скрыть от клиента детали. Типичные примеры использования -- ленивая инициализация оборачиваемого класса или оборачивание вызовов стороннего сервиса Декоратор также оборачивает некоторый класс и предоставляет такой же или расширенный интерфейс. Иногда декоратор называют "умным заместителем" (smart proxy). Т.е. декоратор может притворяться оригинальным классом и при этом расширять его функциональность. Пример: у вас есть заместитель, который прячет вызовы к стороннему сервису. Можно создать декоратор, который будет оборачивать и кэшировать результаты вызовов. Другой пример: нужно расширить функциональность оригинального класса, но он закрыт для наследования. Создается декоратор, который расширяет интерфейс оригинального класса Адаптер также оборачивает некоторый класс, но при этом предоставляет другой интерфейс. Т.е. используется в случаях, когда есть класс с нужными данными и поведением, но с неподходящим интерфейсом | Шаблон | Что делает с интерфейсом | Что делает с функциональностью ------------------------------------------------------------------------------------------------------- | Прокси | Не изменяет | Не изменяет ---------------------------------------------------- | Декоратор | Не изменяет/расширяет | Расширяет ---------------------------------------------------------------------- | Адаптер | Изменяет | Не изменяет Взято с сайтакидатьvasia.getName(), если это не имеет никакого значения. Догадываюсь, что с помощью этой конструкции можно что-то подменять, но нам об этом не рассказаливключенаиспользуется, мы уже не можем, получается, сделать AOT-компиляцию?