Привет! Сегодня мы рассмотрим достаточно важную и интересную тему: создание динамических прокси-классов в 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();
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():
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();
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Теперь он «в курсе» всех методов интерфейса
Person.
Кроме того, мы передали нашему прокси объект
PersonInvocationHandler, настроенный на работу с объектом
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();
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
proxyVasia.introduce(vasia.getName());
}
}
то увидим, что теперь все работает как надо :)
Вывод в консоль:
Привет!
Меня зовут Вася
Где применяются динамические прокси
Где это может тебе понадобиться? На самом деле, много где.
Паттерн проектирования «динамический прокси» активно используется в популярных технологиях...а я, кстати, и забыл тебе сказать, что
Dynamic Proxy это реализация паттерна Proxy средствами самой Java!
Поздравляю, ты выучил еще один! :)
![Рука держит связку разноцветных воздушных шаров с лентами]()
Так вот, он активно используется в популярных технологиях и фреймворках, связанных с безопасностью.
Представь, что у тебя есть 20 методов, которые могут выполнять только залогиненные пользователи твоей программы. С помощью изученных приемов ты легко сможешь добавить в эти 20 методов проверку того, ввел ли пользователь логин и пароль, не дублируя код проверки отдельно в каждом методе.
Или, к примеру, если ты хочешь создать журнал, куда будут записываться все действия пользователей, это также легко сделать с использованием прокси.
Можно даже сейчас: просто допиши в пример код, чтобы название метода выводилось в консоль при вызове
invoke(), и ты получишь простенький журнал логов нашей программы :)
Важное ограничение: прокси создается для интерфейса
В завершение лекции,
обрати внимание на одно важное ограничение.
Создание прокси объекта происходит на уровне
интерфейсов, а не классов. Прокси создается для интерфейса.
Взгляни на этот код:
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);