Посредники (proxies) хорошо подходят для простых ситуаций — например, для создания «оберток» для вызова методов отдельных объектов или классов. Тем не менее динамические посредники, содержащиеся в JDK, работают только с интерфейсами. Чтобы создать посредника для класса, приходится использовать библиотеки для выполнения манипуляций с байт-кодом — такие, как CGLIB, ASM или Javassist[40].
В листинге 11.3 приведена заготовка посредника JDK, обеспечивающего поддержку сохранения объектов в нашем приложении Bank (представлены только методы чтения/записи списка счетов).
Листинг 11.3. Пример посредника JDK// Bank.java (подавление имен пакетов...)
import java.utils.*;
// Абстрактное представление банка.
public interface Bank {
Collection getAccounts();
void setAccounts(Collection accounts);
}
// BankImpl.java
import java.utils.*;
// POJO-объект ("Plain Old Java Object"), реализующий абстракцию.
public class BankImpl implements Bank {
private List accounts;
public Collection getAccounts() {
return accounts;
}
public void setAccounts(Collection accounts) {
this.accounts = new ArrayList();
for (Account account: accounts) {
this.accounts.add(account);
}
}
}
// BankProxyHandler.java
import java.lang.reflect.*;
import java.util.*;
// Реализация InvocationHandler, необходимая для API посредника.
public class BankProxyHandler implements InvocationHandler {
private Bank bank;
public BankHandler (Bank bank) {
this.bank = bank;
}
// Метод, определенный в InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
if (methodName.equals("getAccounts")) {
bank.setAccounts(getAccountsFromDatabase());
return bank.getAccounts();
} else if (methodName.equals("setAccounts")) {
bank.setAccounts((Collection) args[0]);
setAccountsToDatabase(bank.getAccounts());
return null;
} else {
...
}
}
// Подробности:
protected Collection getAccountsFromDatabase() { ... }
protected void setAccountsToDatabase(Collection accounts) { ... }
}
// В другом месте...
Bank bank = (Bank) Proxy.newProxyInstance(
Bank.class.getClassLoader(),
new Class[] { Bank.class },
new BankProxyHandler(new BankImpl()));
Мы определили интерфейс Bank, который будет инкапсулироваться посредником, и POJO-объект («Plain Old Java Object», то есть «обычный Java-объект») BankImpl, реализующий бизнес-логику. (Вскоре мы вернемся к теме POJO-объектов).
Для работы посредника необходим объект InvocationHandler, который вызывается для реализации всех вызовов методов Bank, обращенных к посреднику. Наша реализация BankProxyHandler использует механизм рефлексии Java для отображения вызовов обобщенных методов на соответствующие методы BankImpl.
Код получается весьма объемистым и относительно сложным, даже в этом простом случае[41]. Не меньше проблем создает и использование библиотек для манипуляций с байт-кодом. Объем и сложность кода — два основных недостатка посредников. Эти два фактора усложняют создание чистого кода! Кроме того, у посредников не существует механизма определения «точек интереса» общесистемного уровня, необходимых для полноценного АОП-решения[42].
АОП-инфраструктуры на «чистом» Java
К счастью, большая часть шаблонного кода посредников может автоматически обрабатываться вспомогательными средствами. Посредники используются во внутренней реализации нескольких инфраструктур Java — например, Spring AOP и JBoss AOP — для реализации аспектов непосредственно на уровне Java[43].
В Spring бизнес-логика записывается в форме POJO-объектов. Такие объекты полностью сосредоточены на своей предметной области. Они не имеют зависимостей во внешних инфраструктурах (или любых других областях); соответственно им присуща большая концептуальная простота и удобство тестирования. Благодаря относительной простоте вам будет проще обеспечить правильную реализацию соответствующих пожеланий пользователей, а также сопровождение и эволюцию кода при появлении новых пожеланий.
Вся необходимая инфраструктура приложения, включая поперечные области ответственности (сохранение объектов, транзакции, безопасность, кэширование, преодоление отказов и т.д.), определяется при помощи декларативных конфигурационных файлов или API. Во многих случаях вы фактически определяете аспекты библиотек Spring или JBoss, а инфраструктура берет на себя всю механику использования посредников Java или библиотек байт-кода в режиме, прозрачном для пользователя. Объявления управляют контейнером внедрения зависимостей (DI), который создает экземпляры основных объектов и связывает их по мере необходимости.
В листинге 11.4 приведен типичный фрагмент конфигурационного файла Spring V2.5 app.xml[44].
Листинг 11.4. Конфигурационный файл Spring 2.X
...
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="me"/>
class="com.example.banking.persistence.BankDataAccessObject"
p:dataSource-ref="appDataSource"/>
class="com.example.banking.model.Bank"
p:dataAccessObject-ref="bankDataAccessObject"/>
...
Каждый компонент напоминает одну из частей русской «матрешки»: объект предметной области Bank «упаковывается» в объект доступа к данным DAO (Data Accessor Object), который, в свою очередь, упаковывается в объект источника данных JDBC (рис. 11.3).
Рис. 11.3. «Матрешка» из декораторов
Клиент полагает, что он вызывает метод getAccounts() объекта Bank, но в действительности он взаимодействует с внешним объектом из набора вложенных ДЕКОРАТОРОВ [GOF], расширяющих базовое поведение POJO-объекта Bank. Мы могли бы добавить другие декораторы для транзакций, кэширования и т.д.
Чтобы запросить у DI-контейнера объекты верхнего уровня, заданные в файле XML, достаточно включить в приложение несколько строк:
XmlBeanFactory bf =
new XmlBeanFactory(new ClassPathResource("app.xml", getClass()));
Bank bank = (Bank) bf.getBean("bank");
Так как объем кода, специфического для Spring, минимален, приложение почти полностью изолировано от Spring. Тем самым устраняются все проблемы жесткой привязки, характерные для таких систем, как EJB2.
Хотя код XML занимает много места и плохо читается[45], определяемая в этих конфигурационных файлах «политика» все же проще сложной логики посредников и аспектов, скрытой от наших глаз и создаваемой автоматически. Архитектура выглядит настолько заманчиво, что инфраструктуры вроде Spring привели к полной переработке стандарта EJB для версии 3. EJB3 в значительной мере следует характерной для Spring модели декларативной поддержки поперечных областей ответственности с использованием конфигурационных файлов XML и/или аннотаций Java 5.