Аспектно-Ориентированное Программирование в Spring

Введение

Аспектно-ориентированное программирование, сокращенно АОП (Aspect Oriented Programming или AOP) вместе с принципом конфигурирования зависимостей (dependency injection-DI) и абстракции сервисов (Enterprise Service Abstraction) являются основными принципами, на которых построен главный продукт компании SpringSource – Spring Framework.

SpringAOP1

В основе аспектно-ориентированного программирования лежит понятие crosscutting concerns, которое не имеет пока устоявшегося эквивалента в русском языке. Наиболее близким по смыслу считается и чаще всего используется словосочетание «сквозная функциональность», которому и будет следовать данная статья в дальнейшем.

В основе аспектно-ориентированного программирования лежит понятие crosscutting concerns, которое не имеет пока устоявшегося эквивалента в русском языке. Наиболее близким по смыслу считается и чаще всего используется  словосочетание «сквозная функциональность», которому и будет следовать данная статья в дальнейшем.

Под сквозной функциональностью понимается функциональность, реализовать которую в отдельном компоненте языка программирования традиционными средствами процедурного или объектно-ориентированного программирования или очень сложно, или вообще невозможно, поскольку эта функциональность необходима в большей части модулей системы. Кроме того, эта функциональность не относятся напрямую к предметной области. Примером такой функциональности является протоколирование работы системы (logging). Если надо регистрировать время начала и окончания выполнения методов некоторых классов не используя аспектно-ориентированного программирования, то в исходном коде каждого метода 2 раза явно используется функция записи в журнал. Представив себе прикладную систему даже средней сложности, можно легко понять то огромное  количество необходимой работы, которую необходимо сделать, в случае изменения интерфейса протоколирования.

Использование аспектно-ориентированного программирования помогает следовать принципу разделения ответственности (separation of concerns), что положительно сказывается на многих характеристиках разрабатываемой информационной системы, некоторыми из которых являются следующие:

  • Более эффективный процесс создания системы
  • Более качественная реализация
  • Повышенные возможности тестирования     
  • Улучшенная модульность системы

Основные понятия АОП:

  • Точка соединения (joinpoint) — точка в программе, где существует возможность выполнить дополнительный код средствами АОП. Различные реализации АОП имеют различные возможные точки соединения, таковыми могут являться момент вызова методов класса или обращений к полям объекта.
  • Совет (advice) —класс, реализующий сквозную функциональность. Существуют различные типа советов:  выполняемые до точки соединения, после или вместо неё.
  • Срез (pointcut) —точка соединения (joinpoint), которая выбрана для исполнения в ней сквозной функциональности, определенная советом (advice).
  • Аспект (aspect) — под аспектом понимают комбинацию, состоящую из среза (pointcut) и реализующего сквозную функциональность  совета (advice). Аспект изменяет поведение остального кода, исполняя совет в точках соединения, определённых некоторым срезом. В Spring для этого используется также понятие advisor.
  • Внедрение или введение (introduction) — под этим термином понимают процесс модификации объекта путем добавления дополнительных полей и /или методов. Внедрение также может быть использовано для реализации объектом интерфейса без явного указания этого в классе объекта.
  • Связывание (weaving) – связывание аспектов с объектами для создания новых, «расширенных» объектов.
  • Цель или целевой объект (target) – объект, являющийся результатом связывания (weaving), то есть реализующий первоначальную бизнес логику плюс сквозная функциональность, выполненная одним или несколькими аспектами.

Существует так называемый Альянс АОП (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;

// skipped

      public void someMethod() {

// skipped

            callee.operationA();

// skipped

      }

}

Если ссылка callee указывает на объект класса Callee (например получена в результате вызова new Callee() ), то вызов происходит напрямую,  так как это продемонстрировано на диаграмме последовательностей (sequence diagram), показанной на рисунке 1.

SpringAOP2

Рисунок 1: Прямой вызов метода без участия объекта посредника

