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