Dynamic Proxy
— Сегодня я расскажу тебе новую и очень интересную тему — динамические прокси.
В Java есть несколько способов изменить функциональность нужного класса…
Способ первый. Наследование
Самый простой способ изменить поведение некоторого класса — это создать новый класс, унаследовать его от оригинального (базового) и переопределить его методы. Затем, вместо объектов оригинального класса использовать объекты класса наследника. Пример:
Reader reader = new UserCustomReader();
Способ второй. Использование класса-обертки (Wrapper).
Примером такого класса является BufferedReader . Во-первых, он унаследован от Reader, то есть может быть использован вместо него. Во-вторых, он переадресует все вызовы к оригинальному объекту Reader, который обязательно нужно передать в конструкторе объекту BufferedReader. Пример:
Reader readerOriginal = new UserCustomReader(); Reader reader = new BufferedReader(readerOriginal);
Способ третий. Создание динамического прокси (Proxy).
В Java есть специальный класс (java.lang.reflect.Proxy), с помощью которого фактически можно сконструировать объект во время исполнения программы (динамически), не создавая для него отдельного класса.
Это делается очень просто:
Reader reader = (Reader)Proxy.newProxyInstance();
— А вот это уже что-то новенькое!
— Но, нам ведь не нужен просто объект без методов. Надо чтобы у этого объекта были методы, и они делали то, что нам нужно. Для этого в Java используется специальный интерфейс InvocationHandler, с помощью которого можно перехватывать все вызовы методов , обращенные к proxy-объекту. proxy-объект можно создать только используя интерфейсы.
Invoke – стандартное название для метода/класса, основная задача которого просто вызвать какой-то метод.
Handler – стандартное название для класса, который обрабатывает какое-то событие. Например, обработчик клика мышки будет называться MouseClickHandler, и т.д.
У интерфейса InvocationHandler есть единственный метод invoke, в который направляются все вызовы, обращенные к proxy-объекту . Пример:
Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler()); reader.close();
class CustomInvocationHandler implements InvocationHandler < public Object invoke(Object proxy, Method method, Object[] args) throws Throwable < System.out.println("yes!"); return null; > >
При вызове метода reader . close (), вызовется метод invoke , и на экран будет выведена надпись “yes!”
— Т.е. мы объявили класс CustomInvocationHandler, в нем реализовали интерфейс InvocationHandler и его метод invoke. Метод invoke при вызове выводит на экран строку “yes!”- Затем мы создали объект типа CustomInvocationHandler и передали его в метод newProxyInstance при создании объекта-proxy.
Это очень мощный инструмент, обычно создание таких прокси используется для имитации объектов из программ, которые физически запущены на другом компьютере. Или для контроля доступа
– в таком методе можно проверять права текущего пользователя, обрабатывать ошибки, логировать ошибки и многое другое.
Вот пример, где метод invoke еще и вызывает методы оригинального объекта:
Reader original = new UserCustomReader(); Reader reader = (Reader)Proxy.newProxyInstance(new CustomInvocationHandler(original)); reader.close();
class CustomInvocationHandler implements InvocationHandler private Reader readerOriginal; CustomInvocationHandler(Reader readerOriginal) < this.readerOriginal = readerOriginal; > public Object invoke(Object proxy, Method method, Object[] args) throws Throwable < if (method.getName().equals("close")) < System.out.println("Reader closed!"); > // это вызов метода close у объекта readerOriginal // имя метода и описание его параметров хранится в переменной method return method.invoke(readerOriginal, args); > >
В данном примере есть две особенности.
Во-первых, в конструктор передается «оригинальный» объект Reader , ссылка на который сохраняется внутри CustomInvocationHandler .
Во-вторых, в методе invoke мы снова вызываем этот же метод, но уже у «оригинального» объекта.
— Ага. Т.е. вот эта последняя строчка и есть вызов того же самого метода, но уже у оригинального объекта:
return method.invoke(readerOriginal, args);
— Не сказал бы, что слишком очевидно, но все же понятно. Вроде бы.
— Отлично. Тогда вот еще что. В метод newProxyInstance нужно передавать еще немного служебной информации для создания proxy-объекта. Но, т.к. мы не создаем монструозные прокси-объекты, то эту информацию легко получить из самого оригинального класса.
Reader original = new UserCustomReader(); ClassLoader classLoader = original.getClass().getClassLoader(); Class[] interfaces = original.getClass().getInterfaces(); CustomInvocationHandler invocationHandler = new CustomInvocationHandler(original); Reader reader = (Reader)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
class CustomInvocationHandler implements InvocationHandler < public Object invoke(Object proxy, Method method, Object[] args) throws Throwable < return null; > >
— Ага. ClassLoader и список интерфейсов. Это что-то из Reflection, да?
— Ясно. Что ж, думаю, я смогу создать примитивный простенький прокси объект, если это когда-нибудь мне понадобится.
Может кому будет полезно изложение этой темы без кода: «Dynamic Proxy — это механизм в языке программирования Java, позволяющий создавать объекты, которые могут перехватывать вызовы методов и добавлять дополнительное поведение перед или после выполнения этих методов. Допустим, у вас есть класс или интерфейс, и вы хотите добавить некоторую дополнительную функциональность к его методам без изменения его исходного кода. Например, вы можете захотеть добавить логирование, мониторинг или проверку безопасности. Dynamic Proxy позволяет вам создать объект, который выглядит и ведет себя так же, как и оригинальный класс или интерфейс, но с дополнительными возможностями. Вы можете перехватывать вызовы методов на этом прокси-объекте и добавлять свой код до или после выполнения оригинального метода. Например, представьте, что у вас есть класс для отправки электронных писем, и вы хотите добавить логирование каждого отправленного письма. Вместо того, чтобы изменять код этого класса и вносить в него логирование, вы можете использовать Dynamic Proxy. Вы создаете прокси-объект для класса отправки писем, который реализует тот же интерфейс. Когда вызывается метод отправки письма на прокси-объекте, он перехватывается прокси-механизмом, и вы можете добавить код для записи лога перед вызовом оригинального метода. Затем прокси-механизм передает вызов оригинальному объекту, который фактически отправляет письмо. После выполнения метода отправки письма прокси-механизм может выполнить дополнительные действия, например, записать информацию о том, что письмо было успешно отправлено. Таким образом, Dynamic Proxy дает возможность добавлять дополнительное поведение к существующим классам или интерфейсам, не изменяя их код. Это полезно, когда вы хотите добавить функциональность, такую как логирование, мониторинг или проверку безопасности, без вмешательства в сам класс или интерфейс.»
Динамический прокси Java: что это и как им пользоваться?
Ну что ж до Нового года и старта десятого потока «Разработчик Java» осталось совсем шуть-шуть. Так что у нас остался один открытый урок, который мы подготавливаем для публикации и сегодняшняя заметка, из которой вы узнаете о динамическом прокси Java: что это такое, когда и как его использовать в коде.
Прокси — это шаблон проектирования. Мы создаем и используем его для добавления и изменения функционала уже существующих классов. В таком случае, прокси-объект применяется вместо исходного. Обычно он использует тот же метод, что и оригинальный, и в Java прокси-классы расширяют исходные. Прокси может вызвать метод исходного объекта, так как у него есть дескриптор оригинала.
Таким образом, прокси-классы удобно реализуют многие вещи:
- логирование старта и остановки метода;
- дополнительную проверку аргументов;
- имитацию поведения исходного класса;
- реализацию отложенной инициализации затратных ресурсов;
Все это происходит без изменений оригинального кода класса. Полный список не ограничивается примерами выше, они лишь его малая часть.
На практике, прокси-класс напрямую не реализует функционал. Следуя принципу единственной ответственности, прокси-класс непосредственно выполняет только проксирование, а изменение поведения реализуется в обработчиках. При вызове прокси-объекта вместо исходного, сам прокси решает, вызвать ли оригинальный метод или какие-то обработчики. Обработчик может выполнить как собственную задачу, так и обратиться к оригинальному методу.
Хоть шаблон прокси применяется не только для создания прокси-объекта и класса в среде выполнения, в Java это особенно интересная тема. В этой статье я фокусируюсь именно на таких прокси.
Это сложная тема, которая требует использования класса отражения, или манипулирования байт-кодом, или компиляции Java-кода, сгенерированного динамически. А может всего и сразу. Чтобы новый класс не был доступен в качестве байт-кода во время исполнения, потребуются сгенерированный байт-код и загрузчик классов для загрузки байт-кода. Для создания байт-кода, используйте cglib, bytebuddy или встроенный компилятор Java.
Важность разделения ответственностей, в нашем случае, становится ясна, стоит лишь подумать о прокси-классах и вызываемых ими обработчиках. Прокси-класс генерируется во время выполнения, но вызванные им обработчики могут быть добавлены в обычный исходный код и скомпилированы с остальной программой.
Как этим пользоваться в нашем коде?
Самое простое — использовать java.lang.reflect.Proxy , который является частью JDK. Этот класс может создать прокси-класс или напрямую его инстанс. Пользоваться прокси, встроенным в Java, очень просто. Все что нужно — реализовать java.lang.InvocationHandler , чтобы прокси-объект мог его вызывать. Интерфейс InvocationHandler крайне прост и содержит только один метод: invoke() . При его вызове, аргументы содержат проксируемый оригинальный объект, вызванный метод (как отражение объекта Method ) и массив объектов исходных аргументов. Фрагмент кода ниже демонстрирует применение:
package proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class JdkProxyDemo < interface If < void originalMethod(String s); >static class Original implements If < public void originalMethod(String s) < System.out.println(s); >> static class Handler implements InvocationHandler < private final If original; public Handler(If original) < this.original = original; >public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException < System.out.println("BEFORE"); method.invoke(original, args); System.out.println("AFTER"); return null; >> public static void main(String[] args)< Original original = new Original(); Handler handler = new Handler(original); If f = (If) Proxy.newProxyInstance(If.class.getClassLoader(), new Class[] < If.class >, handler); f.originalMethod("Hallo"); > >
Для вызова оригинального метода исходного объекта, обработчику необходим доступ к нему. Что не предоставлено реализацией прокси Java. Вам понадобится самостоятельно передать аргумент инстансу обработчика в коде. (Обратите внимание на объект (обычно с названием proxy), который передается в качестве аргумента вызываемому обработчику. Это прокси-объект, который отражение Java генерирует динамически, а не тот объект, что мы хотим проксировать.) Таким образом, вы можете использовать как отдельные объекты-обработчики для каждого исходного класса, так и общий объект, который знает, как вызвать оригинальный объект, если для этого вообще есть какой-либо метод.
В особом случае, вы можете создать обработчик вызова и прокси интерфейса без оригинального объекта. Более того, класс для реализации интерфейса в исходном коде — не требуется. Его реализует динамически созданный прокси-класс.
Если же проксируемый класс не реализует интерфейс, стоит задуматься об использовании какой-либо иной реализации прокси.
Ждём ваши комментарии и вопросы. Как всегда или тут, или можно зайти к Виталию на день открытых дверей.