Иная ситуация в случае использования объекта-посредника. Посредник имеет тот же интерфейс, что и исходный класс, но он не вызывает сразу же метод объекта, реализующий класс Callee, есть возможность совершить дополнительные действия как до момента вызова (BeforeAdvice), так и после него (AfterAdvice), как это показано на диаграмме, изображенной рисунке 2.

SpringAOP3

Рисунок 2: Вызов с применением объекта посредника

Средствами Spring AOP поддерживается только один вид точек соединения (jointpoint)  - вызовы методов классов. Это с одной стороны является существенным упрощением по сравнению с такими тяжеловесными реализациями АОП как AspectJ. С другой стороны именно этот вид точек соединения является достаточным в большинстве случаев и является примером эмпирического правила Парето, которое в применении к данному случаю можно переформулировать примерно так: «20 процентов различных вариантов точек соединения покрывают 80 процентов случаев их использования». Если же действительно существует реальная потребность в других типах точек соединения, то можно одновременно использовать AspectJ и Spring.

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

Для советов (advice) предусмотрен базовый интерфейс org.aopalliance.aop.Advice, однако он является достаточно общим и не всегда удобным для применения. Поэтому при создании классов, реализующих сквозную функциональность, используются другие интерфейсы, определенные в Spring Framework, описанные в следующей таблице:

Название совета

Интерфейс

 

Before

MethodBeforeAdvice

Этот тип совета предоставляет возможность выполнить дополнительные действия перед вызовом метода, определенного для точки соединения. Класс, реализующий before advice, имеет доступ как к целевому объекту (target), так и к аргументам метода точки соединения, однако с помощью советов данного типа невозможно отказаться от выполнения метода.

After returning

AfterReturningAdvice

AfterReturningAdvice выполняется после завершения метода, определенного для точки соединения. Этот совет доступ к целевому объекту (target), к аргументам метода точки соединения и к возвращаемому методом объекту.

Around

MethodInterceptor

Advice, реализующий определенный Альянсом АОП интерфейс org.aopalliance.intercept.MethodInterceptor, выполняется вместо целевого метода, а сам целевой метод передается аcпекту в качестве параметера, чтобы вызвать его при необходимости в соответствующий момент. Используя Advice возможно вообще проигнорировать вызов целевой функции

Throws

ThrowsAdvice

ThrowsAdvice перехватывает исключения, сгенерированные внутри метода, для которого определена точка соединения

Introduction

IntroductionInterceptor

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

Использование Spring AOP программным образом

Использование ProxyFactory для создания целевого объекта

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

В этом примере целевой класс, который будет дополнен советом (advice), состоит из одного единственного метода., логика которого проста: метод «засыпает» на время от 0 до 10 секунд, этот период каждый раз генерируется случайным образом. Исходный код класса выглядит следующим образом:

public class ContainingLongRunningMethodClass {

      public void longLoop() {

            try {

                  int delay = (int) (Math.random() * 10);

                  System.out.println("Delay time : " + delay);

                  Thread.sleep(delay * 1000);

            } catch (InterruptedException e) {

                  e.printStackTrace();

            }

      }

}

Реализация совета (advice) состоит из выдачи на консоль информации о текущем времени, вызовом целевого метода, для которого определена точка соединения, и снова выдачи текущего времени. Исходный код совета имеет следующий вид:

public class DisplayTimeIntercepter implements MethodInterceptor {


      public Object invoke(MethodInvocation method) throws Throwable {

            System.out.println("Time before method launch: " + new Date());

            Object value = method.proceed();

            System.out.println("Time after method launch: " + new Date());

            return value;

      }

}

Остается продемонстрировать как создается аспект программным способом с помощью класса ProxyFactory. Сначала (1) инициализируется экземпляр целевого класса ContainingLongRunningMethodClass, длительность выполнения методов которого требуется узнать. Затем создается экземпляр ProxyFactory (2), в который потом передаются уже созданный ранее целевой объект (3) и экземпляр advice (4). Теперь можно получить объект-посредник proxy, который имеет тот же интерфейс, что и исходный целевой объект, однако вызовы методов будут «дополнены» выводом информации о времени начала и окончания их работы (5):

 

