вторник, 31 июля 2018 г.

Java Proxy in nutshell

Здравствуй, дружественная форма жизни!

Сегодня поговорим о Java Proxy. Java Proxy - это реализация шаблона проектирования Proxy (неожиданно!!!), которая входит в стандартную библиотеку Java.

Вообще, идея подмены с сохранением API взаимодействия очень популярна и плодотворна в IT. Тут вам и прокси-сервера, и разделение API и реализации и многое многое другое. Поэтому возможность создавать прокси-объекты - это великая сила в арсенале Java-разработчика. Используйте ее с умом и не забывайте, что с большой силой - приходит большая ответственность.

Проблематика
Прокси-объект - это объект, который замещает реальный объект в контексте его взаимодействия с остальной программой. В идеале, остальной код не должен знать и интересоваться с реальным объектом он взаимодействует или с прокси-объектом. UML-диаграмма так


Приведу примеры, когда такая подмена становится необходимой:
  1. Трассировка вызовов всех методов - вызовы всех методов пишутся в лог;
  2. Ленивая инициализация тяжелых объектов - вместо реального объекта отдаем прокси, а реальный инициализируем только, если вызван один из методов прокси-объекта;
  3. Объекта может и не быть - создание mock-объектов в модульном тестировании;
  4. Оригинальный объект находится на другом сервере - случай распределенной системы: локально работаем с прокси-объектом, который пересылает все вызовы оригинальному и возвращает ответ от него;
  5. и т.д. и т.п. и пр.
Реализация
Реализация Proxy в Java придерживается твердой точки зрения, что прокси-объект должен прятаться исключительно за API интерфейса. Что в принципе правильно, но в реальной жизни не всегда удобно. Итак, для создания прокси в Java есть класс java.lang.reflect.Proxy. 
Все прокси-объекты обладают следующими свойствами:
  1. Их классы являются подклассами класса java.lang.reflect.Proxy;
  2. Все вызовы прокси-объектов переадресуются обработчику вызовов - объекту, реализующему интерфейс java.lang.reflect.reflect.InvocationHandler, который вы должны написать сами;
  3. Прокси-объект можно строить на основе нескольких интерфейсов;
  4. Если все интерфейсы публичны, то proxy-класс публичен, не абстрактен и помечен словом final (от него нельзя наследоваться);
  5. Если не все интерфейсы публичны, то proxy-класс не публичны, не абстракны и помечены словом final;
  6. если прокси-объект x реализует интерфейс A, то x instanceof A будет возвращать true, а операция приведения типа A a = (A) x не будет кидать ошибку;
Итак, теория понятна, перейдем к практике. Для создания прокси-объекта в классе Proxy существует статический метод 
public static Object newProxyInstance(ClassLoader loader,
                      Class[] interfaces,
                      InvocationHandler h)
                               throws IllegalArgumentException
где

  1. loader -  загрузчик классов,  которым будет загружен класс proxy-объекта;
  2. interfaces - массив классов интерфейсов, методы которых мы хотим перехватывать;
  3. h - обработчик перехваченных вызовов;
Пример
Допустим, у нас есть интерфейс

public interface Math {
    int add(int a,  int b);
} 
И есть класс, который его реализует
public class MathImpl implements Math {
    public int add(int a,  int b) {
        return a + b;
    } 
} 
Допустим, мы хотим перехватить все вызовы метода add и записать это в консоль. Создадим обработчик перехваченных методов

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;

public class MathInvocationHandler implements InvocationHandler {
    private final Math math;

    public MathInvocationHandler(Math math) {
        this.math = math;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        traceInvocation(method, args);
        return method.invoke(math, args);
    }

    private void traceInvocation(Method method, Object[] args) {
        System.out.println("invoke: " + method.getName() + 
                (args != null ? "(" + Arrays.toString(args) + ")" : "()"));
    }
}
Теперь мы готовы запустить пример и в консоли вывода увидеть трассировку при вызове метода add:
//оригинальный объект, реально производящий вычисления
Math math = new MathImpl();

//Загрузчик классов текущего потока
ClassLoader loader = Thread.currentThread().getContextClassLoader();

//Массив интерфейсов
Class[] interfaces = new Class[]{Math.class};

//Обработчик вызовов
InvocationHandler handler = new MathInvocationHandler(math);

//Proxy-объект
Math proxy = (Math) Proxy.newProxyInstance(loader, interfaces, handler);

//Первый вызов
int result = proxy.add(1, 2);
System.out.println("result = " + result);

