JavaRush /Java блог /Random UA /Динамічні проксі (Dynamic Proxies) у Java

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

Стаття з групи Random UA
Вітання! Сьогодні ми розглянемо досить важливу та цікаву тему – створення динамічних проксі-класів у 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 Наявність інтерфейсу - обов'язкова вимога. Проксі працює на рівні інтерфейсів. На цьому на сьогодні все :) Як додатковий матеріал по темі проксі можу порекомендувати тобі відмінне відео , а також непогану статтю . Ну а тепер було б непогано вирішити кілька завдань! :) До зустрічі!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