public class AroundAdviceProgrammedExample {


      public static void main(String[] args) {

            ContainingLongRunningMethodClass target =

                  new ContainingLongRunningMethodClass();                    (1)


            ProxyFactory pf = new ProxyFactory();                            (2)

            pf.setTarget(target);                                            (3)

            pf.addAdvice(new DisplayTimeIntercepter());                      (4)


            ContainingLongRunningMethodClass proxy =                         (5)

                  (ContainingLongRunningMethodClass) pf.getProxy();


            proxy.longLoop();

      }


}

 

В результате выполнения этой программы на консоли появится примерно следующий:

Time before method launch: Mon Oct 19 18:18:36 CEST 2010

Delay time : 7

Time after method launch: Mon Oct 19 18:18:43 CEST 2010

 

Использование Pointcut при создании целевого объекта

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

Для создания среза в Spring необходимо создать класс, реализующий интерфейс Pointcut, в котором  определены два метода:

 

public interface Pointcut {

            ClassFilter getClassFilter();

            MethodMather getMethodMatcher();

}

 

Метод 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);

            boolean isRuntime();

}

 

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

Описание

o.s.a.s.StaticMethodMatcherPointcut

Используется для определения статических точек соединения, является наиболее часто используемым способом определения среза (Pointcut)

o.s.a.s.DynamicMethodMatcherPointcut

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

o.s.a.s.ComposablePointcut

Используется, когда необходимо одновременно два или более точек соединения (Pointcut) используя операции union или intersection

o.s.a.s.JdkRegexpMethodPointcut

Позволяет определять точка соединения (Pointcut) используя регулярные выражения JDK1.4

o.s.a.s.AnnotationMatchingPointcut

Использует аннотации языка Java (annotation) при определении точек соединения

o.s.a.s.AspectJExpressionPointcut

Применяется, когда для определения точек соединения используются язык выражений (expression language) языка AspectJ.

Для создания аспекта (напомним, что под аспектом – aspect - понимают комбинацию точка соединения и сквозной функциональности), необходимо создать экземпляр класса, реализующий интерфейс PointcutAdvisor, однако также как и случае с интерфейсом Pointcut нет необходимости полностью брать на себя его реализацию с нуля, поскольку среда Spring предоставляет несколько готовых решений, в частности DefaultPointcutAdvisor.

Рассмотрим пример с использованием DynamicMethodMatcherPointcut  для создания динамической точки соединения для класса, исходный код которого выглядит следующим образом:

 

public class ToBeDecoratedClass {

      public void rundomSleep() {

            try {

                  int delay = (int) (Math.random() * 10);

                  System.out.println("method rundomSleep, delay time : " + delay);

                  Thread.sleep(delay * 1000);

            } catch (InterruptedException e) {

                  e.printStackTrace();

            }

      }

 

      public void printInteger(int n) {

            System.out.println("method printInteger, n=" + n);

      }

}

 

Класс ToBeDecoratedClass содержит два метода, но мы хотим отслеживать вызов только одного из них, а именно printInteger, и только в случае, если значение аргумента n превышает 10.

В этом случае реализация DynamicMethodMatcherPointcut может иметь следующий вид:

 

public class DynamicPointcut extends DynamicMethodMatcherPointcut {

      @Override

      public boolean matches(Method method, Class<?> clazz) {

            return method.getName().equals("printInteger");

      }


      @Override

      public boolean matches(Method method, Class<?> clazz, Object[] args) {

            if (args.length == 0)

                  return false;


            Object obj = args[0];

            if (obj instanceof Integer) {

                  return (Integer) obj > 10;

            }

            else

                  return false;

      }


      @Override

