Болезни Военный билет Призыв

Пример: использование прокси-классов для обобщенного DAO

Теория и практика Java

Декорирование при помощи динамического прокси

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

Серия контента:

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

Листинг 5. Класс factory универсального адаптера сужения
public class GenericProxyFactory { public static T getProxy(Class 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); } }); } }

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

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

Листинг 6. Декоратор на основе прокси, который генерирует системный журнал отладки для каждого вызова метода
private static class LoggingInvocationHandler 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 "); 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() ->

Данный подход - хороший, простой способ добавить оболочку отладки вокруг объекта. Он, конечно же, намного проще (и более универсален), чем производство класса делегирования и создание многочисленных операторов 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 properties = new HashMap(); public JavaBeanProxy(Map 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 getProxy(Class intf, Map 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. Основанные на прокси реализации данных шаблонов просто записать, в них труднее ошибиться, и они более универсальны; во многих случаях один класс динамического прокси может служить декоратором или прокси для всех интерфейсов, вместо того, чтобы записывать статический класс для каждого интерфейса. Для всех приложений, кроме наиболее критических с точки зрения производительности, технология динамического прокси, возможно, будет предпочтительней по сравнению с написанием кода вручную или технологией сгенерированных заглушек.

Всегда, когда мне нужно подглядеть за двумя программами общающимися по TCP/IP протоколу я использую вот эту проксю.

/* * Proxy Server * Copyright (C) 2003, 2004 Healy Computer Systems * www.intermediary.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.intermediary.socketproxy.handler; import com.intermediary.socketproxy.IDataHandler; /** * Sample/utility class that implements IDataHandler. * Passes data straight through without modification. */ public class DataPassThrough implements IDataHandler { /** * Constructs DataPassThrough. */ public DataPassThrough() { //do nothing on construction } /** * Pushes incoming byte array to custom handler class. * The incoming bytes are then available to handler * logic for inspection and/or modification. * * @param bytes the inbound byte array * @return the original (or modified) array of bytes */ public byte pushBytes(byte bytes) { //no action taken //simply return the byte array return bytes; } }
Его метод pushBytes вызывается всякий раз, когда мимо прокси что-то происходит. Причем все проходит именно через метод pushBytes - ты должен вернуть массив байтов, которые передаются дальше. А можно немного пошалить и изменить что-то.

Конфигурируется тулза очень просто. В файле \cfg\properties.cfg все описано.
# port for the proxy server to listen on # этот порт будет слушать прокси, т.е. все, что прийдет на этот порт данного компьютера - попадает на обработку прокси серверу. listen_port=7777 # forwarding destination # на этот хост будет отправляться то, что ответит proxy (то, что мы return"ули в pushBytes) destination_host=localhost # destination port # а это хост места назначения destination_port=8787 # java class to process data entering the proxy server # must be available in your classpath when the proxy server is started # этот обрабочик будет обрабатывать входящие в прокси собщения in_class=com.intermediary.socketproxy.handler.DataPassThrough # java class to process data leaving the proxy server # must be available in your classpath when the proxy server is started # этот обрабочик будет обрабатывать исходящие из прокси собщения out_class=com.intermediary.socketproxy.handler.DataPassThrough # logging section # log - general configuration # максимальный размер файла лога max_log_file_bytes=10000000 # максимально количество лог файлов. Если файлов в папке \logs больше столько сколько у казано тут, то при повторном запуске прокси самый старый лог удалится. log_file_count=5 # log - system information # то где лог файлы хранятся и как именуются system_log_file=../log/log%g_system.txt # то, какого уровня лог сообщения будут записываться в файл # допустимо: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL system_log_level=ALL # то, какого уровня лог сообщения будут выводится на экран # допустимо: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL system_console_level=INFO # log - inbound data # входящие данные логируются тут inbound_log_file=../log/log%g_inbound.txt inbound_log_level=ALL # log - outbound data # исходящие данные логируются тут outbound_log_file=../log/log%g_outbound.txt outbound_log_level=ALL
Запускается вызовом main метода класса
com.intermediary.socketproxy.ProxyServer
В качестве аргументов можно передать путь к config файлу. Если не передавать, то файл будут искать в корне.

  • Constructor Summary

    Constructors
    Modifier Constructor and Description
    protected Proxy (InvocationHandler h)
  • Method Summary

    Methods
    Modifier and Type Method and Description
    static InvocationHandler getInvocationHandler (Object proxy)
    static Class getProxyClass (ClassLoader loader, Class ... interfaces)

    Returns the java.lang.Class object for a proxy class given a class loader and an array of interfaces.

    static boolean isProxyClass (Class cl)

    Returns true if and only if the specified class was dynamically generated to be a proxy class using the getProxyClass method or the newProxyInstance method.

    static Object newProxyInstance (ClassLoader loader, Class interfaces, InvocationHandler h)

    Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler.

    • Methods inherited from class java.lang.Object

      clone , equals , finalize , getClass , hashCode , notify , notifyAll , toString , wait , wait , wait
    • Field Detail

      • h

        protected InvocationHandler h

        the invocation handler for this proxy instance.

    • Constructor Detail

      • Proxy

        protected Proxy(InvocationHandler h)

        Constructs a new Proxy instance from a subclass (typically, a dynamic proxy class) with the specified value for its invocation handler.

        Parameters: h - the invocation handler for this proxy instance
    • Method Detail

      • getProxyClass

        public static Class getProxyClass(ClassLoader loader, Class ... interfaces) throws IllegalArgumentException

        Returns the java.lang.Class object for a proxy class given a class loader and an array of interfaces. The proxy class will be defined by the specified class loader and will implement all of the supplied interfaces. If a proxy class for the same permutation of interfaces has already been defined by the class loader, then the existing proxy class will be returned; otherwise, a proxy class for those interfaces will be generated dynamically and defined by the class loader.

        There are several restrictions on the parameters that may be passed to Proxy.getProxyClass:

        • All of the Class objects in the interfaces array must represent interfaces, not classes or primitive types.
        • No two elements in the interfaces array may refer to identical Class objects.
        • All of the interface types must be visible by name through the specified class loader. In other words, for class loader cl and every interface i , the following expression must be true: Class.forName(i.getName(), false, cl) == i
        • All non-public interfaces must be in the same package; otherwise, it would not be possible for the proxy class to implement all of the interfaces, regardless of what package it is defined in.
        • For any set of member methods of the specified interfaces that have the same signature:
          • If the return type of any of the methods is a primitive type or void, then all of the methods must have that same return type.
          • Otherwise, one of the methods must have a return type that is assignable to all of the return types of the rest of the methods.
        • The resulting proxy class must not exceed any limits imposed on classes by the virtual machine. For example, the VM may limit the number of interfaces that a class may implement to 65535; in that case, the size of the interfaces array must not exceed 65535.

        If any of these restrictions are violated, Proxy.getProxyClass will throw an IllegalArgumentException . If the interfaces array argument or any of its elements are null , a NullPointerException will be thrown.IllegalArgumentException

        Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler. This method is equivalent to: Proxy.getProxyClass(loader, interfaces). getConstructor(new Class { InvocationHandler.class }). newInstance(new Object { handler });

        Proxy.newProxyInstance throws IllegalArgumentException for the same reasons that Proxy.getProxyClass does.

        Parameters: loader - the class loader to define the proxy class interfaces - the list of interfaces for the proxy class to implement h - the invocation handler to dispatch method invocations to Returns: a proxy instance with the specified invocation handler of a proxy class that is defined by the specified class loader and that implements the specified interfaces Throws: IllegalArgumentException - if any of the restrictions on the parameters that may be passed to getProxyClass are violated NullPointerException - if the interfaces array argument or any of its elements are null , or if the invocation handler, h , is null
      • isProxyClass

        public static boolean isProxyClass(Class cl)

        Returns true if and only if the specified class was dynamically generated to be a proxy class using the getProxyClass method or the newProxyInstance method.

        The reliability of this method is important for the ability to use it to make security decisions, so its implementation should not just test if the class in question extends Proxy .

        Parameters: cl - the class to test Returns: true if the class is a proxy class and false otherwise Throws: NullPointerException - if cl is null
      • getInvocationHandler

        public static InvocationHandler getInvocationHandler(Object proxy) throws IllegalArgumentException

        Returns the invocation handler for the specified proxy instance.

        Parameters: proxy - the proxy instance to return the invocation handler for Returns: the invocation handler for the proxy instance Throws: IllegalArgumentException - if the argument is not a proxy instance

Динамические прокси-классы

Сегодня мы поговорим о такой интересной особенности JVM , как динамические прокси-классы. Предположим, что у нас есть класс A , реализующий некоторые интерфейсы. Java-машина во время исполнения может сгенерировать прокси-класс для данного класса A , т.е. такой класс, который реализует все интерфейсы класса A , но заменяет вызов всех методов этих интерфейсов на вызов метода InvocationHandler#invoke , где InvocationHandler - интерфейс JVM , для которого можно определять свои реализации.


Создается прокси-класс с помощью вызова метода Proxy.getProxyClass , который принимает класс-лоадер и массив интерфейсов (interfaces ), а возвращает объект класса java.lang.Class , который загружен с помощью переданного класс-лоадера и реализует переданный массив интерфейсов.

На передаваемые параметры есть ряд ограничений:

1. Все объекты в массиве interfaces должны быть интерфейсами. Они не могут быть классами или примитивами.

2. В массиве interfaces не может быть двух одинаковых объектов.

3. Все интерфейсы в массиве interfaces должны быть загружены тем класс-лоадером, который передается в метод getProxyClass .

4. Все не публичные интерфейсы должны быть определены в одном и том же пакете, иначе генерируемый прокси-класс не сможет их все реализовать.

5. Ни в каких двух интерфейсах не может быть метода с одинаковым названием и сигнатурой параметров, но с разными типами возвращаемого значения.

6. Длина массива interfaces ограничена 65535-ю интерфейсами. Никакой Java -класс не может реализовывать более 65535 интерфейсов (а так хотелось!).

Если какое-либо из вышеперечисленных ограничений нарушено - будет выброшено исключение IllegalArgumentException , а если массив интерфейсов interfaces равен null , то будет выброшено NullPointerException .

Свойства динамического прокси-класса

Необходимо сказать пару слов о свойствах класса, создаваемого с помощью Proxy.getProxyClass . Данные свойства следующие:

1. Прокси-класс является публичным, снабжен модификатором final и не является абстрактным.

2. Имя прокси-класса по-умолчанию не определено, однако начинается на $Proxy . Все пространство имен, начинающихся на $Proxy зарезервировано для прокси-классов.

3. Прокси-класс наследуется от java.lang.reflect.Proxy .

4. Прокси-класс реализует все интерфейсы, переданные при создании, в порядке передачи.

5. Если прокси-класс реализует непубличный интерфейс, то он будет сгенерирован в том пакете, в котором определен этот самый непубличный интерфейс. В общем случае пакет, в котором будет сгенерирован прокси-класс неопределен.

6. Метод Proxy.isProxyClass возвращает true для классов, созданных с помощью Proxy.getProxyClass и для классов объектов, созданных с помощью Proxy.newProxyInstance и false в противном случае. Данный метод используется подсистемой безопасности Java и нужно понимать, что для класса, просто унаследованного от java.lang.reflect.Proxy он вернет false .

7. java.security.ProtectionDomain для прокси-класса такой же, как и для системных классов, загруженных bootstrap -загрузчиком, например - для java.lang.Object . Это логично, потому что код прокси-класса создается самой JVM и у нее нет причин себе не доверять.

Экземпляр динамического прокси-класса и его свойства

Конструктор прокси-класса принимает один аргумент - реализацию интерфейса InvocationHandler . Соответственно, объект прокси-класса можно создать с помощью рефлексии, вызвав метод newInstance объекта класса Class . Однако, существует и другой способ - вызвать метод Proxy.newProxyInstance , который принимает на вход загрузчик классов, массив интерфейсов, которые будет реализовывать прокси-класс, и объект, реализующий InvocationHandler . Фактически, данный метод комбинирует получение прокси-класса с помощью Proxy.getProxyClass и создание экземпляра данного класса через рефлексию.

Свойства созданного экземпляра прокси-класса следующие:

1. Объект прокси-класса приводим ко всем интерфейсам, переданным в массиве interfaces . Если IDemo - один из переданных интерфейсов, то операция proxy instanceof IDemo всегда вернет true , а операция (IDemo) proxy завершится корректно.

2. Статический метод Proxy.getInvocationHandler возвращает обработчик вызовов, переданный при создании экземпляра прокси-класса. Если переданный в данный метод объект не является экземпляром прокси-класса, то будет выброшено IllegalArgumentException исключение.

3. Класс-обработчик вызовов реализует интерфейс InvocationHandler , в котором определен метод invoke , имеющий следующую сигнатуру:

Здесь proxy - экземпляр прокси-класса, который может использоваться при обработке вызова того или иного метода. Второй параметр - method является экземпляром класса java.lang.reflect.Method . Значение данного параметра - один из методов, определенных в каком-либо из переданных при создании прокси-класса интерфейсов или их супер-интерфейсов. Третий параметр - массив значений аргументов метода. Аргументы примитивных типов будут заменены экземплярами своих классов-оберток, таких как java.lang.Boolean или java.lang.Integer . Конкретная реализация метода invoke может изменять данный массив.

Значение, возвращаемое методом invoke должно иметь тип, совместимый с типом значения, возвращаемого интерфейсным методом, для которого вызывается данная обертка. В частности, если интерфейсный метод возвращает значение примитивного типа - необходимо возвратить экземпляр класса-обертки данного примитивного типа. Если возвращается null , а ожидается значение примитивного типа, - будет выброшено NullPointerException . В случае непримитивных типов, класс возвращаемого значения метода invoke должен быть приводим к классу возвращаемого значения интерфейсного метода, иначе будет выброшено ClassCastException .

Внутри метода invoke должны бросаться только те проверяемые исключения, которые определены в сигнатуре вызываемого интерфейсного метода либо приводимые к ним. Помимо этих типов исключений разрешается бросать только непроверяемые исключения (такие как java.lang.RuntimeException ) или ошибки (например, java.lang.Error ). Если внутри метода invoke выброшено проверяемое исключение несопоставимое с описанными в сигнатуре интерфейсного метода - то будет так же выброшено исключение UndeclaredThrowableException.

Методы hashCode , equals и toString , определенные в классе Object , так же будут вызываться не на прямую, а через метод invoke наравне со всеми интерфейсными методами. Другие публичные методы класса Object будут вызываться напрямую.

Пример: использование прокси-классов для обобщенного DAO

Давайте рассмотрим обещанный пример использования динамических прокси-классов. Идея взята из статьи Не повторяйте DAO! , только мы попробуем реализовать ее без использования Spring . Суть в следующем: у нас есть Hibernate , в котором есть такое понятие, как именованные запросы. Мы имеем много DAO для разных типов сущностей, в которых есть методы поиска объектов по каким-либо критериям, подсчет количества объектов и т.д. Причем, каждый метод, фактически, просто вызывает тот или иной именованный запрос и возвращает его результат. Непонятно, зачем плодить методы с одной и той же логикой. Можно просто определять методы в соответствующих интерфейсах, а вызывать их через прокси к данным интерфейсам. В прокси же вызов метода подменяется на вызов соответствующего именованного запроса (имя которого может вычисляться, например, по формуле ИМЯ_СУЩНОСТИ-ИМЯ_МЕТОДА ). Правда есть одна сложность - в самом GenericDao есть методы, которые ненужно подменять, в частности это - метод load , загружающий объект из базы данных и метод save - сохраняющий объект в базе данных, соответственно.

Прежде всего интерфейс IGenericDao :

import ;

public interface IGenericDao< T extends Entity> {

public T load(Serializable id) ;

public void save(T entity) ;

public String getName() ;

Вместо его полной реализации покажу лишь небольшую заглушку. Саму реализацию написать несложно:

package name.samolisov.proxy.dao ;

import java.io.Serializable ;

import name.samolisov.proxy.dao.entity.Entity ;

public class GenericDao< T extends Entity> implements IGenericDao< T> {

private Class< T> clazz;

public GenericDao(Class< T> clazz) {

this .clazz = clazz;

public T load(Serializable id) {

System .out .println ("invoce GenericDao#load, id = " + id) ;

return null ;

public void save(T entity) {

System .out .println ("invoce GenericDao#save, entity = " + entity) ;

public String getName() {

return clazz.getSimpleName () ;

Теперь самое главное - GenericDaoProxy . Создание прокси вынесем в статический метод newInstance , который будет принимать экземпляр класса GenericDao , параметризованный нужным нам типом сущности, и список интерфейсов - DAO для этой сущности. Опять же приведу код заглушки, в которую вместо System.out.println("will be requested by name " + dao.getName() + "-" + method.getName()); return null; нужно будет вставить реальное обращение к Hibernate :

package name.samolisov.proxy.dao ;

import java.lang.reflect.InvocationHandler ;

import java.lang.reflect.InvocationTargetException ;

import java.lang.reflect.Method ;

import java.lang.reflect.Proxy ;

import name.samolisov.proxy.dao.entity.Entity ;

public class GenericDaoProxy< T extends Entity> implements InvocationHandler {

private IGenericDao< T> dao;

private GenericDaoProxy(IGenericDao< T> dao) {

this .dao = dao;

@SuppressWarnings ("unchecked" )

public static < T extends Entity> IGenericDao< T> newInstance(IGenericDao< T> dao, Class ...interf ) {

return (IGenericDao< T> ) Proxy .newProxyInstance (

Dao.getClass () .getClassLoader () ,

new GenericDaoProxy< T> (dao) ) ;

public Object invoke(Object proxy, Method method, Object args) throws Throwable {

Class declaringClass = method.getDeclaringClass () ;

for (Class interf: dao.getClass () .getInterfaces () ) {

if (declaringClass.isAssignableFrom (interf) ) {

try {

return method.invoke (dao, args) ;

} catch (InvocationTargetException e) {

throw e.getTargetException () ;

System .out .println ("will be requested by name " + dao.getName () + "-" + method.getName () ) ;

return null ;

Рассмотрим подробнее метод invoke . В данном методе мы сначала вычисляем где был определен метод, представленный параметром method . Если данный метод был определен в классе объекта dao или в одном из интерфейсов, реализуемых данным классом, то просто вызываем его. Тем самым мы реализуем прямой вызов методов load , save и других, определенных в классе GenericDao . Если же вызываемый метод определен вне класса GenericDao или его интерфейсов, то обращаемся к Hibernate с именованным запросом.

Теперь посмотрим, как данные конструкции можно использовать:

IUserDao dao = (IUserDao) GenericDaoProxy.newInstance (new GenericDao< User> (User.class ) , IUserDao.class ) ;

Dao.findAllUsers () ;

Dao.load (10 ) ;

Повторю, что прокси-объект приводим к любому интерфейсу, для которого он создан. В нашем случае это - интерфейс IUserDao , в котором определен метод findAllUsers() . При вызове данного метода будет осуществлен запрос к Hibernate . Метод load же будет вызван напрямую, т.к. он определен в классе GenericDao .

Замечу, что именно через использование динамических прокси-классов работает Spring AOP , причем т.к. все бины в Spring создаются специальной фабрикой и хранятся в IoC -контейнере, программисту может быть неизвестно, что именно будет создано - объект нужного класса или прокси для этого объекта. Это может вызвать проблемы, если в коде используется явное приведение типов к классам. Приведение типов, когда тип приводится не к интерфейсу, а непосредственно к классу, является нарушение принципов концепции инверсии зависимостей и, по-хорошему, вообще не должно использоваться.

Так же стоит отметить, что помимо средств проксирования, которые определены в JVM , существуют библиотеки, например , обладающие более широкими возможностями. Впрочем, это уже тема другого разговора.

Понравилось сообщение - подпишитесь на блог или читайте меня в