— Сьогодні я поясню тобі нову й дуже цікаву тему — динамічні проксі.

У Java є кілька способів змінити функціональність потрібного класу…

Спосіб перший. Успадкування

Найпростіший спосіб змінити поведінку певного класу – це створити новий клас, успадкувати його від оригінального (базового) та перевизначити його методи. Потім, замість об'єктів оригінального класу, використовувати об'єкти класу-нащадка. Приклад:

Reader reader = new UserCustomReader();

Спосіб другий. Використання класу-обгортки (Wrapper)

Прикладом такого класу є BufferedReader. По-перше, він успадкований від Reader, тобто може бути використаним замість нього. По-друге, він переадресує всі виклики до оригінального об'єкта Reader, який обов'язково потрібно передати в конструкторі об'єкту BufferedReader. Приклад:

Reader readerOriginal = new UserCustomReader();
Reader reader = new BufferedReader(readerOriginal);

Спосіб третій. Створення динамічного проксі (Proxy)

Dynamic Proxy - 1

У Java є спеціальний клас (java.lang.reflect.Proxy), за допомогою якого фактично можна сконструювати об'єкт під час виконання програми (динамічно), без створення для нього окремого класу.

Це можна зробити дуже просто:

Reader reader = (Reader)Proxy.newProxyInstance();

— А ось це вже щось новеньке!

— Але ж нам не потрібен просто об'єкт без методів. Треба щоб цей об'єкт мав методи, і вони робили те, що нам необхідно. Для цього в Java використовується спеціальний інтерфейс InvocationHandler, за допомогою якого можна перехопити всі виклики методів, що звернені до proxy-об'єкта. Proxy-об'єкт можна створити лише за допомогою інтерфейсів.

Invoke – стандартна назва для методу/класу, чиє основне завдання — просто викликати якийсь метод.

Handler – стандартна назва для класу, який опрацьовує певну подію. Наприклад, обробник кліку мишки буде називатися MouseClickHandler тощо.

Інтерфейс InvocationHandler має єдиний метод invoke, до якого надсилаються всі виклики, що звернені до proxy-об'єкту. Приклад:

Код
Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler());
reader.close();
class CustomInvocationHandler implements InvocationHandler
{
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  System.out.println("yes!");
  return null;
 }
}

Під час виклику методу reader.close() викликається метод invoke, і на екран виведеться надпис “yes!”

— Тобто ми оголосили клас CustomInvocationHandler, у ньому реалізували інтерфейс InvocationHandler та його метод invoke, який виводить на екран рядок “yes!”. Потім ми створили об'єкт типу CustomInvocationHandler і передали його до методу newProxyInstance при створенні об'єкта-proxy.

— Так, усе правильно.

Це дуже потужний інструмент. Зазвичай створення таких проксі використовують для імітації об'єктів із програм, які фізично запустили на іншому комп'ютері. Або для контроля доступу.

У такому методі можна перевіряти права поточного користувача, обробляти помилки, логувати помилки тощо.

Ось приклад, де метод invoke ще й викликає методи оригінального об'єкту:

Код
Reader original = new UserCustomReader();

Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler(original));
reader.close();
class CustomInvocationHandler implements InvocationHandler
{
 private Reader readerOriginal;

 CustomInvocationHandler(Reader readerOriginal)
 {
  this.readerOriginal = readerOriginal;
 }

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  if (method.getName().equals("close"))
  {
   System.out.println("Reader closed!");
  }

  // це виклик методу close в об'єкта readerOriginal
  // ім'я методу й опис його параметрів зберігається у змінній method
  return method.invoke(readerOriginal, args);
 }
}

У цьому прикладі є дві особливості.

По-перше, до конструктора передається «оригінальний» об'єкт Reader, посилання на яке зберігається всередині CustomInvocationHandler.

По-друге, в методі invoke ми знову викликаємо цей же метод, але вже в «оригінального» об'єкта.

— Ага. Тобто цей останній рядок і є викликом того ж самого методу, але вже в оригінального об'єкта:

return method.invoke(readerOriginal, args);

— Так.

— Це не те щоб дуже очевидно, але загалом зрозуміло. Начебто.

— Чудово. Тоді ось ще дещо. До методу newProxyInstance потрібно передавати ще трохи службової інформації для створення proxy-об'єкта. Але оскільки ми не створюємо монструозні проксі-об'єкти, цю інформацію легко отримати із самого ж оригінального класу.

Ось тобі приклад:

Код
Reader original = new UserCustomReader();

ClassLoader classLoader = original.getClass().getClassLoader();
Class<?>[] interfaces = original.getClass().getInterfaces();
CustomInvocationHandler invocationHandler = new CustomInvocationHandler(original);

Reader reader = (Reader)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
class CustomInvocationHandler implements InvocationHandler
{
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
 {
  return null;
 }
}

— Ага. ClassLoader і список інтерфейсів. Це щось із Reflection, так?

— Саме так.

— Ясно. Що ж, думаю, я зможу створити примітивний проксі-об'єкт, якщо це колись мені знадобиться.