      public ClassFilter getClassFilter() {

            return new ClassFilter() {


                  @Override

                  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 {

            System.out.println("Start executing " + invocation.getMethod().getName());

            Object value = invocation.proceed();

            System.out.println("End executing " + invocation.getMethod().getName());

            return value;

      }


}

 

Для демонстрации возможности создания и использования динамического среза также создан класс NotToBeDecoratedClass, интерфейс которого полностью идентичен интерфейсу ToBeDecoratedClass, но вызов метода printInteger которого мы не хотим отслеживать.

 

public class DynamicPointcutExample {


      public static void main(String[] args) {

            ToBeDecoratedClass toBeDecoratedClass = new ToBeDecoratedClass();          

            NotToBeDecoratedClass notToBeDecoratedClass =

new NotToBeDecoratedClass();     


            ToBeDecoratedClass proxyDecoratedClass;

            NotToBeDecoratedClass proxyNotToBeDecoratedClass;


            Pointcut pointcut = new DynamicPointcut();                            

            Advice advice = new LogInterceptor();                                 

            Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);       


            ProxyFactory proxyFactory = new ProxyFactory();

            proxyFactory.addAdvisor(advisor);

            proxyFactory.setTarget(toBeDecoratedClass);


            proxyDecoratedClass = (ToBeDecoratedClass) proxyFactory.getProxy();   


            proxyFactory = new ProxyFactory();                                    

            proxyFactory.addAdvisor(advisor);                                     

            proxyFactory.setTarget(notToBeDecoratedClass);                        


            proxyNotToBeDecoratedClass =

(NotToBeDecoratedClass) proxyFactory.getProxy();         


            System.out.println("ToBeDecoratedClass, printInteger(5)");

            proxyDecoratedClass.printInteger(5);


            System.out.println("ToBeDecoratedClass, printInteger(25)");

            proxyDecoratedClass.printInteger(25);


            System.out.println("ToBeDecoratedClass, rundomSleep");

            proxyDecoratedClass.rundomSleep();


            System.out.println("NotToBeDecoratedClass, printInteger(5)");

            proxyNotToBeDecoratedClass.printInteger(5);


            System.out.println("NotToBeDecoratedClass, printInteger(25)");

            proxyNotToBeDecoratedClass.printInteger(25);


            System.out.println("NotToBeDecoratedClass, rundomSleep");

            proxyNotToBeDecoratedClass.rundomSleep();


      }


}

 

Если запустить программу, то на системной консоли будет выведена примерно следующая информация:

 

ToBeDecoratedClass, printInteger(5)

ToBeDecoratedClass, running method printInteger, n=5

ToBeDecoratedClass, printInteger(25)

Start executing printInteger at Mon Oct 13 18:47:31 CEST 2010

ToBeDecoratedClass, running method printInteger, n=25

End executing printInteger at Mon Oct 13 18:47:31 CEST 2010

ToBeDecoratedClass, rundomSleep

ToBeDecoratedClass, running  method rundomSleep, delay time : 0

NotToBeDecoratedClass, printInteger(5)

NotToBeDecoratedClass, running method printInteger, n=5

NotToBeDecoratedClass, printInteger(25)

NotToBeDecoratedClass, running method printInteger, n=25

NotToBeDecoratedClass, rundomSleep

NotToBeDecoratedClass, running method rundomSleep, delay time : 7

 

Как и предполагалось, только вызов метода printInteger объекта класса ToBeDecoratedClass с параметром 25 был «перехвачен» для выполнения дополнительных действий, в данном случае информации о времени начала и окончания исполнения метода.

 

В примере, разобранном в предыдущем разделе AroundAdviceProgrammedExample, необходимо было значительно меньше действий для получения объекта-посредника (proxy), поскольку отсутствовали такие действия как инициализация Pointcut, создание и инициализация Advisor и некоторые другие действия. На самом деле метод addAdvice класса производит все эти операции внутри себя, то есть создает экземпляр класса DefaultPointcutAdvisor и создает срез, который применяется ко всем методам исходного класса. 

 

Использование АОП с помощью средств конфигурации платформы Spring

 

Примерам, которые были приведены до сих пор, были присущи некоторые из тех недостатков, для устранения которых и создавалась платформа Spring. Перечислим некоторые из них.

Во первых, в классах, реализующих сквозную  функциональность, явно указана зависимость от служебных интерфейсов (таких как MethodInterceptor), что добавляет зависимость от библиотек, не относящихся напрямую к логике класса, что противоречит одному из основных принципов платформы Spring об наибольшей независимости бизнес компонентов от вспомогательных служебных интерфейсов (non-invasiveness). Другим серъезным недостатком является создание объектов посредников явным образом в тексте программы. Все это отрицательным образом сказывается на сопровождении, модернизации или тестировании создаваемого программного обеспечения (то есть как раз те цели, для обеспечения которых и задумывалось аспектно-ориентированное программирование).

Как уже отмечалось ранее, программный способ создания аспектов не удобен для практического использования при реальной разработке. Используемый практически во всех проектах декларативный способ применяет принцип внедрения зависимостей (dependency injection). Аспекты объявляются и конфигурируются как обычные управляемые компоненты (bean), используя для этого различные предоставляемые средой технологии (конфигурационный файл, аннотации, автоматическое связывание – autoproxy и другие).

Одним из наиболее часто используемых методов является применение так называемого @AspectJ способа объявления аспектов. @AspectJ не имеет ничего с аспектно-ориентированным расширением языка Java именуемым AspectJ. Это набор Java 5 аннотаций, обеспечивающих удобный способ создания точек среза (pointcut)  и аспектов (aspect) используя только средства Spring AOP, обеспечивая очень удобный способ их создания. Некоторые интегрированные среды разработки (IDE) такие как IntelliJ IDEA или Springsource Toolsuite (STS) имеют встроенные средства поддержки @AspectJ аннотаций, значительно облегчающие процесс создания программного обеспечения.

Рассмотрим пример объявления аспекта в стиле @AspectJ и его использования. В первую очередь надо объявить, что класс, реализующий некоторую сквозную функциональность, является аспектом. Для этого перед определением этого класса помещается аннотацию @Aspect, как это показано на следующем примере:

 

@Aspect

public class AnnotatedLogInterceptor {


