Динамические прокси-классы
Сегодня мы поговорим о такой интересной особенности 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:
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();
}
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();
}
}
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;
}
}
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);
dao.findAllUsers();
dao.load(10);
Повторю, что прокси-объект приводим к любому интерфейсу, для которого он создан. В нашем случае это - интерфейс IUserDao, в котором определен метод findAllUsers(). При вызове данного метода будет осуществлен запрос к Hibernate. Метод load же будет вызван напрямую, т.к. он определен в классе GenericDao.
Замечу, что именно через использование динамических прокси-классов работает Spring AOP, причем т.к. все бины в Spring создаются специальной фабрикой и хранятся в IoC-контейнере, программисту может быть неизвестно, что именно будет создано - объект нужного класса или прокси для этого объекта. Это может вызвать проблемы, если в коде используется явное приведение типов к классам. Приведение типов, когда тип приводится не к интерфейсу, а непосредственно к классу, является нарушение принципов концепции инверсии зависимостей и, по-хорошему, вообще не должно использоваться.
Так же стоит отметить, что помимо средств проксирования, которые определены в JVM, существуют библиотеки, например cglib, обладающие более широкими возможностями. Впрочем, это уже тема другого разговора.
Понравилось сообщение - подпишитесь на блог или читайте меня в twitter
2 комментария:
Спасибо за статью. Все понятно, как всегда :)
Клево, можно не только Spring AOP вспомнить, но еще и Guice AOP. Хотя все на одном и том же работают )
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!