AOP@Work: АОП и метаданные: Совершенное соответствие, часть 1

Концепции и структуры использования метаданных в АОП

В этой статье, первой из двух, автор Рамнивас Ладдад (Ramnivas Laddad) приводит концептуальный обзор новой функции в Java™ - использование метаданных, и показывает как можно получить наибольшую выгоду от добавления аннотаций с использованием метаданных в АОП. Затем он проведет вас через процедуру рефакторинга дизайна, состоящую из пяти частей, начиная с не использующих метаданных реализаций АОП и завершая реализациями, в которых комбинируется шаблон проектирования Participant и составленные в аннотациях аспекты.

Ramnivas Laddad, Директор, Aspectivity

Рамнивас Ладдад (Ramnivas Laddad) является автором, докладчиком, консультантом и инструктором, специализирующимся в аспектно-ориентированном программировании и J2EE. Его последняя книга "AspectJ в действии: Практическое аспектно-ориентированное программирование" (Manning, 2003), была признана наиболее полезным руководством по AOP/AspectJ. Более десятилетия он разрабатывает сложные программные системы с использованием таких технологий как платформа Java, J2EE, AspectJ, UML, создание сетей и XML. Рамнивас является активным членом сообщества пользователей AspectJ и работает в области аспектно-ориентированного программирования с самого начала. Он регулярно докладывает на таких конференциях как JavaOne, No Fluff Just Stuff, Software Development, EclipseCon и O'Reilly OSCON. Рамнивас живет в Sunnyvale, California. Более подробно о Рамнивасе можно узнать на его Web-сайте: http://ramnivas.com.



08.03.2005

Новая возможность Java по использованию метаданных (как часть J2SE 5.0), возможно, наиболее значительное добавление к языку Java на сегодняшнее время. Обеспечивая стандартный способ для присоединения дополнительных данных к элементам программы, метаданные потенциально упрощают и улучшают многие области разработки приложений, включая управление конфигурацией, реализацию среды разработки и генерирование кода. Эта возможность имеет также особенно значительное влияние на аспектно-ориентированное программирование, или АОП.

Комбинирование метаданных и АОП поднимает серьезные вопросы, а именно:

  • Каково влияние возможности использования метаданных на АОП?
  • Является ли использование метаданных в АОП произвольным или оно необходимо?
  • Каковы основные принципы эффективного использования метаданных в АОП?
  • Как такая комбинация влияет на распространение АОП?

Я начну отвечать на поставленные вопросы в этой статье, состоящей из двух частей, второй по счету в новой серии AOP@Work. В первой части я начну с концептуального обзора метаданных и новой функции Java по поддержке метаданных. Также рассмотрю отличия между предоставлением метаданных и их потреблением и приведу несколько общих сценариев программирования, способствующих использованию аннотаций с метаданными. Затем я кратко рассмотрю основы модели точек соединения АОП и объясню, где можно получить выгоды от использования метаданных. Завершу я практическим примером, использующим АОП с метаданными, проходя через пять этапов. Во второй части я продемонстрирую новый способ просмотра метаданных как сигнатур многомерного пространства, поговорю о влиянии метаданных на распространение АОП и завершу некоторыми основными принципами эффективного комбинирования АОП и метаданных.

Рассматриваемые в статье концепции я буду показывать на практических примерах, используя три ведущие реализации АОП: AspectJ, AspectWerkz и JBoss AOP. Список вступительных статей по использованию метаданных в Java и по аспектно-ориентированному программированию вы можете найти в разделе "Ресурсы".

Концепции метаданных

Метаданные - это данные о данных. В контексте языка программирования метаданные представляют собой дополнительную информацию, прикрепленную к элементам программы, таким как методы, поля, классы и пакеты. Метаданные записываются при помощи программных элементов, называемых аннотациями. Семантики, связанные с метаданными, варьируются от обычной документации до изменения поведения во время исполнения. Например, вы можете использовать метаданные для указания автора и владельца прав класса, в этом случае это не повлияет на выполнение программы; либо вы можете использовать их для описания свойств метода, например характеристик процесса обработки транзакций, при этом, возможно, поменяется поведение метода, как я позднее опишу в этой статье.

Об этом цикле статей

Цикл статей AOP@Work предназначен для разработчиков, обладающих некоторыми основными знаниями аспектно-ориентированного программирования и желающих расширить или углубить их (материалы по основам АОП можно найти в разделе "Ресурсы"). Как и большинство статей developerWorks, статьи этого цикла очень практичны: из каждой статьи вы получите новые знания, которые можно сразу же использовать.

Каждый из авторов статей этой серии был выбран из-за превосходных знаний или компетентности в АОП. Многие авторы являются участниками рассматриваемых в статьях проектов или инструментальных средств. Каждая статья рецензируется, чтобы гарантировать справедливость и точность выраженной в ней точки зрения.

Связывайтесь, пожалуйста, с авторами лично, чтобы прокомментировать их статьи или задать о них вопрос. Чтобы прокомментировать цикл статей в целом, вы можете связаться с ее ведущим Николасом Лесицки (Nicholas Lesiecki). Дополнительные материалы по АОП можно найти в разделе "Ресурсы".

И хотя существует большое количество инструментов для работы с метаданными в языке Java (XDoclet является наиболее широко известным), аннотации метаданных были ведены в ядро языка Java только в версии Java 5.0. Функциональные возможности использования метаданных в Java (JSR 175; см. раздел "Ресурсы") включают механизм добавления пользовательских аннотаций в Java-код, а также предоставление программного доступа к аннотациям через отражение.