      @Around("execution(* longRunningMethod(..)")

      public Object invoke(MethodInvocation invocation) throws Throwable {

            System.out.println("Start executing " + invocation.getMethod().getName() +

" at " + new Date());

            Object value = invocation.proceed();

            System.out.println("End executing " + invocation.getMethod().getName() +

" at " + new Date());

            return value;

      } }

 

Внутри аспекта существует метод invoke, который помечен аннотацией @Around с выражением для определения  точек соединения «execution(* longRunningMethod(..))». Это объявление определяет, что метод  invoke будет как бы «обворачивать» вызовы метода с именем longRunningMethod любого управляемого компонента, существующего в контексте окружения Spring. Основные элементы языка выражений и его ключевые слова для определения точек среза представлены в следующей таблице:

Выражение @AspectJ

                                                              Описание                                                

execution

Определяет точки соединения на основании имени метода. Наиболее часто используемое выражение для определения jointpoint. При использоании выражения execution возможно указывать пакет, имя класса, название метода, видимость метода, тип возвращаемого объекта и тип аргументов.

Например

execution(String com.package.subpackage.Classname.somemethod(..)) определяет вызов метода somemethod класса com.package.subpackage.Classname с любым количеством аргументов и возврашающий строку

execution(* com.package.subpackage.Classname.*(..)) – вызов любого метода класса com.package.subpackage.Classname

execution(* somemethod(..)) – вызов метода с именем somemethod у любого класса

within

определяет возможные точки соединения только у объектов заданного типа или у классов, определенных в заданном пакете и его подпакетах .

Пример использования:

within(com.package.subpackage.*) – определяет вызовы методов всех классов, определенных в пакете com.package.subpackage

within(com.package.subpackage..*) – определяет вызовы методов всех классов, определенных в пакете com.package.subpackage и всех дочерних пакетах

this

Определяет точки соединения для всех объектов, у которых объект посредника (AOP proxy) реализует указанный в аннотации тип.

Пример использования:

this(com.package.InterfaceName) –определяет вызовы методов у объектов-посредников, реализующих интерфейс com.package.InterfaceName

target

Определяет точки соединения для всех объектов, у которых целевой объект (target) реализует указанный в аннотации тип.

Пример использования:

target(com.package.InterfaceName) –определяет вызовы методов объектов, целевой класс которых реализует интерфейс com.package.InterfaceName

args

Определяет точки соединения сравнением аргументов вызываемого метода с типами аргументов, указанных в аннотации.

Пример использования:

args(String) – определяет методы, у которых определен один строковый аргумент

bean

Определяет точки соединения для управляемых компонентов (beans), имеющих определенный в аннотации идентификатор или имя (атрибуты id или name компонента). При указании имени бина возможно использовать групповой символ (wildcard)

Пример использования:

bean(„justABean“) – определяет точки соединения для управляемого компонента с именем (идентификатором) justABean

bean(„*Bean“) - определяет точки соединения для всех управляемых компонент с именем (идентификатором) заканчивающимся на Bean

 

@annotation

Задает точки соединения для методов, которые были «помечены» указанной аннотацией

Пример использования:

@annotation(com.package.annotation.Annotation)

Однако наличие у класса аннотации @Aspect не означает, что окружение автоматически обнаружит и инициализирует его. Для этого необходимо во-первых, чтобы в окружении Spring существовала поддержка @AspectJ, которая по умолчанию отсутствует. Один из возможных способов обеспечения поддержки  @AspectJ  является указание в конфигурационном файле XML элемента <aop:aspectj-autoproxy/>. И во-вторых необходимо, чтобы аспект также являлся управляемым компонентов, то есть существовал в контексте окружения. Для этого существует множество возможных способов, можно например использовать аннотацию @Component при определении класса и в конфигурационном файле зарегистрировать механизм поиска context:component-scan (подробнее об этом можно узнать из статьи на нашем сайте Dependency Injection и Spring). В данном случае аспект явно объявлен в конфигурационном файле:

 

<?xml version="1.0" encoding="UTF-8"?>

<beans      xmlns="http://www.springframework.org/schema/beans"

