воскресенье, 11 апреля 2010 г.

Об использовании динамических Proxy-классов в Java


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


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

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


Здесь 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:

package name.samolisov.proxy.dao;



import java.io.Serializable;



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



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(),

                interf,

                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, существуют библиотеки, например cglib, обладающие более широкими возможностями. Впрочем, это уже тема другого разговора.

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

2 комментария:

D комментирует...

Спасибо за статью. Все понятно, как всегда :)

Alexander комментирует...

Клево, можно не только Spring AOP вспомнить, но еще и Guice AOP. Хотя все на одном и том же работают )

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!