Теория и практика Java : Декорирование при помощи динамического прокси

Динамические прокси - удобный инструмент для построения декораторов и адаптеров

Средство динамического прокси, являющееся частью пакета java.lang.reflect и добавленное в JDK версии 1.3, позволяет программам создавать прокси-объекты, которые могут выполнять один или несколько известных интерфейсов и посылать запросы методам интерфейса программным путём, используя отражение вместо отправки встроенного виртуального метода. Данный процесс позволяет осуществить "перехват" запросов метода и перенаправить их или динамически добавить функциональность. В этом месяце Брайан Гетц расскажет вам о нескольких приложениях для динамических прокси.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



08.02.2007

Динамические прокси обеспечивают дополнительный динамический механизм для реализации многочисленных конструктивных шаблонов, среди которых Facade (Фасад), Bridge (Мост), Interceptor (Перехватчик), Decorator (Декоратор), Proxy (Заместитель), в том числе удаленный и виртуальный прокси, а также шаблоны Adapter (Адаптер). В то время как все эти шаблоны могут быть легко реализованы при помощи обычных классов вместо динамических прокси, во многих случаях технология использования динамического прокси является более удобной и компактной, а также может устранить необходимость создания многочисленных классов вручную и их генерирования.

Модель прокси

Модель прокси предполагает создании "объекта-заглушки" (stub) или "объекта-заместителя" (surrogate), цель которого принимать запросы и пересылать их другому объекту, который фактически выполняет работу. Модель прокси используется удалённым вызовом метода (технология RMI), чтобы заставить объект, выполняющийся в другой JVM, выглядеть как локальный объект; с помощью Enterprise JavaBeans (EJB) добавить удалённый вызов, безопасность и установление границ транзакций; с помощью JAX-RPC Web-сервисов представить удалённые сервисы в виде локальных объектов. В каждом случае поведение потенциального удалённого объекта определяется интерфейсом, который по своему характеру допускает множественные реализации. Как правило, вызывающий оператор не может определить, что они содержат ссылку на заглушку, а не на настоящий объект, потому что оба они реализуют один и тот же интерфейс; заглушка следит за работой по поиску настоящих объектов, маршализацией параметров, их отправкой настоящему объекту, демаршализацией выводимого значения и выводом его для вызывающей программы. Прокси могут использоваться для обеспечения дистанционной связи (как в RMI, EJB и JAX-RPC), упаковывать объекты с помощью политик безопасности (EJB), обеспечивать отложенную загрузку дорогостоящих объектов (EJB Entity Beans) или добавлять инструментальное оснащение, например, регистрацию (logging).

В версиях JDK ниже 5.0, заглушки RMI (и их антиподы, "скелеты" - skeletons) являлись классами, сгенерированными во время компиляции RMI-компилятором (rmic), который является частью инструментария JDK. Для каждого удалённого интерфейса генерируется класс заглушки (прокси), который олицетворяет удалённый объект. Также генерируется объект "скелет", выполняющий работу, противоположную работе заглушки, в удалённом JVM - он демаршализует параметры и вызывает реальный объект. Аналогично инструментальные средства JAX-RPC для Web-сервисов генерируют классы прокси для удалённых Web-сервисов, которые представляют их локальными объектами.

Созданы ли сгенерированные классы заглушки как исходный код или байт-код, генерация кода добавляет дополнительные шаги к процессу компиляции и может привести к путанице вследствие быстрого распространения одинаково названных классов. С другой стороны, механизм динамического прокси позволяет создавать прокси-объект во время выполнения, не генерируя классы заглушки во время компиляции. В версии JDK 5.0 и выше средство RMI использует динамические прокси вместо сгенерированных заглушек, в результате чего RMI стал проще в использовании. Многие контейнеры J2EE также используют динамические прокси для осуществления EJB. Технология EJB во многом полагается на использование перехвата для реализации безопасности и установления границ транзакций; динамические прокси упрощают реализацию перехвата, обеспечивая центральный путь потока управляющих команд для всех методов, вызванных на интерфейс.


Механизм динамического прокси

В основе динамического прокси лежит интерфейс InvocationHandler, показанный в Листинге 1. Задача обработчика обращения - фактически исполнить запрошенный метод от имени динамического прокси. Обработчик обращения передаётся объекту Method (из пакета java.lang.reflect) и списку параметров, которые должны быть переданы методу; в простейшем случае он просто может вызвать рефлективный метод Method.invoke() и вернуть результат.

Листинг 1. Интерфейс InvocationHandler (Обработчик Вызовов)
public interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

