Привет! Сегодня мы рассмотрим достаточно важную и интересную тему — создание динамических прокси-классов в Java. Она не слишком простая, поэтому попробуем разобраться с ней на примерах :)
Итак, самый важный вопрос: что такое динамические прокси и для чего они нужны?
Прокси-класс — это некоторая «надстройка» над оригинальным классом, которая позволяет нам при необходимости изменить его поведение.
Что значит «изменить поведение» и как это работает? Рассмотрим простой пример.
Допустим, у нас есть интерфейс
Как же нам в такой ситуации поступить?
Нам понадобятся несколько вещей:
Так вот, он активно используется в популярных технологиях и фреймворках, связанных с безопасностью.
Представь, что у тебя есть 20 методов, которые могут выполнять только залогиненные пользователи твоей программы. С помощью изученных приемов ты легко сможешь добавить в эти 20 методов проверку того, ввел ли пользователь логин и пароль, не дублируя код проверки отдельно в каждом методе.
Или, к примеру, если ты хочешь создать журнал, куда будут записываться все действия пользователей, это также легко сделать с использованием прокси.
Можно даже сейчас: просто допиши в пример код, чтобы название метода выводилось в консоль при вызове
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-библиотеки и не можем просто взять и переписать его код.
Тем не менее, нам нужно изменить его поведение. К примеру, мы не знаем, какой именно метод будет вызван у нашего объекта, а потому хотим, чтобы при вызове любого из них человек сначала говорил «Привет!» (никто не любит невежливых).

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()
выводим в консоль «Привет!»).
- Оригинальный объект и его прокси.
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
— это паттерн!
Поздравляю, ты выучил еще один! :)

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
Наличие интерфейса — обязательное требование. Прокси работает на уровне интерфейсов.
На этом на сегодня все :)
В качестве дополнительного материала по теме прокси могу порекомендовать тебе отличное видео, и также неплохую статью.
Ну, а теперь было бы неплохо решить несколько задач! :)
До встречи!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