Ключевой концепцией для понимания и использования метаданных является концепция предоставления и потребления. Предоставление метаданных - это средство ассоциирования экземпляров аннотаций с элементами программы, в то время как потребление - это средство чтения, интерпретации и действий с экземплярами аннотаций. В следующем разделе я рассмотрю эти понятия более детально.

Предоставление метаданных

Средство работы с метаданными определяет механизм для предоставления аннотаций программным элементам. Как вариант, может указываться также способ определения типов аннотации. Типы аннотаций определяют шаблон для создания экземпляров аннотаций почти таким же способом, каким классы указывают шаблон для создания объектов. Средство работы с метаданными может затем проверить экземпляры метаданных на соответствие этим типам. Также может указываться общий способ потребления аннотаций. Что не делается, так это определение интерпретации и семантик, связанных с аннотациями. Это предоставлено потребителю и рассматривается в следующем разделе.

Возможности средств работы с метаданными очень разные. Например, спецификация Javadoc разрешает вам указывать аннотации в комментариях, ассоциированных с элементом программы. В качестве альтернативы некоторые средства используют отдельный документ (часто XML-документ) для записи метаданных. Например, описатели развертывания EJB указывают дополнительные характеристики корпоративных компонентов. В отличие от Javadoc такой подход соединяет элементы программы и метаданные слабыми связями; недостаток этого подхода - разработчик должен изменять различные источники, описывающие один и тот же элемент.

Средства работы с метаданными в Java добавляют в язык новую поддержку метаданных для объявления типов аннотаций и для аннотирования элементов программы. Также есть возможность запомнить метаданные на уровне исходного кода в файлах классов и во время исполнения путем управления политикой сохранения.

Поставщик метаданных может иметь возможность присоединять определенные аннотации пересекающимся способом, а не индивидуально, поставляя их нескольким элементам. Благодаря пересекающейся природе аннотаций, АОП - это хорошее средство для их предоставления. Я расскажу о деталях такой возможности позднее в этой статье.

Выбор средства работы с метаданными влияет на способы выражения метаданных, но фундаментальная идея об ассоциировании дополнительных данных с элементами программы является общей для всех них.

Потребление метаданных

Создание значения из аннотаций требует их потребления. Метаданные могут потребляться разными способами, и понимание их применения поможет осуществить комбинирование АОП и метаданных. Следующие примеры использования должны помочь вам понять, как аннотации, предоставленные не для АОП, могут быть потреблены в реализациях АОП.

Глазами очевидца

