Динамічні проксі в Java

1. Вступ

У цій статті йдеться про динамічні проксі-сервери Java, що є одним із основних механізмів проксі-сервісу, доступних нам у цій мові.

Простіше кажучи, проксі - це фронти або обгортки, які передають виклик функцій через власні засоби (як правило, на реальні методи) - потенційно додаючи певну функціональність.

Динамічні проксі дозволяють одному одному класу з одним єдиним методом обслуговувати кілька викликів методів до довільних класів з довільною кількістю методів. Динамічний проксі можна розглядати як різновид фасаду , але такий, що може прикинутися реалізацією будь-якого інтерфейсу. Під обкладинкою він спрямовує всі виклики методів до одного обробника - методу invoke () .

Хоча це не інструмент, призначений для повсякденних завдань програмування, динамічні проксі можуть бути цілком корисними для фреймворків. Він також може використовуватися в тих випадках, коли конкретні реалізації класів не будуть відомі до часу виконання.

Ця функція вбудована в стандартний JDK, отже, додаткові залежності не потрібні.

2. Обробник викликів

Давайте побудуємо простий проксі-сервер, який насправді нічого не робить, крім друку того методу, який було запропоновано викликати, і повернемо жорстко закодований номер.

Спочатку нам потрібно створити підтип java.lang.reflect.InvocationHandler :

public class DynamicInvocationHandler implements InvocationHandler { private static Logger LOGGER = LoggerFactory.getLogger( DynamicInvocationHandler.class); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { LOGGER.info("Invoked method: {}", method.getName()); return 42; } }

Тут ми визначили простий проксі, який реєструє, який метод був викликаний, і повертає 42.

3. Створення екземпляра проксі

Екземпляр проксі, що обслуговується щойно визначеним нами обробником виклику, створюється за допомогою виклику фабричного методу в класі java.lang.reflect.Proxy :

Map proxyInstance = (Map) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, new DynamicInvocationHandler());

Отримавши екземпляр проксі, ми можемо викликати його методи інтерфейсу як зазвичай:

proxyInstance.put("hello", "world");

Як і слід було повідомляти про метод put (), який викликається, роздруковується у файлі журналу.

4. Обробник викликів через лямбда-вирази

Оскільки InvocationHandler є функціональним інтерфейсом, можна визначити обробник вбудованим, використовуючи лямбда-вираз:

Map proxyInstance = (Map) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, (proxy, method, methodArgs) -> { if (method.getName().equals("get")) { return 42; } else { throw new UnsupportedOperationException( "Unsupported method: " + method.getName()); } });

Тут ми визначили обробник, який повертає 42 для всіх операцій get і кидає UnsupportedOperationException для всього іншого.

Він викликається точно так само:

(int) proxyInstance.get("hello"); // 42 proxyInstance.put("hello", "world"); // exception

5. Приклад динамічного проксі-сервера часу

Давайте розглянемо один потенційний реальний сценарій для динамічних проксі.

Припустимо, ми хочемо записати, скільки часу виконуються наші функції. Для цього ми спочатку визначаємо обробник, здатний обертати “реальний” об’єкт, відстежувати інформацію про час та відображати виклик:

public class TimingDynamicInvocationHandler implements InvocationHandler { private static Logger LOGGER = LoggerFactory.getLogger( TimingDynamicInvocationHandler.class); private final Map methods = new HashMap(); private Object target; public TimingDynamicInvocationHandler(Object target) { this.target = target; for(Method method: target.getClass().getDeclaredMethods()) { this.methods.put(method.getName(), method); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.nanoTime(); Object result = methods.get(method.getName()).invoke(target, args); long elapsed = System.nanoTime() - start; LOGGER.info("Executing {} finished in {} ns", method.getName(), elapsed); return result; } }

Згодом цей проксі-сервер можна використовувати для різних типів об’єктів:

Map mapProxyInstance = (Map) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, new TimingDynamicInvocationHandler(new HashMap())); mapProxyInstance.put("hello", "world"); CharSequence csProxyInstance = (CharSequence) Proxy.newProxyInstance( DynamicProxyTest.class.getClassLoader(), new Class[] { CharSequence.class }, new TimingDynamicInvocationHandler("Hello World")); csProxyInstance.length()

Тут ми проксірували карту та послідовність символів (Рядок).

Виклики методів проксі будуть делегувати обгорнутий об'єкт, а також створювати оператори ведення журналу:

Executing put finished in 19153 ns Executing get finished in 8891 ns Executing charAt finished in 11152 ns Executing length finished in 10087 ns

6. Висновок

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

Як завжди, код у прикладах можна знайти на GitHub.