            xmlns:context="http://www.springframework.org/schema/context"

            xmlns:aop="http://www.springframework.org/schema/aop"

            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

            xsi:schemaLocation="

                  http://www.springframework.org/schema/beans

                  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

                  http://www.springframework.org/schema/context

                  http://www.springframework.org/schema/context/spring-context-3.0.xsd

                  http://www.springframework.org/schema/aop

                  http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

 

      <bean class="com.finecosoft.aop.aspectj.AnnotatedLogInterceptor"/>

      <bean id="longRunner" class="com.finecosoft.aop.aspectj.ContainingLongRunningMethodClass"/>

 

      <aop:aspectj-autoproxy/>

</beans>

 

Целевой класс, экземпляр которого также объявлен в конфигурационном файле, может выглядеть следующим образом, основным моментом является наличие метода с именем longRunningMethod, вызовы которого будут «перехвачены» созданным аспектом:

 

public class ContainingLongRunningMethodClass {

      public void longRunningMethod() {

            try {

                  int delay = (int) (Math.random() * 10);

                  System.out.println("Delay time : " + delay);

                  Thread.sleep(delay * 1000);

            } catch (InterruptedException e) {

                  e.printStackTrace();

            }

      }

}

 

Если запустить данный пример:

 

public class AspectjDemo {