У каждого прокси имеется связанный с ним обработчик обращения, который вызывается каждый раз, когда вызван один из методов прокси. В соответствии с главным принципом проекта, который гласит, что интерфейсы служат для определения типов, а классы для определения реализаций, прокси-объекты могут реализовывать один или несколько интерфейсов, но не классов. Так как классы прокси не имеют доступных имён, у них не может быть конструкторов, поэтому они должны изготавливаться с помощью factories. Листинг 2 отображает простейшую возможную реализацию динамического прокси, которая реализует интерфейс Set и отправляет все методы Set (а также все методы Object ) закапсулированному экземпляру Set. .

Листинг 2. Простой динамический прокси, упаковывающий Set)
public class SetProxyFactory {

    public static Set getSetProxy(final Set s) {
        return (Set) Proxy.newProxyInstance
          (s.getClass().getClassLoader(),
                new Class[] { Set.class },
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, 
                      Object[] args) throws Throwable {
                        return method.invoke(s, args);
                    }
                });
    }
}

Класс SetProxyFactory содержит один статический фабричный метод, getSetProxy(), который возвращает динамический прокси, реализующий Set. Прокси-объект действительно реализует Set - вызывающий оператор не может определить (за исключением отражения), является ли возвращённый объект динамическим прокси. Прокси, возвращённый SetProxyFactory не делает ничего кроме отправки метода экземпляру Set, переданному в фабричный метод. В то время как код отражения зачастую трудно прочитать, несложно следовать за потоком управления, здесь происходит так мало процессов, что нетрудно следить за управляющей логикой программы - всякий раз, когда метод вызывается на прокси Set, он отправляется обработчику вызова, который просто рефлективно вызывает желаемый метод на связанный с ним упакованный объект. Конечно же, ничего не делающий прокси не имеет смысла - разве нет?

Пустые (Do-nothing) адаптеры

Фактически есть хорошее применение для пустого упаковщика, например SetProxyFactory - её можно использовать, чтобы безопасно ограничить ссылку на объект определённым интерфейсом (или набором интерфейсов) таким образом, что вызывающий оператор не сможет случайно обратиться к ссылке, что делает безопаснее передачу ссылок на объект ненадёжному коду типа плагинов или повторных вызовов. Листинг 3 содержит набор определений класса, которые реализуют типичный сценарий повторного вызова; вы увидите, как динамические прокси могут более удобно заменять шаблон адаптера, который, как правило, реализуется вручную (или мастерами генерации кода, предоставляемыми IDE).

Листинг 3. Типичный сценарий повторного вызова
public interface ServiceCallback {
    public void doCallback();
}

public interface Service {
    public void serviceMethod(ServiceCallback callback);
}

public class ServiceConsumer implements ServiceCallback {
    private Service service;

    ...
    public void someMethod() {
        ...
        service.serviceMethod(this);
    }
}

Класс ServiceConsumer реализует ServiceCallback (что обычно является удобным способом поддержки повторных вызовов) и передаёт this ссылку на serviceMethod() как ссылку повторного вызова. Проблема данного подхода в том, что невозможно помешать реализации Service привести ServiceCallback к ServiceConsumer и вызвать методы, которые ServiceConsumer не хочет вызвать для Service. Иногда вы и не задумываетесь об этой опасности, а иногда задумываетесь. Если задумываетесь, то вы можете сделать вызываемый объект внутренним классом или записать пустой класс адаптера (см. ServiceCallbackAdapter в листинге 4) и упаковать ServiceConsumer при помощи ServiceCallbackAdapter. ServiceCallbackAdapter не даёт Service привести ServiceCallback к ServiceConsumer.

Листинг 4. Класс адаптера, который безопасно ограничивает объект интерфейсом, чтобы он не мог быть запущен враждебным программным кодом
public class ServiceCallbackAdapter implements ServiceCallback {
    private final ServiceCallback cb;

    public ServiceCallbackAdapter(ServiceCallback cb) {
        this.cb = cb;
    }

    public void doCallback() {
        cb.doCallback();
    }
}

Написание классов адаптера, таких как ServiceCallbackAdapter является несложной, но утомительной процедурой. Приходится писать метод отправки для каждого метода в упакованном интерфейсе. В случае ServiceCallback реализовать нужно было только один метод, но некоторые интерфейсы, например Collections или интерфейсы JDBC, содержат множества методов. Современные IDE уменьшают объём работы, связанной с записью класса адаптера, при помощи мастера "Delegate Methods", но вы всё еще должны написать один класс адаптера для каждого интерфейса, который вы хотите упаковать. Есть проблема с классами, содержащими только сгенерированный код. Кажется, есть возможность выразить "do-nothing narrowing adapter pattern" (шаблон пустого адаптера сужения) более сжато.

Универсальный (generic) класс адаптера

