Сегодня мы расскажем о любопытном и не очень широко известном классе, предоставляемым пакетом java.lang.reflect, классе java.lang.reflect.Proxy. Этот класс появился в JDK1.3 и несколько выделяется среди остальных классов пакета java.lang.reflect. В отличие от них, Proxy используется не для получения информации, относящейся к загруженным классам, а для динамического создания новых классов.
Класс Proxy предоставляет всего 4 статических метода, и, пожалуй, наиболее часто используемый из них—это
staticObject Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h). В
результате вызова этого метода динамически генерируется класс (называемый класс-заместитель), реализующий набор интерфейсов,
переданных в массиве interfaces, а затем создаётся и возвращается экземпляр этого класса. Поскольку Java интерфейс не определяет
никакой реализации объявленных в нём методов, то создаваемый класс-заместитель предоставляет шаблонную реализацию
для всех интерфейсных методов—их вызовы перенаправляются специальному объекту—InvocationHandler'у,
который и должен предоставлять их реализацию
(те, кто интересуется деталями динамического создания Java классов, могут найти пример реализации генерации
байт кода в классе sun.misc.ProxyGenerator, на самом деле
динамическое создание Java класса это не очень сложный процесс: формат
файла, представляющего класс, довольно прост и, думаю, что познакомившись
со спецификацией
виртуальной машины, реализовать генерацию байт кода может каждый).
Интерфейс InvocationHandler содержит всего один метод: Object invoke(Object proxy, Method method, Object[] args),
в этот метод передаётся объект класса-заместителя, для которого был вызван интерфейсный метод (proxy),
сам вызванный метод (method) и его параметры (args).
Справка javadoc предоставляет подробную информацию о классе Proxy, но я всё-таки приведу маленький пример, который может прояснить принцип его использования:
// ProxyExample.java
import java.lang.reflect.*;
/*
* Интерфейс, объявляющий 2 метода
*/
interface InterfaceExample {
public String interfaceMethod1();
public String interfaceMethod2();
}
/*
* Обработчик вызова методов, объявленных в интерфейсе InterfaceExample
*/
class InvocationHandlerExample implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) {
/*
* Выполнить действия в зависимости от вызванного метода
*/
if (method.getName().equals("interfaceMethod1")) {
return "interfaceMethod1 was called";
} else if (method.getName().equals("interfaceMethod2")) {
return "interfaceMethod2 was called";
}
throw new IllegalArgumentException("Unknown method was called: " + method);
}
}
public class ProxyExample {
public static void main(String[] args) {
/*
* Создаём объект класса-заместителя
*/
Object proxyObject = Proxy.newProxyInstance(
ProxyExample.class.getClassLoader(),
new Class[]{InterfaceExample.class},
new InvocationHandlerExample());
String message;
/*
* Следующие 2 вызова методов интерфейса InterfaceExample должны быть обработаны
* объектом InvocationHandlerExample
*/
message = ((InterfaceExample)proxyObject).interfaceMethod1();
System.out.println(message);
message = ((InterfaceExample)proxyObject).interfaceMethod2();
System.out.println(message);
}
}
И, как нетрудно догадаться, при запуске программа выведет следующее:
interfaceMethod1 was called
interfaceMethod2 was called
Этот пример просто демонстрирует способ использования API, предоставлемого java.lang.reflect и, естественно,
в этом примере использование Proxy абсолютно излишне: если во время написания программы нам известно, какой
интерфейс необходимо реализовать (в этом примере это интерфейс InterfaceExample), то можно просто
написать класс, реализующий этот интерфейс, и нет никакой необходимости связываться с созданием класса-заместителя.
Использование Proxy оправдано и необходимо в том случае,
когда на момент написания программы неизвестно, какой интерфейс должен предоставлять объект во время выполнения.
Для демонстрации возможностей этого класса посмотрим, как он используется в JDK.
Рассмотрим, например, класс javax.management.JMX. Этот класс содержит метод, возвращающий объект-заместитель для MBean'а,
предоставляемого локальным или удалённым MBean сервером:
static <T> T newMBeanProxy(MBeanServerConnection connection, ObjectName objectName, Class<T> interfaceClass)
.
Например, если MBean сервер, представленный объектом server, содержит MBean с именем "MyMBeanName", и интерфейс для
этого MBean'а описан с помощью Java интерфейса MyMBean, то создать объект-заместитель можно так:
MyMBean proxy = JMX.newMBeanProxy(server, "MyMBeanName", MyMBean.class)
Объект, возвращемый методом newMBeanProxy должен удовлетворять следующим тебованиям:
- Чтобы клент мог работать с этим объектом, как с реальным экземпляром MyMBean, объект должен реализовавать интерфейс MyMBean.
- Объект должен перенаправлять вызовы методов интерфейса MyMBean серверу, в котором расположен
реальный объект, реализующий интерфейс MyMBean, причём в силу спецификации механизма JMX, способ перенаправление
запросов к серверу не зависит от конкретного интрефейса MBean'а. Так, например, для запросов значений атрибутов MBean'ов
используются методы вида
getAttributeName(), и эти запросы могут быть перенаправлены заместителем MBean серверу какserver.getAttribute("objectName", "AttributeName"). А запросы на изменения значений атрибутов (setAttributeName(attributeValue)) какserver.setAttribute("objectName", new Attribute("AttributeName", attributeValue)).
Таким образом, можно создать только один универсальный класс-заместитель, который мог бы перенаправлять серверу
запросы любого MBean'a. Единственная проблема—неизвестно, интерфейсы каких именно MBean'ов должен реализовывать
этот класс (точнее, в этот класс должна добавляться реализация интерфейса любого запрошенного пользователем MBean'а).
В этом случае использование java.lang.reflect.Proxy—оправданное и красивое решение: при вызове метода newMBeanProxy с помощью Proxy
создается класс-заместитель, реализующий интерфейсы заданных MBean'ов,
а вся работа по перенаправлению вызовов методов нужного MBean реализована в
классе MBeanServerInvocationHandler,
который реализует интерфейс java.lang.reflect.InvocationHandler. Реализация метода InvocationHandler.invoke выглядит примерно так:
class MBeanServerInvocationHandler implements InvocationHandler {
...
public Object invoke(Object proxy, Method method, Object[] args) {
...
String methodName = method.getName();
if (methodName.startsWith("get") {
return connection.getAttribute(objectName, methodName.substring(3));
}
if (methodName.startsWith("set"))) {
Attribute attr = new Attribute(methodName.substring(3), args[0]);
connection.setAttribute(objectName, attr);
return null;
}
...
}
...
}
Другой интересный случай использования Proxy—это реализация поддержки аннотаций (Annotations).
Начиная с версии 1.5 в языке Java реализована возможность добавления метаданных к программным элементам (таким как,
например, классы, методы, пакеты).
Определение аннотации аналогично определению интерфейса, только для определения аннотации используется ключевое
слово @interface:
@interface MyRuntimeAnnotation {
String myAnnotationText();
}
У аннотации может быть 3 степени видимости:
- аннотация доступна только во время компиляции
- аннотация сохраняется в класс-файле, но не доступна во время выполнения
- аннотация доступна всегда
Для задания видимости аннотации используется аннотация Retention:
@Retention(RetentionPolicy.RUNTIME)
@interface MyRuntimeAnnotation {
String myAnnotationText();
}
Теперь аннотация MyRuntimeAnnotation доступна всегда, и если при определении
класса к его имени добавить эту аннотацию:
@MyRuntimeAnnotation(myAnnotationText = "Any text")
class MyAnnotatedClass {
...
...
}
то во время выполнения программы можно будет получить все аннотации класса (Class.getDeclaredAnnotations()) и значения атрибутов аннотаций:
((MyRuntimeAnnotation)(MyAnnotatedClass.class.getDeclaredAnnotations()[0])).myAnnotationText();
В чём сложность реализации поддержки аннотаций во время выполнения? Как видно из приведенного выше кода, метод Class.getDeclaredAnnotations() возвращает объект, реализующий интерфейс MyRuntimeAnnotation, но где находится класс, реализующий этот интерфейс? Согласно JSR 175
определение аннотации практически эквивалентно определению обычного
интерфейса, то есть класс-файл аннотации не содержит реализации
объявленных в ней методов, а при аннотировании элемента, информация о
добавленных аннотациях сохраняется в класс-файле в виде специального
атрибута (RuntimeVisibleAnnotations), который фактически представляет
набор элементов вида [тип атрибута] - [имя атрибута] - [значение
атрибута], то есть получается, что класс-файлов с реализацией методов
аннотаций просто не существует. В то же время к классу могут быть
добавлены абсолютно любые аннотации, и метод Class.getDeclaredAnnotations() должен уметь возвращать объекты, реализующие интерфейсы, задаваемые этими аннотациями.
Как вы уже поняли, эта проблема легко решается с помощью java.lang.reflect.Proxy: в момент запроса аннотаций (например, при вызове Class.getDeclaredAnnotations()) sun.reflect.annotation.AnnotationParser выполняет разбор класс-файла, находит аннотации и значения их атрибутов, а затем, используя java.lang.reflect.Proxy, создаёт объекты-заместители, реализующие интерфейсы аннотаций (обработка запросов атрибутов аннотаций реализована в классе sun.reflect.annotation.AnnotationInvocationHandler).
Таким образом, класс java.lang.reflect.Proxy действительно полезный и мощный инструмент. Надеюсь, что эти примеры заинтересовали вас и, возможно, помогут использовать java.lang.reflect.Proxy в ваших программах.
Ссылки:
Семён Бойков
опубликовал vmrobot ( мар 29 2007, 06:20:39 AM MSD ) Permalink Комментарии [3]

опубликовал Alex Май 24, 2007 at 01:25 AM MSD #
опубликовал Семён Май 24, 2007 at 11:47 AM MSD #
Proxy - мощная вещь. Я применял ее, когда писал свою библиотеку RMI. Странно, но стандартный RMI, похоже, не использует Proxy и геренирует _Stub классы через rmic. В 1.5 это извращение убрали, однако классы, имплементирующие Remote-объект все-равно требуются на клиенте, то есть требуется таскать с собой практически ВСЕ.
опубликовал null Август 08, 2007 at 03:55 PM MSD #