      public static void main(String[] args) {

            ApplicationContext beanFactory =

                  New ClassPathXmlApplicationContext("com/finecosoft/aop/aspectj/applicationContext.xml");

 

            ContainingLongRunningMethodClass longRunner =

                  (ContainingLongRunningMethodClass) beanFactory.getBean("longRunner");

 

            longRunner.longRunningMethod();

      }

}

 

на системной консоли будет выведен примерно следующий результат:

 

 

Start executing longRunningMethod at Fri Oct 21 16:33:36 CEST 2010

Delay time : 5

End executing longRunningMethod at Fri Oct 21 16:33:41 CEST 2010

 

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

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

Аннотация @AspectJ

                                                              Описание                                                

@Before

Метод аспекта, помеченный этой аннотацией, будет вызван перед выполнением метода целевого класса. В отличие от аннотации @Around, избежать вызова метода целевого класса возможно только генерацией исключительной ситуации (exception) внутри аспекта.

Пример использования:

 

@Before (pointcut="execution(* beforeAnnotatedMethod(..)")

public void logBeforeExecution() {

       System.out.println(“Calling method : beforeAnnotatedMethod”);

}

@AfterReturning

Метод аспекта, помеченный этой аннотацией, будет вызван после нормального завершения работы метода целевого класса. Под «нормальным завершением» понимается, что в ходе выполнения не было сгенерировано исключительной ситуации (exception)

Пример использования:

 

@Before (pointcut="execution(* beforeAnnotatedMethod(..)")

public void logBeforeExecution() {

       System.out.println(“Calling method : beforeAnnotatedMethod”);

}

@AfterThrowing

Использование этой аннотации определяет, что сквозная функциональность будет выполнена в том случае, если выполнение целевой метода закончилось исключительной ситуацией. Тип исключения определяется аргументом метода, помеченного этой аннотацией, например:

 

@AfterThrowing(pointcut="execution(* longRunningMethod(..)",

throwing=”exception”, argNames=” exception”)

public void logException(IOException exception) {

       System.out.println(“Exception thrown: ” + exception);

}

@After

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

@Around

Помеченный этой аннотацией метод как бы «обворачивает» вызов метода целевого класса, как это показано в приведенном в этой статье примере. Данная аннотация является наиболее мощным средством для создания аспектов, но при этом такой подход требует наибольших трудозатрат.

Использование внедрения (introduction) в Spring AOP

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

Пример внедрения продемонстрируем на более простом примере. В системе существует следующий интерфейс

 

public interface ToBeExtendedService {

      void someMethod();

}

 

и реализующий его класс

 

public class ToBeExtendedServiceImpl implements ToBeExtendedService {

      public void someMethod() {

            System.out.println("call of the method someMethod");

      }

}

 

Допустим, что необходимо посчитать, сколько раз в ходе работы программы был вызван метод someMethod у экземпляра класса, реализующего интерфейс ToBeExtendedService. Потребностям такого простейшего счетчика удовлетворяет состоящий из двух методов интерфейс:

 

public interface MethodCallCounter {

      void count();

      int getCounter();

}

 

который может иметь следующую реализацию:

 

public class MethodCallCounterImpl implements MethodCallCounter {

      private int counter;

 

      public void count() {

            counter++;

      }

 

      public int getCounter() {

            return counter;

      }

}

 

Следующим шагом является создание аспекта, который включает совет (advice), выполняющийся перед вызовом целевой функции (someMethod в нашем примере) и определение, управляемый компонент какого класса должен быть модифицирован путем включения дополнительной функциональности. Пример такого аспекта приведен ниже:

 

@Aspect

public class MethodCallCounterAspect {

      @DeclareParents(value = "com.finecosoft.aop.introduction.ToBeExtendedService+",

                  defaultImpl=MethodCallCounterImpl.class)

      public static MethodCallCounter mixin;

 

      @Before(value="execution(* com.finecosoft.aop.introduction.ToBeExtendedService.someMethod(..)) && this(methodCallCounter)",

                  argNames="methodCallCounter")