Класс SetProxyFactory в Листинге 2 конечно же, более компактен по сравнению с эквивалентным классом адаптера для интерфейса Set, но он работает только на один интерфейс: Set. Но используя универсальные адаптеры, вы с лёгкостью можете создать универсальную прокси-factory, которое может делать то же самое для любого интерфейса, как показано в Листинге 5. Он почти идентичен SetProxyFactory, но может работать для любого интерфейса. Теперь вам никогда не придётся писать заново класс адаптера сужения! Если вы хотите создать прокси-объект, который будет безопасно сужать объект к интерфейсу T, просто вызовите getProxy(T.class,object), и вы его получите без дополнительной груды классов адаптера.

Листинг 5. Класс factory универсального адаптера сужения
public class GenericProxyFactory {

    public static<T> T getProxy(Class<T> intf, 
      final T obj) {
        return (T) 
          Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                new Class[] { intf },
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, 
                      Object[] args) throws Throwable {
                        return method.invoke(obj, args);
                    }
                });
    }
}

Динамические прокси как декораторы

Конечно, средства динамического прокси могут не только сужать тип объекта к конкретному интерфейсу. Это всего лишь короткий прыжок от простого адаптера сужения в Листинге 2 и Листинге 5 к образцу декоратора, где прокси добавляет к вызовам дополнительную функциональность, типа проверок безопасности или регистрации данных. Листинг 6 показывает регистратор InvocationHandler, который пишет сообщение в журнал, показывающее вызванный метод, удовлетворяющие требованиям параметры и возвращаемое значение в дополнение к простому вызову метода на желаемый целевой объект. За исключением рефлективного вызова invoke(), весь код здесь - это просто часть генерации отладочного сообщения и очень-очень малая часть. Код для фабричного метода прокси практически идентичен GenericProxyFactory, за тем исключением, что тот использует LoggingInvocationHandler вместо анонимного обработчика вызовов.

Листинг 6. Декоратор на основе прокси, который генерирует системный журнал отладки для каждого вызова метода
    private static class LoggingInvocationHandler<T> 
      implements InvocationHandler {
        final T underlying;

        public LoggingHandler(T underlying) {
            this.underlying = underlying;
        }

        public Object invoke(Object proxy, Method method, 
          Object[] args) throws Throwable {
            StringBuffer sb = new StringBuffer();
            sb.append(method.getName()); sb.append("(");
            for (int i=0; args != null && i<args.length; i++) {
                if (i != 0)
                    sb.append(", ");
                sb.append(args[i]);
            }
            sb.append(")");
            Object ret = method.invoke(underlying, args);
            if (ret != null) {
                sb.append(" -> "); sb.append(ret);
            }
            System.out.println(sb);
            return ret;
        }
    }

Если вы упаковываете HashSet при помощи прокси протоколирования и выполняете следующую простую тестовую программу:

   Set s = newLoggingProxy(Set.class, new HashSet());
    s.add("three");
    if (!s.contains("four"))
        s.add("four");
    System.out.println(s);

вы получаете следующие данные на выходе:

add(three) -> true
  contains(four) -> false
  add(four) -> true
  toString() -> [four, three]
  [four, three]

Данный подход - хороший, простой способ добавить оболочку отладки вокруг объекта. Он, конечно же, намного проще (и более универсален), чем производство класса делегирования и создание многочисленных операторов println() вручную. Я мог бы и еще расширить применение этого метода; вместо того, чтобы безо всяких условий генерировать данные отладки на выходе, прокси мог бы консультироваться с динамическим хранилищем конфигурации (инициализированным из файла конфигурации, а его можно было бы динамически изменять при помощи JMX MBean), чтобы определить, действительно ли генерировать отладочные операторы, возможно, даже на основе метода класс-за-классом или экземпляр-за-экземпляром.

На данном этапе я ожидаю, что поклонники AOP практически взорвутся фразой типа "Но как раз для этого и хорош AOP!" Это так, но решить любую данную проблему можно не одним способом - и только то, что метод может решить данную проблему, вовсе не значит, что это решение самое лучшее. В любом случае, подход динамического прокси имеет то преимущество, что работает полностью в пределах "Pure Java", и не в каждом вычислительном центре используют AOP (или им там не следует его использовать) .

Динамические прокси как адаптеры

Прокси также могут использоваться как истинные адаптеры, обеспечивая представление объекта, который экспортирует интерфейс, отличный от того, который реализуется нижележащим объектом. Обработчику вызова не нужно отправлять каждый вызов метода одному и тому же нижележащему объекту; он может исследовать имя и отправлять разные методы различным объектам. Как пример, предположим, что у вас есть набор интерфейсов JavaBeans для представления постоянных объектов (Person, Company и PurchaseOrder), которые определяют геттеры и сеттеры для свойств, и вы создаёте уровень долговременного хранения, который преобразует записи базы данных в объекты, реализующие данные интерфейсы. Вместо того, чтобы писать или генерировать класс для каждого интерфейса, вы могли бы иметь один универсальный класс прокси в стиле JavaBeans, который сохранял бы свойства в карте (Map).

