Динамический прокси / Java Dynamic Proxy

Не смотря на мой не большой опыт в java я все же могу по праву считать себя фанатиком, хлебом не корми дай поковыряться в чем нибудь и вот после очередного отвлечения на Wicket приходится вновь освежать память с чем мне обычно помогают такие люди как Юрий Ткач и Евгений Матюшкин(Skipy) своими статьями и видео за что им собственно спасибо. Так как сижу уже примерно пол года в пассивном поиске работы, фриланс спокойно позволяет заниматься любимым делом и не рваться в лапы работорговли, занимаюсь по мелочи своими стартапами. Учу что-то новое,  вот в очередной раз перечитывая статью Skipy про «синхронизацию GUI« вновь встретился с такой полезной штукой как динамический прокси и поймав себя на мысли что совсем не помню что и зачем решил сделать заметку на будущее.

содержание

История

Концепция динамических прокси-классов появилась еще в 2000 году в JDK 1.3.
С тех ее используют все, кому не лень. Более того, она стала даже де-факто стандартом в некоторых областях java.

Пример

Интерфейс

Реализуем интерфейс IUser

InvocationHandler — интерфейс, реализованный обработчиком вызова экземпляра прокси. У каждого экземпляра прокси есть связанный обработчик вызова. Когда метод вызывается на экземпляр прокси, вызов метода кодируется и диспетчеризируется invoke метод его обработчика вызова.

Далее как пользоваться… чудеса))

Ограничения и свойства

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

На передаваемые параметры есть ряд ограничений:
  1. Все объекты в массиве interfaces должны быть интерфейсами. Они не могут быть классами или примитивами.
  2. В массиве interfaces не может быть двух одинаковых объектов.
  3. Все интерфейсы в массиве interfaces должны быть загружены тем класс-лоадером, который передается в метод getProxyClass.
  4. Все не публичные интерфейсы должны быть определены в одном и том же пакете, иначе генерируемый прокси-класс не сможет их все реализовать.
  5. Ни в каких двух интерфейсах не может быть метода с одинаковым названием и сигнатурой параметров, но с разными типами возвращаемого значения.
  6. Длина массива interfaces ограничена 65535-ю интерфейсами. Никакой Java-класс не может реализовывать более 65535 интерфейсов (а так хотелось!).

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

Свойства динамического прокси-класса
  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 будут вызываться напрямую.

 Магия за кулисами

Объект User — вполне обычный, никакой магии.

Proxy.newProxyInstance — сами истоки магии, параметры вызова следующие:

  • ClassLoader класса User, о нем немного ниже;
  • Массив типа Class, должен принимать массив интерфейсов, которые реализует наш класс (User). МЕТОДЫ ЭТИХ ИНТЕРФЕЙСОВ БУДУТ ПЕРЕХВАТЫВАТЬСЯ (invocationHandler-ом).
  • Экземпляр InvocationHandler, который будет перехватывать методы вызываемые для объекта user (на самом деле, вызовы будут идти через вновь созданный userProxy).

На выходе получаем экземпляр некого класса(прокси), дающий следующую магическую функциональность :

  • Исполняет все методы интерфейсов, переданных во 2-ом параметре на вход при вызове Proxy.newProxyInstance (в нашем примере это getName,setName,rename). В этом он похож на User;
  • При вызове этих методов нашего экземпляра (например userProxy.setName) вызывается метод INVOKE() InvocationHandler-а. InvocationHandler уже дальше решает, как ему поступить —
    • вызвать соответствующий метод реального класса User
    • cделать что-то еще, в нашем случае

То есть  в нашем примере InvocationHandler просто выводит имя вызываемого метода в консоль и вызывает его для сохраненного объекта. Таким образом, перед каждым исполнением методов user будет выводиться название вызванного метода.

Зачем это нужно?

Скажем есть у нас «жирный» класс с кучей методов в которых мы захотели выполнять одно и то же действие — например логирование, audit trail, security, сериализация результатов. В классическом OOП такая задача решается либо копи-пастом, либо выделением каждого метода в отдельный класс, что тоже не удобно. И в этот самый момент на помощь приходит магия под названием рефлексия в виде Proxy. Достаточно выделить методы класса в интерфейс, чтобы создать обертку, которая будет выполнять действия одинаковые для всех методов (до или после логики самого метода). В целом это уже дебри АОП кому нужны подробности прошу сюда.

Background (самое интересное)

Откуда же взялся этот «некий класс», экземпляр которого мы получили на выходе Proxy.newProxyInstance?

Это динамически созданный класс, созданный ИЗ МАССИВА БАЙТ.
Цепочка вызовов : Proxy.newProxyInstance -> Proxy.getProxyClass -> sun.misc.ProxyGenerator.generateProxyClass
Этот последний метод возвращает массив байтов, который потом посредством ClassLoader.defineClass преобразуется в Class, и далее newInstance.
В результате мы получаем программу которая генерирует сама себя.
Естественно, хочется поглядеть, что с генерировал Peter Jones .

К счастью,  можно получить байт-код генерируемого класса, установив магическое (и никому не известное) системное свойство «sun.misc.ProxyGenerator.saveGeneratedFiles» в true перед созданием класса.

Заключение

При проксировании метода возвращающего примитивный тип, InvocationHandler не должен возвращать null, или примитив другого типа иначе получим NullPointerException/ClassCastException в прокси-классе.
В документации написано «type-safe» типы, тогда и не должен прокси-класс падать при передаче ему неверного/null параметра.

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

Список литературы

Подписаться
Уведомлять о
guest

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

6 комментариев
старым
новым колличеству голосов
Межтекстовые Отзывы
Посмотреть все комментарии
Samuil
Samuil
21.09.2014 10:25

Спасибо за статью. Увидел свет в конце туннеля 🙂

trackback
Пример работы с динамическими прокси-классами в Scala | Записки программиста
28.01.2015 18:06
JavaBean
JavaBean
08.10.2015 14:59

Супер! Все доходчиво и понятно.

Алексей
Алексей
05.05.2017 23:04

Вот такой пример есть в интернетах (остальной код не привожу, он почти всегда одинаков)
Reader reader =(Reader)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);

Решил проверить, запустил — падение с Exception in thread «main» java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to java.io.Reader.

Cидел пока не осинило, что Reader — это не интерфейс в принципе, а абстрактный класс. Я же правильно понимаю, что объект прокси класса приводим только к интерфейсам, и этот код из примера — это «фу-фу-фу»

Abstract
Abstract
13.12.2017 18:31

Был бы я бабой — захотел бы от тебя детей. Шикарнейшая статья

Алекс
Алекс
14.06.2019 10:45

хм, а откуда появился Person ? ))) или все-таки там должен быть User?

6
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x