Вітання! Сьогодні ми розглянемо досить важливу та цікаву тему – створення динамічних проксі-класів у 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-бібліотеки і не можемо просто взяти та переписати його код. Тим не менш, нам потрібно змінити його поведінку. Наприклад, ми не знаємо, який саме метод буде викликаний у нашого об'єкта, а тому хочемо, щоб при виклику будь-якої з них людина спочатку говорила «Привіт!» (Ніхто не любить неввічливих). Як же нам у такій ситуації вчинити? Нам знадобиться кілька речей:
-
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
це патерн ! Вітаю, ти вивчив ще одну! :) Так ось, він активно використовується в популярних технологіях та фреймворках, пов'язаних із безпекою. Уяви, що ти маєш 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 Наявність інтерфейсу - обов'язкова вимога. Проксі працює на рівні інтерфейсів. На цьому на сьогодні все :) Як додатковий матеріал по темі проксі можу порекомендувати тобі відмінне відео , а також непогану статтю . Ну а тепер було б непогано вирішити кілька завдань! :) До зустрічі!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