Метаданные - это дополнительная информация, связанная с программными элементами. Понятие "дополнительная" является субъективным. Более того, выбранный язык программирования влияет на то, что программисты считают внутренней информацией об элементе, а не метаданными. Например, проверяемые исключительные ситуации (checked exceptions) рассматриваются независимо от языка Java. Они могли бы считаться метаданными - инструкциями для компилятора. Фактически, в языках, не поддерживающих проверяемые исключительные ситуации (например, C#), метаданные являются хорошим выбором для предоставления аналогичной информации.

Подобным образом при помощи метаданных могут быть выражены даже типы аргументов и возвращаемых значений в слабо типизированных языках. Или представьте язык Java без родовых типов (generics). Такие среды программирования как Hibernate и EJB используют метаданные для указания такой же информации, какая может быть определена при помощи родовых типов Java. Даже спецификаторы (например спецификаторы доступа) в таких ситуациях могут считаться метаданными. Фактически, если кому-то это вскоре потребуется, встроенная функциональность аннотации @Override метаданных языка Java может быть реализована при помощи дополнительного спецификатора – скажем, ключевого слова override. То есть, различие между программным элементом и метаданными зависит от вашей точки зрения.

Генерирование кода

Генерирование кода, возможно, наиболее привычный способ использования метаданных. Используя инструменты типа XDoclet, вы можете принять указанные в виде тегов Javadoc аннотации для создания таких артефактов, как XML-документы или Java-код. Сгенерированный код в свою очередь влияет на поведение аннотированных элементов во время исполнения. Разрабатывается новая версия Xdoclet, которая поддерживает средства работы с метаданными в языке Java. Инструментальная программа командной строки apt (Annotation Processing Tool), часть дистрибутива Java 2 SDK 5.0, также предоставляет способ обработки аннотаций при помощи написания подключаемых модулей. Например, Contract4J, недавно выпущенная программа для поддержки контрактного программирования (DBC - design-by-contract), использует apt для генерирования.

Программное изменение поведения

Возможности стандартных метаданных позволяют сохранять доступность аннотаций во время исполнения. Они также разрешают программный доступ к экземплярам аннотаций при помощи отражения (reflection). Эти экземпляры аннотаций могут затем использоваться также как и другие объекты для изменения поведения программы. Такое программное использование может дать программистам возможность избежать рутинного генерирования кода приложений, в которых сгенерированные фрагменты позволяют чтение только закодированной в аннотациях информации.

Потребление средой

Метаданные обычно используются для взаимодействия между элементами программы и такими программными средами как EJB, EMF и TestNG. Среда сама может выбрать для реализации определенной логики использование генерирования кода, отраженного доступа или АОП. В EJB 3.0 предлагается использовать аннотации, например @Remove и @Session, для взаимодействия со средой в качестве программных элементов. Eclipse Modeling Framework использует аннотации (обозначаемые в настоящее время в виде тегов Javadoc) для создания UML-моделей и поддержки персистентности XML. Что касается инструментальных средств, то TestNG, например, использует метаданные для взаимодействия между наборами тестовых данных и программой проведения тестирования.

Расширение языка

Использование метаданных расширяет применяемый язык программирования и компилятор. Ассоциирование свойств семантики с метаданными означает, что скомпилированные классы могут иметь структуру и поведение, отличные от остальных классов (Дальнейшее обсуждение этой темы можно найти в разделе "Ресурсы"). Недавно анонсированный интерфейс программирования Pluggable Annotation Processing API (JSR 269) может привести к стандартному способу обработки аннотаций для этих целей. Использование метаданных для расширения языка программирования Java может быть как успешным, так и опасным: с одной стороны аннотации дают нам возможность добавить новые функции к языку Java без изменения основ языка, делая его таким образом открытым; в самом лучшем случае хорошо проработанное расширение может преодолеть некоторые ограничения языка. С другой стороны, нестандартная, специализированная или некорректная настройка аннотаций может привести к малопонятному коду.

Кстати говоря, использование АОП совместно с обычным объектно-ориентированным языком является примером использования метаданных для расширения языка. AspectWerkz и JBoss AOP используют метаданные для изменения семантики класса (в виде аспектов), полей данных (в виде pointcut), метода (в виде advice) и т.д. AspectJ 5, похоже, будет поддерживать синтаксис @AspectJ, что следует из слияния проектов AspectJ и AspectWerkz. Для получения дополнительной информации о различных реализациях АОП и расширениях языка Java смотрите раздел "Ресурсы".

В следующем разделе я кратко опишу основы АОП-модели точек соединения (join point model), потом покажу, где можно с выгодой применить метаданные.


Метаданные и модель точек соединения

Точка соединения представляет собой идентифицируемую точку в процессе выполнения программы. Модель точек соединения, наиболее фундаментальная и отличительная концепция в АОП, определяет набор указываемых точек соединения и способ их перехвата. Для реализации пересекающейся функциональности при помощи аспектов вам необходимо перехватывать нужные точки соединения с использованием программной конструкции под названием pointcut (срез точки).

Эти pointcut выбирают точки соединения и собирают их контекст. Все АОП-системы предоставляют язык для определения pointcut. Изощренность языка описания pointcut является отличительным фактором различных АОП-систем. Чем больше продуман язык описания pointcut, тем легче написать устойчивые pointcut. Детальное рассмотрение важности языка описания pointcut можно найти в первой статье серии AOP@Work (ссылка приведена в разделе "Ресурсы").

Перехват точек соединения

Pointcut определяет свойства данного элемента программы. Главным в науке и искусстве написания хороших аспектов является написание устойчивых pointcut; еще очень важным является хорошо продуманное наследование аспектов. Pointcut, перехватывающие больше ожидаемого количества точек соединения или пропускающие нужные точки соединения, могут привести к неустойчивой реализации системы. Написание хороших pointcut является ключевым вопросом овладения АОП, хотя часто это затруднительно для новичков.

В настоящее время наиболее общим способом перехвата точек соединения является использование неявных свойств программных элементов, включая такие статические свойства как сигнатура (в которую входят тип и название метода, типы аргументов, тип возвращаемого значения и тип исключительной ситуации и др.) и лексическое расположение, а также динамические свойства, такие как поток управления. Разумное использование групповых символов в сигнатурах точек соединения часто приводит к хорошим, лаконичным определениям pointcut. Можно также скомпоновать отдельные pointcut в более сложные. Модель точек соединения, основанная на неявных свойствах программных элементов, является как мощной, так и полезной, что и доказывает текущий успех АОП в нескольких промышленных системах.

Для перехвата нужных точек соединения часто достаточно доступной неявной информации в сигнатурах программных элементов. В этой модели, иногда называемой динамическим пересечением, комбинирование неявных данных, групповых символов и динамических свойств, таких как поток управления, дает возможность перехватывать точки соединения без изменения перехватываемых программных элементов. Например, вы можете перехватить все вызовы RMI, указав операции генерирования RemoteException в классе, реализующем интерфейс Remote. Такой pointcut как, например, execution(* Remote+.*(..) throws RemoteException) (определенный в AspectJ) аккуратно перехватывает все RMI-действия без модификации программных элементов, а также гарантирует устойчивость pointcut. Красота здесь заключается в способности перехвата точек соединения без дополнительных работ вне требуемой RMI-инфраструктуры.

Перехват точек соединения с использованием метаданных

Для реализации определенных схем пересечения нельзя использовать pointcut, основанные на сигнатурах. Например, как бы вы реализовали перехват точек соединения для управления транзакциями или авторизации? В отличие от примера с RMI в названии или сигнатуре элементов нет ничего связанного с транзакциями или характеристиками авторизации. Требуемые в таких ситуациях pointcut могут стать громоздкими, что вы увидите в следующем примере. (Пример реализован в AspectJ, но pointcut в других системах концептуально идентичны.)

pointcut transactedOps()
    : execution(public void Account.credit(..))
      || execution(public void Account.debit(..))
      || execution(public void Customer.setAddress(..)) 
      || execution(public void Customer.addAccount(..))
      || execution(public void Customer.removeAccount(..));

В подобных ситуациях для перехвата необходимых точек соединения предлагается использовать метаданные. Например, вы могли бы написать показанный ниже pointcut для перехвата всех методов, использующих аннотацию @Transactional.

pointcut execution(@Transactional * *.*(..));

Метаданные и модульность

И хотя приведенный выше пример использования метаданных для перехвата точек соединения может показаться слишком очевидным, важно понимать последствия такого использования, особенно когда дело касается модульности. Как только вы начнете использовать метаданные в своих pointcut, методы должны содержать соответствующую аннотацию для работы в пересекающейся реализации использующих их аспектов, что показано ниже:

public class Account {
    ...

    @Transactional(kind=Required)
    public void credit(float amount)  {
        ...
    }

    @Transactional(kind=Required)
    public void debit(float amount)  {
        ...
    }
	
    public float getBalance() {
        ...
    }

    ...

}

Аналогичным образом методы addAccount(), removeItem() и setAddress() класса Customer должны теперь содержать аннотацию @Transactional.

Большинство практиков АОП в настоящее время реализуют задачи управления транзакциями и авторизации, используя существующую функциональность АОП обычно при помощи шаблонов проектирования с наследованием аспектов. Однако, как вы увидите в этой статье, добавление метаданных в АОП-системы может значительно их улучшить. Более подробно о влиянии добавления метаданных на модульность АОП-систем, а также о сценариях, где использование метаданных наиболее эффективно, я поговорю в Части 2 этой статьи. В следующем разделе я более конкретно объясню способы расширения реализаций АОП для поддержки метаданных.


АОП с поддержкой метаданных

АОП-системы и их модели точек соединения могут быть расширены для потребления определяемых в метаданных аннотаций. JBoss AOP, Spring AOP, AspectWerkz и AspectJ обеспечивают или планируют обеспечить механизмы поддержки метаданных. JBoss AOP и AspectWerkz поддерживают метаданные в текущей версии. Spring AOP осуществляет такую поддержку, позволяя писать pointcut программным способом, реализуя интерфейс org.springframework.aop.Pointcut. Следующая версия AspectJ будет поддерживать метаданные после модификации языка AspectJ.

В предыдущем разделе я рассмотрел основы того, как АОП-системы могут потреблять метаданные, используя пример выбора методов с аннотацией @Transactional. В этом разделе и до конца статьи я остановлюсь на специфике комбинирования АОП и метаданных.

Хотя примеры в этой статье основаны на реализациях АОП, поддерживающих метаданные, существует возможность (как побочный эффект поддержки генерирования кода) потреблять метаданные даже в базовой АОП-системе, не поддерживающей их. Например, Barter - это программа с открытым исходным кодом, которая использует аннотации и предварительное генерирование кода для реализации DBC-контрактов в старых версиях AspectJ, которые не поддерживали перехват точек соединения, оcнованных на тегах Javadoc. В настоящее время Contract4J выполняет аналогичную задачу, используя стиль определяемых в метаданных аннотаций Java. Ссылки на более подробную информацию по этим программам можно найти в разделе "Ресурсы".


Поддержка метаданных в АОП-системах

Для поддержки основанного на метаданных пересечения АОП-система должна обеспечивать способ потребления и предоставления аннотаций. Сейчас я объясню основы обоих типов поддержки. В следующем разделе я более детально остановлюсь на специфике каждого из них.

Поддержка потребления аннотаций

АОП-системы, поддерживающие потребление аннотации, дают возможность выбирать точки соединения в зависимости от ассоциированных с программными элементами аннотаций. Современные АОП-системы, предлагающие такую поддержку, расширяют определение различных шаблонов сигнатур для разрешения указания типов аннотаций и свойств. Например, pointcut мог бы выбирать все методы, содержащие аннотации типа Timing. Затем, он мог бы выбрать из них только те методы, свойство value в которых превышает, допустим, 25. Для реализации advice, зависящего как от типа аннотаций, так и от свойств, система могла бы включать синтаксис pointcut, перехватывая экземпляры аннотаций, связанные с точками соединений. И, наконец, система могла бы также разрешать advice осуществлять доступ к экземплярам аннотаций через API отражений.

Поддержка предоставления аннотаций

При стандартной поддержке метаданных в Java для каждого аннотируемого программного элемента объявляется один экземпляр аннотации. Однако, объявление одной и той же аннотации для нескольких программных элементов может привести к ненужному загромождению программы. Существует возможность реализовать механизм пересечения в АОП, объявив аннотацию один раз для всех требующихся элементов. Поддерживающие предоставление аннотации АОП-системы дают возможность присоединять аннотации к программным элементам пересекающимся способом. Например, вы могли бы присоединить аннотацию @Secure ко всем методам класса Account простым объявлением без необходимости разбрасывать аннотации по всем этим методам.

Как вы увидите из приведенного ниже детального рассмотрения, не все АОП-системы поддерживают все упомянутые возможности. Я начну с обзора того, как несколько АОП-систем предоставляют поддержку потребления аннотаций.


Потребление аннотаций в АОП

В различных АОП-системах, поддерживающих метаданные, синтаксис pointcut различается. Вы можете увидеть это различие сами, изучив, как каждая система обрабатывает pointcut, перехватывающий все точки соединения на основе типа аннотации; немного ниже я расскажу о некоторых основных факторах, которые могут быть важны при выборе точки соединения.

AspectJ5

Синтаксис AspectJ 5 (промежуточная версия на момент написания статьи) расширяет определение сигнатуры типа, метода и полей для включения определяемых в метаданных аннотаций как части сигнатуры:

pointcut transactedOps(): execution(@Transactional * *.*(..));

Если бы вы использовали стиль определения @AspectJ в AspectJ 5, тот же самый pointcut выглядел бы так:

@Pointcut("execution(@Transactional * *.*(..))")
void transactedOps();

AspectWerkz

Как и в большинстве других АОП-средств, синтаксис pointcut в AspectWerkz очень напоминает AspectJ. Объявление pointcut в следующем фрагменте использует аннотацию в стиле метаданных, хотя XML-стиль будет иметь аналогичное выражение pointcut:

@Expression("execution(@Transactional * *.*(..))")
Pointcut transactedOps;

Обратите внимание, что AspectWerkz использует метаданные для расширения языка программирования Java с целью поддержки АОП, как показано в примере. Следовательно, AspectWerkz использует метаданные в двух целях: для расширения семантики программных элементов и для реализации пересечения на основе метаданных. В вышеприведенном примере я использовал второй вариант.

JBossAOP

JBossAOP концептуально сильно не отличается от других АОП-систем, хотя и использует другой синтаксис. Показанный здесь pointcut аналогичен остальным примерам, но использует синтаксис JBoss XML:

<pointcut name="transactedOps" expr="* *->@Transactional(..)"/>

Как вы видите, больших концептуальных различий между способом перехвата точек соединения на основе присоединенных к ним определяемых в метаданных аннотаций в различных АОП-системах нет.

Выбор при помощи свойств аннотаций

Тип - это не единственный атрибут при выборе точек соединения: могут также применяться свойства. Например, cледующий pointcut перехватывает все методы с аннотацией @Transactional с установленным свойством value в RequiredNew:

execution(@Transactional(value==RequiredNew) *.*(..))

На момент написания этой статьи не существует АОП-систем, поддерживающих pointcut, основанных на свойствах аннотации. Вместо этого каждая система использует динамическое решение внутри advice для проверки свойств и применения соответствующей логики (или в крайнем случае использует динамические проверки в if() pointcut). Есть определенные преимущества pointcut, основанных на свойствах аннотации, особенно для проверок во время компиляции и для эффективности статического выбора. Будущие версии АОП-систем будут поддерживать этот тип pointcut.

Экспонирование экземпляров аннотаций

Поскольку логика advice может зависеть как от типа определяемых в метаданных аннотаций, так и от свойств экземпляра, каждый экземпляр аннотации должен быть экспонирован в виде контекста, так же как и другой контекст точки соединения (например, объект, аргументы метода и др.). AspectJ 5 расширяет существующие pointcut и добавляет новые для экспонирования аннотаций. Например, следующий pointcut собирает экземпляр аннотаций типа Transactional, связанный с перехватываемой точкой соединения:

pointcut transactedOps(Transactional tx)
    : execution(@Transactional * *.*(..)) && @annotation(tx);

После перехвата экземпляры аннотаций могут использоваться таким же способом, как и любой другой контекст. Например, в следующем advice осуществляется запрос перехваченной аннотации о ее свойствах:

Object around(Transactional tx) : transactedOps(tx) { if(tx.value() == Required) { ... implement the required transaction behavior } else if(tx.value() == RequiredNew) { ... implement the required-new transaction behavior } ... }

Большинство АОП-систем используют только API отражения для экспонирования экземпляров аннотации перехваченной точки соединения и не позволят связаться с аннотацией как контекстом точки соединения. В этих случаях вы можете запросить объект, представляющий желаемую точку соединения, о связанных с ним аннотациях. AspectJ предлагает как доступ через отражение, так и традиционный контекст точки соединения. Более подробно о поддержке экспонирования экземпляров аннотаций в AspectJ можно узнать из источников, ссылки на которые приведены в разделе "Ресурсы".


Предоставление аннотаций в АОП

Основной идеей предоставления аннотаций при помощи АОП-конструкций является попытка избежать загромождения определений программных элементов аннотациями. Концептуально такая конструкция предоставляет возможность присоединить аннотации к программным элементам способом пересечения. На первый взгляд предоставление аннотаций при помощи АОП-конструкций и дальнейшее их использование для перехвата точек соединения кажется необязательным и замысловатым. В конце концов, если вы можете определить необходимость наличия аннотаций в точках соединения, вы можете написать pointcut и информировать эти точки соединения напрямую. Однако предоставление аннотаций способом пересечения может быть очень полезным. Прежде всего такое объявление может выступать как проводник взаимодействия с не АОП-клиентами. Кроме того, предоставление аннотаций способом пересечения дает возможность спроектировать более слабо связанную систему, одновременно избегая загромождения аннотациями.

В конце этой статьи я объясню некоторые из возможностей проектирования при использовании АОП-конструкций для предоставления аннотаций. А сейчас я расскажу об основном синтаксисе для предоставления аннотаций способом пересечения.

Синтаксис предоставления аннотаций

Предложенный в AspectJ синтаксис расширяет текущие статические конструкции пересечения для создания нового синтаксиса объявления аннотаций. В следующем фрагменте кода аннотация с типом Authenticated присоединяется к свойству permission, установленному в banking:

declare annotation : * Account.*(..)
                   : @Authenticated(permission=<banking>);

Pointcut @AspectJ также предоставляет аналогичную функциональность через использование @DeclareAnnotation, при котором такое же объявление можно записать следующим образом:

@DeclareAnnotation(<* Account.*(..)>)
@Authenticated(permission=<banking>)
void bankAccountMethods();

В JBoss AOP, при использовании аспектов в XML-стиле, аннотации присоединяются при помощи элемента annotation-introduction. Атрибут invisible указывает, должна ли быть сохранена аннотация во время исполнения (эквивалент свойства RetentionPolicy.SOURCE в стандартных метаданных языка Java).

<annotation-introduction expr="method(* Account->*(..))"
                         invisible="false">
      @Authenticated(permission=<banking>)
</annotation-introduction>

Как видите, принципы предоставления аннотаций являются одинаковыми в различных АОП-системах, хотя синтаксис отличается.


Дизайн АОП с метаданными

Как вы заметили в предыдущих разделах, скомбинировать метаданные и АОП довольно просто. Важно знать, когда можно применить основанное на метаданных пересечение, а когда нет. В этом разделе я начну отвечать на этот вопрос, рассматривая, как система может развиться от использования в ней АОП-реализации с неявными свойствами точек соединения до использования pointcut, основанных на метаданных. Во второй части я глубже проанализирую подход с использованием метаданных.

Представленная в этом разделе дискуссия должна быть полезна по двум причинам: во-первых, важно понимать, что использование метаданных не всегда первый и единственный выбор в практической работе с АОП. Во-вторых, реализованный здесь пример послужит инструкцией по развитию дизайна, если вы решите использовать основанное на метаданных пересечение.

Примером в этом упражнении является программа управления транзакциями. Хотя я использовал AspectJ для разработки всех кодов примера, реализации в других АОП-системах концептуально идентичны. Рассматривайте каждый шаг в этом упражнении как рефакторинг оригинального дизайна. Нашей целью является постепенное разъединение (decouple) системы и улучшение ее модульности.


Версия 1: Простейший аспект

В моей первой попытке повышения модульности пересекающихся процессов я использовал специализированный аспект, содержащий определения pointcut, а также advice к этому pointcut. Это очень простая схема и очень часто первая, с которой вы сталкиваетесь при изучении АОП. На рисунке 1 показана схема проекта, реализующего один аспект:

Рисунок 1. Первая попытка реализации управления транзакциями с использованием АОП
Первая попытка реализации управления транзакциями с использованием АОП

Листинг 1 реализует приведенную выше схему:

Листинг 1: Аспект управления транзакциями для банковской системы
public aspect BankingTxMgmt {
    pointcut transactedOps()
        : execution(void Customer.setAddress(..))
          || execution(void Customer.addAccount(..))
          || execution(void Customer.removeAccount(..))
          || execution(void Account.credit(..))
          || execution(void Account.debit(..));
          
    Object around() : transactedOps() {
         try {
             beginTransaction();
             Object result = proceed();
             endTransaction();
             return result;
         } catch (Exception ex) {
             rollbackTransaction();
             return null;
         }
    }
    
    ... implementation of beginTransaction() etc.

}

Эта схема работает хорошо с аспектами, требующими очень незначительных знаний об используемой системе. Например, если бы вы захотели разрешить использование пула, вы могли бы написать общий аспект, формирующий вызовы для создания и уничтожения используемых в пуле ресурсов. Для функциональности пересечения, при котором нельзя перехватить требуемые точки соединения общим способом, такой подход был бы ограничивающим. Во-первых, аспект не является повторно используемым, поскольку определение pointcut зависит от системы. Во-вторых, изменения системы могли бы привести к необходимости изменения аспекта. Другими словами, эта первая версия системы оставляет нас с типом связи "многие к одному" между программными элементами и pointcut. Поскольку это не лучший вариант, я продолжаю.


Версия 2: Повторно используемый аспект

В моем втором примере я улучшу аспект, сделав его повторно-используемым. Я выделил повторно-используемый фрагмент аспекта и добавил субаспект, определяющий pointcut зависящим от системы способом. На рисунке 2 показана структура после извлечения базового аспекта:

Рисунок 2. Выделение повторно-используемого аспекта в управлении транзакциями
Выделение повторно-используемого аспекта в управлении транзакциями

В листинге 2 приведен базовый аспект, который уже является повторно-используемым. Вы заметите два изменения по сравнению с листингом 1: аспект указан как abstract, и pointcut transactedOps() указан как abstract с удаленными определениями:

Листинг 2. Повторно-используемый базовый аспект управления транзакциями
public abstract aspect TxMgmt {
    public abstract pointcut transactedOps();
          
    Object around() : transactedOps() {
         try {
             beginTransaction();
             Object result = proceed();
             commitTransaction();
             return result;
         } catch (Exception ex) {
             rollbackTransaction();
             return null;
         }
    }
   
    ... implementation of beginTransaction() etc.

}

Теперь мне необходимо написать субаспект для моего базового аспекта. Приведенный ниже субаспект определяет pointcut, который перехватывает точки соединения, необходимые для поддержки управления транзакциями. В листинге 3 приведен зависящий от системы субаспект, расширяющий аспект TxMgmt в листинге 2. Субаспект определяет pointcut transactedOps() с таким же определением, что и в листинге 1.

Листинг 3. Зависящий от системы субаспект
public aspect BankingTxMgmt extends TxMgmt {
    public pointcut transactedOps() 
        : execution(void Customer.setAddress(..))
          || execution(void Customer.addAccount(..))
          || execution(void Customer.removeAccount(..))
          || execution(void Account.credit(..))
          || execution(void Account.debit(..));
}

Хотя это и улучшение, этот дизайн все еще оставляет нас с зависимостью "многие к одному" (N-to-1) между субаспектами и классами. Любые изменения функциональности транзакций для банковской системы потребует модификации определения pointcut BankingTxMgmt. Поскольку мы пока далеки от идеала, продолжим.


Версия 3: Шаблон Participant

Выше я решил проблему повторного использования, но я все еще нуждаюсь в избавлении от зависимости "многие к одному". Я могу это сделать при использовании шаблона Participant (см. раздел "Ресурсы"). Вместо использования одного субаспекта для всей системы я могу использовать много субаспектов (один на подсистему), позволяя запись относительно стабильного pointcut. Субсистемой в этом контексте может быть пакет, набор пакетов или даже класс. На рисунке 3 показаны структурные взаимосвязи между различными элементами:

Рисунок 3. Реализация шаблона проектирования participant
Реализация шаблона проектирования participant

В листинге 4 показан класс Customer с субаспектом participant, который отвечает за определение pointcut для встраиваемого класса.

Листинг 4. Класс Customer со встраиваемым аспектом participant
public class Customer {
    public void setAddress(Address addr) {
        ...
    }

    public void addAccount(Account acc) {
        ...
    }

    public void removeAccount(Account acc) {
        ...
    }

    ...

    private static aspect TxMgmtParticipant extends TxMgmt {
        public pointcut transactedOps()
            : execution(void Customer.setAddress(..))
              || execution(void Customer.addAccount(..))
              || execution(void Customer.removeAccount(..));
    }
}

Субаспект в примере класса Customer просто перечисляет все методы с групповыми символами для аргументов. На практике однако вы, возможно, захотите использовать групповые символы для упрощения определения pointcut. Например, вы можете объявить все public-методы класса для перехвата transactedOps() pointcut при помощи следующего определения:

public pointcut transactedOps()
    : execution(public * Customer.*(..));

В листинге 5 вы можете увидеть, как в класс Account встраивается субаспект для участия в системной функциональности управления транзакциями.

Лисинг 5. Класс Account со встроенным аспектом participant
public class Account {
    public void credit(float amount) {
        ...
    }

    public void debit(float amount) {
        ...
    }
    public float getBalance() {
        ...
    }

    ...

    private static aspect TxMgmtParticipant extends TxMgmt {
        public pointcut transactedOps()
            : execution(void Account.credit(..))
              || execution(void Account.debit(..));
    }
}

Как и в классе Customer, добавление еще одного шага упростило бы pointcut. Например, что если бы я решил, что все методы public, кроме метода getBalance(), должны выполняться в контексте управления транзакциями? Я мог бы определить pointcut для перехвата этой реализации следующим образом:

public pointcut transactedOps()
   : execution(public void Account.*(..))
     && !execution(float Account.getBalance());

Теперь при изменении класса я должен изменить только вложенный субаспект в этом классе. Вместо большого N я уменьшил связи системы до небольшого количества программных элементов, перехватываемых каждым субапсектом, скажем - n-to-1. Кроме того, если класс меняется в соответствии с требованиями управления транзакциями, мне нужно только изменить pointcut во встроенном участвующем аспекте, сберегая таким образом локальность.

Этот пример иллюстрирует важный момент, обычно упускаемый в связанных с АОП дискуссиях: если вы пытаетесь найти шаблон сигнатуры для всей системы, вы столкнетесь с неприятными сюрпризами - нестабильными, сложными и некорректными pointcut. Если вы рассматриваете подмножества системы, то часто можете найти шаблон сигнатуры, который довольно хорошо работает для подсистемы. Использование аспекта Participant с одним аспектом на класс рассматривает класс как подсистему, хотя любое логическое деление на подсистемы будет всегда полезным.

Это решение является приемлемым для большинства ситуаций. Его отрицательной стороной является то, что классы обладают прямой зависимостью от базового аспекта, поэтому базовый аспект всегда должен присутствовать в системе. Другим моментом при такой ситуации является то, что его функциональность пересечения не будет применяться до тех пор, пока классы явно не станут "участвовать" во взаимодействиях, встраивая вложенный субаспект. В этом моменте нужно больше обращать внимание на природу функциональности пересекающихся процессов, а не на само решение. И, как вы вскоре увидите, я могу немного это улучшить.


Версия 4: pointcut, основанные на метаданных

На этом этапе я собираюсь изменить каждый метод для поддержки аннотаций и возврата к одному субаспекту в системе (как в версии 2). Но на этот раз мои субаспекты будут использовать pointcut, основанные на метаданных, для перехвата необходимых точек соединения - методов, которые содержат только что предоставленные мною аннотации. Субаспект сам по себе будет способен повторно использоваться в системе. На рисунке 4 показана схема этой версии.

Рисунок 4. Управление транзакциями с использованием метаданных
Управление транзакциями с использованием метаданных

При использовании субаспекта с метаданными при изменении характеристик точки соединения в классе необходимо изменить только аннотацию для этой точки соединения. В листинге 6 приведен субаспект, расширяющий аспект TxMgmt из версии 2 и определяющий pointcut transactedOps(), просто перехватывая все методы, содержащие аннотацию с типом Transactional.

Листинг 6. Субаспект управления транзакциями с использованием метаданных
public aspect MetadataDrivenTxMgmt extends TxMgmt {
    public pointcut transactedOps()
        : execution(@Transactional * *.*(..));
}

Класс должен работать с субаспектом, используя присоединение аннотации с типом Transactional к каждому методу, который должен быть выполнен в контексте управления транзакциями. В листинге 7 приведена реализация класса Customer с методами, содержащими аннотации:

Листинг 7. Класс Customer с аннотациями
public class Customer {
    @Transactional
    public void setAddress(Address addr) {
        ...
    }

    @Transactional
    public void addAccount(Account acc) {
        ...
    }

    @Transactional
    public void removeAccount(Account acc) {
        ...
    }

    ...

}

В листинге 8 приведена аналогичная реализация класса Account:

Листинг 8. Класс Account с аннотациями
public class Account {
    @Transactional
    public void credit(float amount) {
        ...
    }

    @Transactional
    public void debit(float amount) {
        ...
    }

    public float getBalance() {
        ...
    }

    ...

}

На данном этапе я установил связь "один-к-одному" между методами и взаимодействующим аспектом. Я также устранил прямую зависимость между аспектом и классами. В результате, если я захочу изменить базовый аспект, я могу сделать это без каких-либо изменений в системе.

Использование базового аспекта не является обязательным (то есть вы могли бы свернуть иерархию). Однако отделение базового аспекта от субаспекта с метаданными имеет несколько преимуществ. Во-первых, в дочернем аспекте можно выбрать тип аннотации. В одной системе в нем можно использовать Transactional в качестве типа аннотации для перехвата точек соединения, тогда как в другой системе тип аннотации может быть Tx. Во-вторых, он вытесняет выбор между шаблоном Participant и подходом с использованием метаданных в дочерний аспект. В-третьих, такой подход делает возможным наследование транзакционного pointcut из бизнес-аннотаций, таких как @Purchase или @OrderProcessing. И, наконец, он разрешает комбинирование подхода с использованием метаданных и подхода, основанного на шаблоне Participant.

При взаимодействии через аннотации ответственность за совместную работу перемещается в индивидуальные методы (вместо субаспекта participant). Зависимость между MetadataDrivenTxMgmt и классами ограничивается типами аннотации и связанной с ними семантики.

В большинстве случаев данной итерации достаточно. Но существует один особенный сценарий, где я могу провести дальнейший рефакторинг дизайна для получения оптимальных результатов.


Версия 5: Аспекты как поставщики метаданных

В определенных ситуациях большинству методов класса необходимо содержать аннотацию (например, в версии 4). Более того, многие свойства пересечений могут потребовать наличия в каждом методе одной или более аннотаций. Такие ситуации могут привести к объявлению множества аннотаций в каждом методе - этот феномен часто описывают как аннотационный ад (annotation hell). Загромождение аннотациями может быть уменьшено путем комбинирования шаблона Participant с поставляющими аннотации аспектами. Это полезный вариант при наличии ясных способов выражения взаимодействующих точек соединения. В этих ситуациях такой дизайн помогает уменьшить риск пропустить аннотации для точек соединения.

Рисунок 5. Аспект как поставщик метаданных
Аспект как поставщик метаданных

Аспекты-аннотаторы просто используют один или несколько операторов declare annotations: например, declare annotation : <Method pattern> : <Annotation definition>;. В этом примере я использовал взаимодействие с одним аспектом-аннотатором на класс, подобное шаблону Participant. Однако это действие не является внутренним требованием такого дизайна. Вы могли бы, например, реализовать один аспект-аннотатор на пакет. Основной идеей здесь является поиск подходящей подсистемы с ясным шаблоном сигнатуры или динамической контекстной информацией (поток управления и т.д.), которая перехватывала бы требуемые точки соединения и устраняла нагромождение аннотаций. В листинге 9 показан класс Customer с аспектом-аннотатором.

Листинг 9. Класс Customer со встроенным аспектом-аннотатором
public class Customer {
    public void setAddress(Address addr) {
        ...
    }

    public void addAccount(Account acc) {
        ...
    }

    public void removeAccount(Account acc) {
        ...
    }

    ...

    private static aspect Annotator {
        declare annotation: public Customer.*(..): @Transactional;
    }
}

Аналогично класс Account в листинге 10 содержит аспект-аннотатор.

Листинг 10. Класс Account со встроенным аспектом-аннотатором
public class Account {
    public void credit(float amount) {
        ...
    }

    public void debit(float amount) {
        ...
    }

    ...

    private static aspect Annotator {
        declare annotation: public Account.*(..): @Transactional;
    }
}

Теперь сравним эту реализацию с реализацией, использующей шаблон Participant в версии 3. Та версия страдает от одного главного недостатка: в ней класс привязывается к конкретным аспектам. В некотором смысле то было очень активное взаимодействие - базовый аспект должен был существовать всегда (поскольку он является базовым для всех аспектов). При использовании подхода с аспектом-аннотатором взаимодействие возникает только на уровне общего понимания типа аннотации.

Соединение типов аннотаций

Одним из вариантов применения этой техники является использование аспекта-аннотатора для соединения бизнес-аннотаций и аннотаций, применяемых для реализации аспектов. Например, если вы знаете, что все методы с аннотациями @Purchase и @OrderProcessing должны работать с транзакциями, вы могли бы написать аспект, показанный в листинге 11.

Листинг 11. Трансляция бизнес-аннотаций в аннотации пересечения
public aspect BusinessTransactionBridge {
    declare annotation: @Purchase *.*(..): @Transactional;
    declare annotation: @OrderProcessing  *.*(..): @Transactional;
}

Этот аспект присоединяет аннотацию @Transactional ко всем методам с аннотациями @Purchase или @OrderProcessing. Этот подход, в комбинации с аспектом из листингов 2 и 6, ведет к окружению выполнения методов логикой управления транзакциями.


Заключение

Метаданные - это способ выражения дополнительной информации о программных элементах. Новые возможности метаданных языка программирования Java разрешают использование типизированных аннотаций. Использовать метаданные довольно просто, хотя существует множество вариантов их потребления. Аспектно-ориентированное программирование является одним из основных потребителей метаданных. Дополнение модели точек соединения метаданными дает возможность применения в АОП более простых pointcut для пересекающихся процессов, которые трудно было бы указать при использовании pointcut со статической сигнатурой.

В первой части этой статьи я привел общий обзор концепции метаданных и объяснил, как АОП может использовать информацию из метаданных программных элементов. Я также кратко описал механизмы различных АОП-систем, участвующие в поддержке основанных на метаданных pointcut, и привел пример рефакторинга дизайна из пяти частей для демонстрации применения метаданных в АОП-системах.

Во второй половине статьи я более подробно остановлюсь на анализе дизайна при определении и использовании метаданных с АОП для потребления и предоставления. Я расскажу о том, как использование метаданных влияет на принцип рассеянности в АОП-системах, а также как оно могло бы повлиять на освоение АОП-систем. Я также представлю вам новый способ подхода к АОП как сигнатуре в многомерном деловом пространстве, концепцию, являющуюся полезной для ежедневной работы с АОП, а также для проектирования аннотационных типов в не АОП целях.

Благодарности

Я хочу поблагодарить Рона Бодкина (Ron Bodkin), Веса Исберга (Wes Isberg), Мика Керстена (Mik Kersten), Николаса Лесицки (Nicholas Lesiecki) и Рика Уоррена (Rick Warren) за рецензию на эту статью.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=96659
ArticleTitle=AOP@Work: АОП и метаданные: Совершенное соответствие, часть 1
publish-date=03082005