//Первый вызов
int result2 = proxy.add(33, 55);
System.out.println("result = " + result2);
В результате выполнения этого кода мы увидим:
invoke: add([1, 2])
result = 3
invoke: add([33, 55])
result = 88
Таким образом, наш прокси-объект прекрасно справился с поставленной задачей. Все вызовы метода add перехвачены и запротоколированы.
Пример 2 (когда что-то пошло не так)
Рассмотрим случай, когда при вызове метода оригинального объекта возникает ошибка. Как на это отреагирует прокси-объект? Проверим же это. Пусть есть класс
public class MathImpl2 implements Math {
    @Override
    public int add(int a, int b) {
        throw new RuntimeException("ОШИБКА!!!");
    }
}
Тогда выполнение следующего кода
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Example {
    public static void main(String[] args) {
        Math math = new MathImpl2();

        //Загрузчик классов текущего потока
        ClassLoader loader = Thread.currentThread().getContextClassLoader();

        //Массив интерфейсов
        Class[] interfaces = new Class[]{Math.class};

        //Обработчик вызовов
        InvocationHandler handler = new MathInvocationHandler(math);

        //Proxy-объект
        Math proxy = (Math) Proxy.newProxyInstance(loader, interfaces, handler);

        int result = proxy.add(1, 2);
        System.out.println("result = " + result);
    }
}
приведет к результату
invoke: add([1, 2])
Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
 at com.sun.proxy.$Proxy0.add(Unknown Source)
 at Example.main(Example.java:21)
Caused by: java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at MathInvocationHandler.invoke(MathInvocationHandler.java:15)
 ... 2 more
Caused by: java.lang.RuntimeException: ОШИБКА!!!
 at MathImpl2.add(MathImpl2.java:4)
 ... 7 more
То есть ошибка при вызове метода оригинального объекта приводит к ошибке при вызове метода прокси-объекта. Единственное неудобство в том, что оригинальная ошибка (выделено красным) скрыта внутри нескольких вспомогательных ошибок. Но это легко исправить:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Example {
    public static void main(String[] args) {
        Math math = new MathImpl2();

        //Загрузчик классов текущего потока
        ClassLoader loader = Thread.currentThread().getContextClassLoader();

        //Массив интерфейсов
        Class[] interfaces = new Class[]{Math.class};

        //Обработчик вызовов
        InvocationHandler handler = new MathInvocationHandler(math);

        //Proxy-объект
        Math proxy = (Math) Proxy.newProxyInstance(loader, interfaces, handler);

        try {
            int result = proxy.add(1, 2);
            System.out.println("result = " + result);
        } catch (Exception ex) {
            Throwable th = ex;
            while (th.getCause() != null) {
                th = th.getCause();
            }
            th.printStackTrace();
        }
    }
}
теперь причину ошибки легко определить:
invoke: add([1, 2])
java.lang.RuntimeException: ОШИБКА!!!
 at MathImpl2.add(MathImpl2.java:4)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at MathInvocationHandler.invoke(MathInvocationHandler.java:15)
 at com.sun.proxy.$Proxy0.add(Unknown Source)
 at Example.main(Example.java:22)

Пример 3 (возвращаемые типы)
При обработке методов внутри обработчика очень важно возвращать в точности те типы, которые возвращаются методами интерфейсов, иначе это вызовет непредвиденные ошибки. Пусть есть обработчик, который просто перенаправляет вызовы к оригинальному объекту:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MathInvocationHandler implements InvocationHandler {
    private final Math math;

    public MathInvocationHandler(Math math) {
        this.math = math;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(math, args);
    }
}
Использование такого обработчика приведет к тому же результату, что и использование оригинального объекта. Теперь допустим, что при каждом вызове метода add мы хотим удвоить результат. Поскольку мы переживаем, что разрядности типа int не хватит, чтобы корректно сохранить результат - мы используем тип long. Выглядит это так:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MathInvocationHandler implements InvocationHandler {
    private final Math math;

    public MathInvocationHandler(Math math) {
        this.math = math;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if("add".equals(method.getName())) {
            Integer value = (Integer) method.invoke(math, args);
            long result = 2 * value;
            return result;
        }
        return method.invoke(math, args);
    }
}
Использование такого обработчика приводит к ошибке
java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Integer
 at com.sun.proxy.$Proxy0.add(Unknown Source)
 at Example.main(Example.java:21)
Поскольку метод invoke должен вернуть Object, любые примитивные типы - например int, long, double и пр. - превратятся, благодаря явлению автоупаковки в свои классы-аналоги - Integer, Long, Double и пр. При этом, поскольку метод add возвращает int, механизм прокси ожидает, что от обработчика вернется Integer, но не Long, как показано в примере выше, и такое расхождение типов воспринимается как ошибка.
Ограничения
Конечно, за гибкость и простоту использования прокси-объектов приходится платить. Все вызовы методов прокси-объектов и само их создание занимает намного больше времени, чем аналогичные операции с обычным объектом. Поэтому, если для вас критично быстродействие конкретного участка кода - используйте Proxy с осмотрительностью. 
Итог
Прокси создан, надеюсь мне удалось донести до тебя, юный падаван, всю силу и выразительность этого механизма. Используй его с умом. И да пребудет с тобой Сила!

Комментариев нет:

Отправить комментарий