      public void countMethodCall(MethodCallCounter methodCallCounter) {

            methodCallCounter.count();

      }

}

 

Отличительной особенностью этого аспекта от предыдущих примеров является использование аннотации @DeclareParents, которая определяет, что все классы, реализующий интерфейс ToBeExtendedService (определяется аргументом value), будут «дополнены» интерфейсом MethodCallCounter (аннотация применяется к полю данного типа), используя реализацию MethodCallCounterImpl (значение аргумента defaultImpl).

В аспекте уже описанным ранее способом определена дополнительная функциональность, помеченная аннотацией @Before, которой будет дополнен вызов метода someMethod класса, одновременно реализующий интерфейс ToBeExtendedService и MethodCallCounter. Реализация интерфейса ToBeExtendedService задается уже знакомым выражением com.finecosoft.aop.introduction.ToBeExtendedService, связывание аннотированной фукции с интерфейсом MethodCallCounter происходит следующим образом: вторая часть выражения AspectJ аннотации – this(methodCallCounter) – определяет, что объект должен быть того же типа, что и аргумент метода.

Конфигурационный файл, который используется в данном примере, не использует ничего нового и отличается от предыдущего примера только названием компонентов и классов:

<beans      xmlns="http://www.springframework.org/schema/beans"

            xmlns:context="http://www.springframework.org/schema/context"

            xmlns:aop="http://www.springframework.org/schema/aop"

            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

            xsi:schemaLocation="

                  http://www.springframework.org/schema/beans

                  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

                  http://www.springframework.org/schema/context

                  http://www.springframework.org/schema/context/spring-context-3.0.xsd

                  http://www.springframework.org/schema/aop

                  http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

 

      <bean class="com.finecosoft.aop.introduction.MethodCallCounterAspect"/>

      <bean id="toBeExtendedService" class="com.finecosoft.aop.introduction.ToBeExtendedServiceImpl"/>

 

      <aop:aspectj-autoproxy/>

</beans>

 

Однако в данном случае Spring AOP самостоятельно модифицирует создаваемый управляемый компонент toBeExtendedService добавлением методов count и getCounter, поскольку к этому компоненту применяется аспект MethodCallCounterAspect.

Пример приложения, которая демонстрирует пример использования внедрения в Spring AOP, выглядит следующим образом:

 

public class IntroductionDemo {

      public static void main(String[] args) {

            ApplicationContext ctx =

                  new ClassPathXmlApplicationContext("com/finecosoft/aop/introduction/applicationContext.xml");

 

            ToBeExtendedService toBeExtendedService =

                  (ToBeExtendedService) ctx.getBean("toBeExtendedService");

 

            toBeExtendedService.someMethod();

            toBeExtendedService.someMethod();

            toBeExtendedService.someMethod();

 

            printCallCounter(toBeExtendedService);

      }

 

 

      public static void printCallCounter(Object obj) {

            MethodCallCounter counter = (MethodCallCounter) obj;

            System.out.println("The method was called " + counter.getCounter() + " time(s)");

      }

}

 

Приведенный пример показывает, что несмотря на то, что компонент  toBeExtendedService был инициализирован из класса ToBeExtendedServiceImpl, реализующим только интерфейс ToBeExtendedService, тем не менее он также может быть использован как объект, реализующий также интерфейс MethodCallCounter, как это сделано в методе printCallCounter. Выполнение данного приложения приведет к следующему выводу на системной консоли, что подтверждает дополнение исходного кода класса ToBeExtendedServiceImpl реализацией интерфейса MethodCallCounter:

 

call of the method someMethod

call of the method someMethod

call of the method someMethod

The method was called 3 time(s)

 

 

Примеры

 

Исходные тексты, приведенные в статье можно скачать здесь

© 2008-2023 Финэкософт.

 

Oracle Silver Partner
+7 (495) 234 8808
Учебный центр
Центр обучения и сертификации в области информационных технологий (IT).

Широкий выбор курсов и программ обучения. Подробности здесь.

Отправить письмо
Обратная связь

 

Для Ваших вопросов и отзывов