aop spring что это
Знакомство с АОП
Парадигмы программирования
В современном мире IT-разработки существует довольно большое множество различных подходов к написанию программ. Так, например, кому-то нравиться представлять программу в виде последовательности действий, а кто-то считает, что программа должна представлять собой множество объектов, общающихся друг с другом. Совокупности этих идей и понятий образуют своего рода стиль написания программы, который принято назвать – парадигма программирования.
В этой статье я хочу рассказать о сравнительно молодой, но крайне, на мой взгляд, полезной парадигме программирования – аспектно-ориентированном программировании.
Основы АОП
public BookDTO getBook(Integer bookId) <
BookDTO book = bookDAO.readBook(bookId);
return book;
>
public BookDTO getBook(Integer bookId) <
LOG.debug( «Call method getBook with id » + bookId);
BookDTO book = bookDAO.readBook(bookId);
LOG.debug( «Book info is: » + book.toString());
return book;
>
public BookDTO getBook(Integer bookId) throws ServiceException <
LOG.debug( «Call method getBook with id » + bookId);
BookDTO book = null ;
try <
book = bookDAO.readBook(bookId);
> catch(SQLException e) <
throw new ServiceException(e);
>
LOG.debug( «Book info is: » + book.toString());
return book;
>
public BookDTO getBook(Integer bookId) throws ServiceException, AuthException <
if (!SecurityContext.getUser().hasRight(«GetBook»))
throw new AuthException(«Permission Denied»);
LOG.debug( «Call method getBook with id » + bookId);
BookDTO book = null ;
try <
book = bookDAO.readBook(bookId);
> catch (SQLException e) <
throw new ServiceException(e);
>
LOG.debug( «Book info is: » + book.toString());
return book;
>
public BookDTO getBook(Integer bookId) throws ServiceException, AuthException <
if (!SecurityContext.getUser().hasRight( «GetBook» ))
throw new AuthException( «Permission Denied» );
LOG.debug( «Call method getBook with id » + bookId);
BookDTO book = null ;
String cacheKey = «getBook:» + bookId;
try <
if (cache.contains(cacheKey)) <
book = (BookDTO) cache.get(cacheKey);
> else <
book = bookDAO.readBook(bookId);
cache.put(cacheKey, book);
>
> catch (SQLException e) <
throw new ServiceException(e);
>
LOG.debug( «Book info is: » + book.toString());
return book;
>
Можно продолжать совершенствовать данный метод, но для начала — достаточно. В ходе наших доработок мы получили метод в 10 раз (с 2 до 20 LOC) превышающий исходный размер. Самое интересное, что объём бизнес-логики в нём не изменился – это всё та же 1 строка. Остальной код реализует некоторую общую служебную функциональность приложения: логирование, обработку ошибок, проверку прав доступа, кеширование и так далее.
Пример использования (AspectJ)
AspectJ является аспектно-ориентированным расширением/framework’ом для языка Java. На данный момент это, пожалуй, самый популярный и развивающийся АОП движок.
Рассмотрим реализацию аспекта логирования с его помощью:
@Aspect
public class WebServiceLogger <
private final static Logger LOG =
Logger.getLogger(WebServiceLogger. class );
@Pointcut( «execution(* example.WebService.*(..))» )
public void webServiceMethod()
@Pointcut( «@annotation(example.Loggable)» )
public void loggableMethod()
LOG.debug( «Call method » + methodName + » with args » + methodArgs);
Object result = thisJoinPoint.proceed();
LOG.debug( «Method » + methodName + » returns » + result);
Первым делом создаётся аспект логирования методов сервисов – класс WebServiceLogger, помеченный аннотацией Aspect. Далее определяются два среза точек соединения: webServiceMethod (вызов метода, принадлежащего классу WebService) и loggableMethod (вызов метода, помеченного аннотацией @Loggable). В завершении объявляется совет (метод logWebServiceCall), который выполняется вместо (аннотация Around) точек соединения, удовлетворяющих срезу («webServiceMethod() && loggableMethod()»).
В коде совета происходит получение информации о текущем методе (точке соединения), логирование начала выполнения метода, непосредственный вызов запрошенного метода, логирование и возвращение результата работы.
Для того, что бы использовать аспекты AspectJ их придётся скомпилировать и «вшить» в основные классы с помощью специального компилятора AJC.
Продукт бесплатный. Распространяется под Eclipse License.
Пример использования (PostSharp)
Рассмотрим, как с помощью него описать аспект обработки исключений. Первым делом необходимо создать класс, расширяющий соответствующий аспект:
public class ExceptionDialogAttribute : OnExceptionAspect
<
public override void OnException(MethodExecutionEventArgs eventArgs)
<
string message = eventArgs.Exception.Message;
Window window = Window.GetWindow((DependencyObject)eventArgs.Instance);
MessageBox.Show(window, message, «Exception» );
eventArgs.FlowBehavior = FlowBehavior.Continue;
>
>
Строго говоря, аспекты в терминологии PostSharp – это, как мы можем видеть, аспект и совет в терминологии АОП.
Для того, что бы указать срез точек пересечения для данного аспекта необходимо в файл настроек сборки (AssemblyInfo.cs) добавить следующую строку:
Или же явно пометить интересующие вас методы атрибутом ExceptionDialog:
[ExceptionDialog]
public BookDTO GetBook(Integer bookId)
Вот собственно и всё: теперь все выброшенные в соответствующих методах исключения будут обрабатываться созданным аспектом.
Продукт платный. Есть Community Edition.
От теории к практике
И так, мы только что увидели, как красиво и эффективно можно решить проблему «выноса за скобки» сквозного функционала в вашем приложении. Однако, это всё теория. На практике всё, естественно, немного иначе 🙂
Прежде всего, в обоих случаях для компиляции и «вшивания» (weaving) аспектов придётся использовать специальный компилятор и тащить вместе с проектом дополнительные библиотеки. Вроде бы, это не проблема: компилятор легко скачивается и интегрируется в среду (например, при использовании maven’a задача сведётся всего лишь к добавлению плагина aspectj-maven-plugin), а множество зависимостей – обычное дело, по крайней мере для Java-приложений (решаемая с помощью того же maven’a). Однако, необходимость включения в проект чего-то, что требует отдельной компиляции, да ещё и не имеет широкого распространения, зачастую отпугивает разработчиков, не смотря на все потенциальные плюсы.
В данном случае решением проблемы может стать Spring Framework [1,2]. Данный фреймворк имеет много достоинств, однако в рамках данной статьи нас интересует его AOP-составляющая. Spring Framework реализует ограниченную AOP-функциональность на чистом Java (C#) без использования сторонних библиотек с помощью создания прокси-объектов (JDK Dynamic Proxy, CGLIB). Другими словами в Spring AOP можно использовать только точки соединения типа «выполнение метода». Однако, как показывает практика, данное ограничение не играет значительной роли, так как для решения большинства задач, требуется точки соединения именно этого типа.
Кроме того, Spring Framework поддерживает конфигурирование приложений c помощью @AspectJ аннотаций, а так же интеграцию аспектов скомпилированных непосредственно с помощью AspectJ.
У себя в компании мы используем именно Spring AOP. Учитывая прочие заслуги Spring Framework, на мой взгляд, он является самой доступной и удобной площадкой для работы с AOP, внося значительный вклад в его популяризацию и развитие.
Резюме
ФИНЭКОСОФТ
Аспектно-Ориентированное Программирование в Spring
Введение
Аспектно-ориентированное программирование, сокращенно АОП (Aspect Oriented Programming или AOP) вместе с принципом конфигурирования зависимостей (dependency injection-DI) и абстракции сервисов (Enterprise Service Abstraction) являются основными принципами, на которых построен главный продукт компании SpringSource – Spring Framework.
В основе аспектно-ориентированного программирования лежит понятие crosscutting concerns, которое не имеет пока устоявшегося эквивалента в русском языке. Наиболее близким по смыслу считается и чаще всего используется словосочетание «сквозная функциональность», которому и будет следовать данная статья в дальнейшем.
В основе аспектно-ориентированного программирования лежит понятие crosscutting concerns, которое не имеет пока устоявшегося эквивалента в русском языке. Наиболее близким по смыслу считается и чаще всего используется словосочетание «сквозная функциональность», которому и будет следовать данная статья в дальнейшем.
Под сквозной функциональностью понимается функциональность, реализовать которую в отдельном компоненте языка программирования традиционными средствами процедурного или объектно-ориентированного программирования или очень сложно, или вообще невозможно, поскольку эта функциональность необходима в большей части модулей системы. Кроме того, эта функциональность не относятся напрямую к предметной области. Примером такой функциональности является протоколирование работы системы (logging). Если надо регистрировать время начала и окончания выполнения методов некоторых классов не используя аспектно-ориентированного программирования, то в исходном коде каждого метода 2 раза явно используется функция записи в журнал. Представив себе прикладную систему даже средней сложности, можно легко понять то огромное количество необходимой работы, которую необходимо сделать, в случае изменения интерфейса протоколирования.
Использование аспектно-ориентированного программирования помогает следовать принципу разделения ответственности (separation of concerns), что положительно сказывается на многих характеристиках разрабатываемой информационной системы, некоторыми из которых являются следующие:
Основные понятия АОП:
Существует так называемый Альянс АОП (AOP Alliance), который объединяет усилия многих коммерческих компаний и проектов (в том числе и SpringeSource) по разработке стандартных интерфейсов для различных реализаций АОП. Альянс является довольно консервативной организацией и в настоящее время определяет весьма ограниченный набор АОП функциональности, часто не являющейся достаточной для разработчиков. Но когда это возможно создатели Spring Framework для обеспечения переносимости предоставляют разработчикам возможность использовать стандартные интерфейсы, а не определяют аналогичные собственные.
Различные типы Аспектно-Ориентированного Программирования
Существует два различных способа реализации аспектно-ориентированного программирования: статический и динамический. Эти способы различаются моментами времени, когда происходит связывание (weaving) и способом, как это связывание происходит.
Статическое АОП
При статической реализации аспектно-ориентированного программирования связывание является отдельным шагом в процессе построения программного продукта (build process) путем модификации байт-кода (bytecode) классов, изменяя и дополняя его необходимым образом.
Полученный в результате такого подхода код является более производительным, чем при использовании динамического АОП, так как во время исполнения (runtime) нет необходимости отслеживать момента, когда надо выполнить ту или иную сквозную функциональность, представленную в виде совета (aspect).
Недостатком такого подхода реализации аспектно-ориентированного программирования является необходимость перекомпилирования приложения даже в том случае, когда надо только добавить новый срез (pointcut).
Динамическое АОП
Продукты, реализующие динамический вариант АОП отличается от статического тем, что процесс связывания (weaving) происходит динамически в момент исполнения. В Spring Framework используется именно такой способ связывания и это реализовано с помощью использования специальных объектов-посредников (proxy) для объектов, к которым должны быть применены советы (advice). Недостатки статического подхода АОП являются достоинствами динамического: поскольку связывание происходит динамически, то нет необходимости перекомпилировать приложение для изменения аспектов. Однако эта гибкость достигается ценой небольшой потери производительности.
Архитектура Spring АОП
Как уже было отмечено, реализация аспектно-ориентированного программирования в Spring основана на использовании объектов-посредников (proxy). Создание посредников возможно программным образом, используя класс ProxyFactory, однако на практике чаще все используется декларативный способ создания посредников, основанный на ProxyFactoryBean.
Суть посредников можно объяснить на следующем примере. Предположим обычный вариант взаимодействия двух объектов, когда объект класса Caller вызывает метод operationA объекта класса Callee:
public class Caller <
private Callee callee ;
public void someMethod () <
Если ссылка callee указывает на объект класса Callee (например получена в результате вызова new Callee() ), то вызов происходит напрямую, так как это продемонстрировано на диаграмме последовательностей (sequence diagram), показанной на рисунке 1.
Рисунок 1: Прямой вызов метода без участия объекта посредника
Иная ситуация в случае использования объекта-посредника. Посредник имеет тот же интерфейс, что и исходный класс, но он не вызывает сразу же метод объекта, реализующий класс Callee, есть возможность совершить дополнительные действия как до момента вызова (BeforeAdvice), так и после него (AfterAdvice), как это показано на диаграмме, изображенной рисунке 2.
Рисунок 2: Вызов с применением объекта посредника
Аспектами в Spring АОП являются объекты классов реализующих интерфейс Advisor, причем в самом фреймворке уже существуют некоторые реализации, которые можно использовать в приложениях, таким образом избавляя разработчиков от необходимости самостоятельно создавать требуемую функциональность.
Для советов (advice) предусмотрен базовый интерфейс org.aopalliance.aop.Advice, однако он является достаточно общим и не всегда удобным для применения. Поэтому при создании классов, реализующих сквозную функциональность, используются другие интерфейсы, определенные в Spring Framework, описанные в следующей таблице:
Название совета
Интерфейс
Этот тип совета предоставляет возможность выполнить дополнительные действия перед вызовом метода, определенного для точки соединения. Класс, реализующий before advice, имеет доступ как к целевому объекту (target), так и к аргументам метода точки соединения, однако с помощью советов данного типа невозможно отказаться от выполнения метода.
AfterReturningAdvice выполняется после завершения метода, определенного для точки соединения. Этот совет доступ к целевому объекту (target), к аргументам метода точки соединения и к возвращаемому методом объекту.
Advice, реализующий определенный Альянсом АОП интерфейс org.aopalliance.intercept.MethodInterceptor, выполняется вместо целевого метода, а сам целевой метод передается аcпекту в качестве параметера, чтобы вызвать его при необходимости в соответствующий момент. Используя Advice возможно вообще проигнорировать вызов целевой функции
ThrowsAdvice перехватывает исключения, сгенерированные внутри метода, для которого определена точка соединения
Специальный тип совета, используя который возможно который добавить новую функциональность к исходному классу.
Использование Spring AOP программным образом
Использование ProxyFactory для создания целевого объекта
Создание объекта-посредника программным образом будет показано на примере создания аспекта, реализующего интерфейс AroundAdvice, который представляет собой реализацию простейшего профайлера, который можно использовать для того, чтобы узнать, сколько времени выполняется код метода. С его помощью в тестовой эксплуатации можно использовать вариант с профайлером, оптимизируя по мере надобности код, а в промышленной эксплуатации без него. При этом основная бизнес функциональность код самого метода остается одинаковой в обоих случаях.
В этом примере целевой класс, который будет дополнен советом (advice), состоит из одного единственного метода., логика которого проста: метод «засыпает» на время от 0 до 10 секунд, этот период каждый раз генерируется случайным образом. Исходный код класса выглядит следующим образом:
public class ContainingLongRunningMethodClass <
public void longLoop() <
int delay = ( int ) (Math.random() * 10);
System.out.println(» Delay time : » + delay);
> catch (InterruptedException e) <
Реализация совета (advice) состоит из выдачи на консоль информации о текущем времени, вызовом целевого метода, для которого определена точка соединения, и снова выдачи текущего времени. Исходный код совета имеет следующий вид:
public class DisplayTimeIntercepter implements MethodInterceptor <
public Object invoke(MethodInvocation method) throws Throwable <
Object value = method.proceed();
return value;
Остается продемонстрировать как создается аспект программным способом с помощью класса ProxyFactory. Сначала (1) инициализируется экземпляр целевого класса ContainingLongRunningMethodClass, длительность выполнения методов которого требуется узнать. Затем создается экземпляр ProxyFactory (2), в который потом передаются уже созданный ранее целевой объект (3) и экземпляр advice (4). Теперь можно получить объект-посредник proxy, который имеет тот же интерфейс, что и исходный целевой объект, однако вызовы методов будут «дополнены» выводом информации о времени начала и окончания их работы (5):
public class AroundAdviceProgrammedExample <
public static void main(String[] args) <
new ContainingLongRunningMethodClass(); (1)
ProxyFactory pf = new ProxyFactory(); (2)
pf.addAdvice( new DisplayTimeIntercepter()); (4)
ContainingLongRunningMethodClass proxy = (5)
В результате выполнения этой программы на консоли появится примерно следующий:
Time before method launch: Mon Oct 19 18:18:36 CEST 2010
Time after method launch: Mon Oct 19 18:18:43 CEST 2010
Использование Pointcut при создании целевого объекта
Создание объекта-посредника программным образом, который был продемонстрирован в предыдущей части, неудобен тем, что в результате сквозная функциональность совета (advice) будет применяться для вызова любого метода целевого объекта. Это не всегда является желательным, необходимо дополнить только какие-то определенные методы исходного объекта, или, выражаясь в терминах АОП, из всех возможных точек соединения (joinpoints) выбрать необходимый срез (pointcut).
Для создания среза в Spring необходимо создать класс, реализующий интерфейс Pointcut, в котором определены два метода:
public interface Pointcut <
Метод getClassFilter возвращает класс, реализующий интерфейс ClassFilter, содержащий единственный метод:
public interface ClassFilter <
boolean matches(Class clazz);
Метод matches возвращает истину (true), если в качестве параметра передан класс, для которого необходимо выполнить дополнительную функциональность и ложь (false) если нет.
Класс, реализующий интерфейс MethodMather, экземпляр которого возвращает метод getMethodMatcher, имеет несколько более сложный вид:
public interface MethodMather <
boolean matches(Method m, Class targetClass);
boolean matches(Method m, Class targetClass, Object[] args);
Spring поддерживает 2 типа MethodMather: статический и динамический. В зависимости от возвращаемого значения функции isRuntime окружение Spring считает, что MethodMather является динамическим (возвращается true), или статическим (false).
В случае статического Pointcut окружение Spring для принятия решения, нужно ли вызывать код совета (advice), использует метод matches(Method m, Class targetClass) при вызове каждого метода целевого объекта в первый раз. После вызова метода matches возвращаемое значение сохраняется во внутренней кэш-памяти и оно используется впоследствии для принятия решения, такой подход позволяет увеличить производительность системы за счет минимизации количества вызовов метода matches.
В случае динамического Pointcut также используется этот метод matches(Method m, Class targetClass) для определения необходимости использования сквозной функциональности. Однако проверка на этом не заканчивается, после нее в Spring вызывает метод matches(Method m, Class targetClass, Object[] args). Таким образом, динамический MethodMather определяет применимость данной точка соединения (Pointcut) при каждом конкретном вызове (проверяя например значения аргументов метода), а не только на основании статической информации о классе и названии метода.
Spring Framework включает несколько абстрактных классов, реализующих интерфейс Pointcut, которые являются достаточными для большинства возможных ситуаций, и разработчики редко вынуждены создавать имплементацию этого интерфейса с нуля. В приведенной ниже таблице указаны некоторые, наиболее часто используемые из этих абстрактных классов (o.s.a.s. сокращение от org.springframework.aop.support):
Класс, реализующий Pointcut
Описание
Используется для определения статических точек соединения, является наиболее часто используемым способом определения среза (Pointcut)
Применяется при создании динамических точек соединения, которые используют информацию об аргументах метода во время выполнения.
Используется, когда необходимо одновременно два или более точек соединения (Pointcut) используя операции union или intersection
Позволяет определять точка соединения (Pointcut) используя регулярные выражения JDK1.4
Использует аннотации языка Java (annotation) при определении точек соединения
Применяется, когда для определения точек соединения используются язык выражений (expression language) языка AspectJ.
Рассмотрим пример с использованием DynamicMethodMatcherPointcut для создания динамической точки соединения для класса, исходный код которого выглядит следующим образом:
public class ToBeDecoratedClass <
public void rundomSleep() <
int delay = ( int ) (Math.random() * 10);
> catch (InterruptedException e) <
public void printInteger( int n) <
System.out.println( «method printInteger, n=» + n);
Класс ToBeDecoratedClass содержит два метода, но мы хотим отслеживать вызов только одного из них, а именно printInteger, и только в случае, если значение аргумента n превышает 10.
В этом случае реализация DynamicMethodMatcherPointcut может иметь следующий вид:
public class DynamicPointcut extends DynamicMethodMatcherPointcut <
public boolean matches(Method method, Class clazz) <
return method.getName().equals( «printInteger» );
public boolean matches(Method method, Class clazz, Object[] args) <
if (args. length == 0)
return false ;
if (obj instanceof Integer) <
return (Integer) obj > 10;
else
return false ;
public ClassFilter getClassFilter() <
return new ClassFilter() <
public boolean matches(Class clazz) <
return clazz == ToBeDecoratedClass. class ;
Первый метод matches(Method method, Class clazz) проверяет, является ли исходный объект экземпляром класса ToBeDecoratedClass и вызываемый метод printInteger. Если проверка прошла успешно, то Spring использует второй метод matches(Method method, Class clazz, Object[] args) для того, чтобы определить тип аргумента функции и его значение. Если аргумент является целым числом и его значение больше 10, то будет вызван соответствующий метод класса, реализующий сквозную функциональность. В данном случае на системную консоль просто будут выведено время запуска и окончания работы метода printInteger.
Ниже приведен исходный код класса, содержащего дополнительную функциональность:
public class LogInterceptor implements MethodInterceptor <
public Object invoke(MethodInvocation invocation) throws Throwable <
Object value = invocation.proceed();
return value;
Для демонстрации возможности создания и использования динамического среза также создан класс NotToBeDecoratedClass, интерфейс которого полностью идентичен интерфейсу ToBeDecoratedClass, но вызов метода printInteger которого мы не хотим отслеживать.
public class DynamicPointcutExample <
public static void main(String[] args) <
ToBeDecoratedClass toBeDecoratedClass = new ToBeDecoratedClass();
new NotToBeDecoratedClass();
Pointcut pointcut = new DynamicPointcut();
Advice advice = new LogInterceptor();
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
ProxyFactory proxyFactory = new ProxyFactory();
proxyDecoratedClass = (ToBeDecoratedClass) proxyFactory.getProxy();
proxyFactory = new ProxyFactory();