Листинг 7 отображает динамический прокси, который определяет имя вызванного метода и реализует методы getter и setter напрямую, консультируясь или изменяя карту свойств. Данный класс прокси теперь может реализовать объекты нескольких интерфейсов в стиле JavaBeans.

Листинг 7. Класс динамического прокси, который отправляет getter'ы и setter'ы карте
public class JavaBeanProxyFactory {
    private static class JavaBeanProxy implements InvocationHandler {
        Map<String, Object> properties = new HashMap<String, 
          Object>();

        public JavaBeanProxy(Map<String, Object> properties) {
            this.properties.putAll(properties);
        }

        public Object invoke(Object proxy, Method method, 
          Object[] args) 
          throws Throwable {
            String meth = method.getName();
            if (meth.startsWith("get")) {
                String prop = meth.substring(3);
                Object o = properties.get(prop);
                if (o != null && !method.getReturnType().isInstance(o))
                    throw new 
                      ClassCastException(o.getClass().getName() + 
                      " is not a " + method.getReturnType().getName());
                return o;
            }
            else if (meth.startsWith("set")) {
                // Dispatch setters similarly
            }
            else if (meth.startsWith("is")) {
                // Alternate version of get for boolean properties
            }
            else {
                // Can dispatch non get/set/is methods as desired
            }
        }
    }

    public static<T> T getProxy(Class<T> intf,
      Map<String, Object> values)         return (T) Proxy.newProxyInstance
          (JavaBeanProxyFactory.class.getClassLoader(),
                new Class[] { intf }, new JavaBeanProxy(values));
    }
}

В то время, как есть потенциальная возможность потери безопасности типа из-за того, что отражение работает в терминах Object, getter, оперирующий в JavaBeanProxyFactory "испытывает на принудительный отказ" часть дополнительных необходимых проверок соответствия типов, как проверка isInstance() для getter в моём примере.


Затраты на выполнение

Как вы увидели, динамические прокси обладают возможностью упрощать большой объём кода - они не только могут заменять многие сгенерированные коды, один класс прокси может заменить множественные классы написанного вручную или сгенерированного кодов. Что же приходится за это платить? Есть, вероятно, некоторые затраты на выполнение из-за рефлективной отправки методов вместо использования встроенного метода виртуальной отправки. В самых ранних JDK производительность отражения была слабой (как и все остальное в ранних JDK), но за последнее десятилетие отражение стало гораздо быстрее.

Не вдаваясь в предмет создания тестирующих программ, я написал простую и "ненаучную" испытательную программу, которая создаёт цикл, вводя данные в Set, беспорядочно вставляя, отыскивая и удаляя элементы из Set. Я выполнил её в виде трёх реализаций Set: неукрашенный HashSet, написанный вручную адаптер Set, который просто переправляет все методы к нижележащему HashSet и основанный на прокси адаптер Set, который также просто переправляет все методы к нижележащему HashSet. Каждая итерация цикла генерировала несколько случайных чисел и выполняла одну или несколько операций Set. Вводимый вручную адаптер генерировал только несколько процентов издержек по сравнению с простым HashSet (по-видимому из-за эффективного встроенного кэширования на уровне JVM и предсказания ветвлений на аппаратном уровне); адаптер прокси был заметно медленнее, чем простой HashSet, но издержки были меньше, чем в два раза.

Моё заключение из эксперимента таково: для огромного большинства случаев технология динамического прокси довольно-таки хорошо работает для лёгких методов, и поскольку операции, где используются прокси становятся более тяжеловесными (например, удалённые вызовы методов или методы, которые используют сериализацию, выполняют ввод-вывод или извлекают данные из базы данных), непроизводительные издержки прокси будут приближаться к нулю. Несмотря на то, что, конечно же, будут случаи, в которых технология динамического прокси будет иметь недопустимые издержки при выполнении, они, вероятно, составят меньшую часть ситуаций.


Заключение

Динамические прокси - мощный и недостаточно используемый инструмент в реализации многих конструктивных шаблонов, в том числе Proxy, Decorator и Adapter. Основанные на прокси реализации данных шаблонов просто записать, в них труднее ошибиться, и они более универсальны; во многих случаях один класс динамического прокси может служить декоратором или прокси для всех интерфейсов, вместо того, чтобы записывать статический класс для каждого интерфейса. Для всех приложений, кроме наиболее критических с точки зрения производительности, технология динамического прокси, возможно, будет предпочтительней по сравнению с написанием кода вручную или технологией сгенерированных заглушек.

Ресурсы

Научиться

Получить продукты и технологии

  • JProxy Javadoc: Подробности и объяснение для класса JProxy.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=194637
ArticleTitle=Теория и практика Java : Декорирование при помощи динамического прокси
publish-date=02082007