12.08.2014

Java: Аспектно-ориентированное программирование с помощью Spring Framework.

АОП - парадигма программирования, которая расширяет возможности существующих концепций, конкретнее - ООП. Она служит для удобной и четкой реализации сквозной функциональности в программе. Сквозная функциональность - это то, что происходит во всей программе в различных местах - в методах, конструкторах класса и т.п., что-то такое что можно было бы логически выделить в одну "сущность", но сделать это с помощью обычных средств ООП сложно, а то и вовсе невозможно. Не смотря на то, что программа всегда разбита на какие-то модули - классы, методы классов и т.п., случается, что в каждом из этих мест нужно выполнять схожие действия. И используя традиционный подход к проектированию, получаются весьма запутанные и сложные для поддержки и отладки конструкции. АОП предлагает некоторые концепции проектирования приложений, с помощью которых можно такие конструкции превратить в простые, стройные и удобные для поддержки и отладки. На первый взгляд, идея АОП может показаться слишком сложной и запутанной, но достаточно понять идею и использовать АОП будет очень просто - особенно с помощью Spring Framework. Spring предлагает ограниченную поддержку АОП в Java, но этого достаточно для множество задач, к тому же преимуществом использования Spring для АОП является простота. Сначала я хотел бы привести несколько ключевых терминов из концепции АОП, а потом рассмотреть простой, но очень детальный пример реализации АОП с помощью Spring.
 
Кое-что из концепции
 
Точка соединения (joinpoint) - это точка во времени выполнения программы, в которой будет выполнены какие-то действия. Например - создание экземпляров классов в программе. Или вызов методов определенного класса. С помощью АОП можно внедрить определенную логику в обозначенную точку соединения. Допустим, при вызове метода класса будет выполняться логирование аргументов переданных этому методу. Вызов метода класса - точка соединения.
Совет (advice) - Логика, которая будет выполняться в заданной точке соединения. В вышеописанном примере - логирование аргументов переданных методу.
Срез (pointcut) - Совокупность точек соединения - на примере выше это вызов любого из  методов определенного класса.
Аспект (aspect) - Это комбинация логики и указания, в каких точках программы эта логика должна выполняться. Т.е. комбинация совета и срезов.
Цель (target) - объект, поведение которого будет меняться с помощью АОП. Например класс, поведение методов которого могут изменяться во время выполнения с помощью советов. Т.е. объект, который снабжен советом. Как было замечено выше - Spring обеспечивает ограниченную поддержку АОП в Java. Основное ограничение - доступен только один тип точек соединения - вызов метода. Для больших возможностей можно посмотреть на АОП расширение языка Java - AspectJ. Но тема этого поста - Spring и АОП, так что давайте перейдем к конкретному примеру.

АОП с помощью Spring - пример. 

Очень часто в качестве примеров использования АОП приводят логирование. Это действительно наглядный пример, но на первый взгляд может показаться, будто АОП и пригодно разве что для логирования, а для чего же еще применять эту концепцию - не особо понятно. Поняв подход АОП, его можно применять во множестве ситуаций. Просто так взять и придумать такие ситуации бывает поначалу сложно, но зная суть АОП и столкнувшись с ситуацией где другие способы выглядят как-то "некрасиво", на ум приходит мысль, что именно здесь АОП подойдет идеально. Посмотрим на простой, но детальный пример, как использовать АОП в Spring. Предположим у нас класс, который отвечает за обработку счетов. Не важно что конкретно он делает, но у него может быть множество методов, которые работают с одним и тем же объектом - счетом. Перед тем как обработать счет, его нужно проверить по заданному алгоритму. Проще простого - создадим метод для проверки и добавим в каждый из методов нашего сервиса. Вдруг появляется проблема - начиная с завтрашнего утра, счета нужно проверять по другому. Окей, создадим другой метод проверки и изменим каждый из методов нашего класса, сервиса обработки счетов. Подобные изменения возможны и в дальнейшем, а возможно нам придется использовать старый алгоритм проверки, так что приходится менять сервис, пусть и не существенно (добавлять разные методы проверок). Еще проблема - теперь для определенного вида счетов нужно выполнять отдельные действия, уже после валидации данных. Нужен второй этап проверки в каждом из методов сервиса, и дополнительный сценарий обработки. Это конечно утрированный пример, но представьте что методов у сервиса работы со счетами очень много, а постоянно менять метод проверки не представляется хорошим - нужно оставить различные варианты проверок. Значит нам нужно вносить соответствующую функциональность и менять ее - в каждом из методов сервиса. С помощью АОП можно сделать это более красиво и гибко. Целью для создания совета будет сервис обработки счетов. Срезом будет являться вызов каждого из методов этого класса. Перед вызовом любого из его методов мы проверим аргументы, если они прошли проверку - выполним метод. Кроме того, если это необходимо - можно даже пропустить выполнение целевого метода, заменив его совсем другой реализацией. Иными словами - добавим функциональность для каждого из целевых методов, не изменяя эти методы. Здесь небольшое отступление. Итак, мы будем использовать точку соединения (только такие точки соединения и доступны в Spring) - Method Invocation. Совет (логика) будет выполняться вместо целевого метода. Этот тип советов так и называется "вместо". Вот типы советов, которые есть в Spring:

Перед (before) - Это тип совета, когда логика выполняется перед вызовом определенного метода. Совет "перед" имеет полный доступ к аргументам метода, но не имеет контроля за дальнейшим выполнением метода.

После возврата (after returning) - Имеет доступ к аргументам метода и возвращаемому значению, но не имеет контроля над выполнением метода. Выполняется только в случае успешного завершения метода.

После (after) - Аналогично пункту 3, за тем исключением, что этот совет выполняется в любом случае, даже если метод завершился не успешно.

Вместо (around) - Полный доступ к аргументам и возвращаемому значению, а также полный контроль над методом - его можно даже пропустить или заменить другой реализацией. Этот тип и будет использован в примере.  

Перехват (throws) - Выполняется только в том случае если метод сгенерировал определенный тип исключения.

Введение (introduction) - Для указания реализации метода, которая должна быть введена советом. Итак, в нашем примере мы создадим совет типа "вместо" для сервиса обработки счетов. В Spring для реализации АОП используется понятие прокси. Это объект который "содержит" цель и который ответственен за применение советов.

В нашем примере применение совета к методу будет выглядеть так:

В примере есть простой класс "счет":
import java.math.BigDecimal;

public class Bill {

 private BigDecimal summ;
 private String currency;
 private String country;

//getters and setters

}

И сервис работы со счетами, который будет целевым объектом:

public class PaymentService {

 public String proceedBill1(Bill input) {
  if (input == null)
      throw new IllegalArgumentException("bill == null");
  //какие-то действия со счетом
  return "Обработка счета по варианту #1 завершена";
 }

 public String proceedBill2(Bill input) {
  if (input == null)
      throw new IllegalArgumentException("bill == null");
  //какие-то другие действия со счетом
  return "Обработка счета по варианту #2 завершена";
 }

}

Для начала включим необходимые зависимости в проект:

<dependency>
   <groupId>aopalliance</groupId>
   <artifactId>aopalliance</artifactId>
   <version>1.0</version>
</dependency>

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aop</artifactId>
   <version>3.2.4.RELEASE</version>
</dependency>

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

import java.math.BigDecimal;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class PaymentAdviser implements MethodInterceptor {

 @Override
 public Object invoke(MethodInvocation object) 
                      throws Throwable {
  
  //аргуметы, переданные целевому методу
  Object[] arguments = object.getArguments();
  
  for (Object buff: arguments) {
   
   if (buff instanceof Bill) {
    Bill bill = (Bill) buff;
    
    if (bill.getCurrency() == null ||
     bill.getCurrency().isEmpty() ||
     bill.getSumm() == null  ||
     bill.getSumm().equals(BigDecimal.valueOf(0))) {
     
     return "Не указаны: сумма и/или валюта счета!";
    }
    
    //Для этой страны обработка счета идет по
    //другому, отдельному сценарию
    if (bill.getCountry() != null)
    if (bill.getCountry().equals("Кокосовые острова"))
     return this.proceedBill3(bill);
   } 
  }
  
  //если все проверки пройдены - мы просто 
  //вызываем целевой метод 
  return object.proceed();
 }
 
 private String proceedBill3 (Bill input) {
  //какие-то действия, которые не предусмотрены 
  //в сервисе обработки счетов
  return "Это счастливый счет, для него у нас" +
    " есть специальный вариант обработки!";
 }
}

Теперь создадим тест в методе Main:

import java.math.BigDecimal;
import org.springframework.aop.framework.ProxyFactory;

public class Main {

 public static void main(String[] args) {
  
  //прокси - реализация АОП в Spring
  ProxyFactory pf = new ProxyFactory();
  //объект, для которого будет выполняться совет
  pf.setTarget(new PaymentService());
  //созданный нами совет
  pf.addAdvice(new PaymentAdviser());
  
  //объект "сервис платежей" с которым будем работать
  PaymentService service = (PaymentService) pf.getProxy();
  
  //тесты для трех вариантов, предусмотренных в совете
  Bill test1 = new Bill();
  test1.setCountry("Россия");
  test1.setCurrency("RUB");
  test1.setSumm(new BigDecimal("100.50"));
  
  Bill test2 = new Bill();
  test2.setCountry("США");
  test2.setCurrency("USD");
  
  Bill test3 = new Bill();
  test3.setCountry("Кокосовые острова");
  test3.setCurrency("USD");
  test3.setSumm(new BigDecimal("1000000.00"));
  
  //вызываем ЛЮБОЙ из методов на целевом объекте
  System.out.println("Счет 1: " + service.proceedBill1(test1));
  System.out.println("Счет 2: " + service.proceedBill2(test2));
  System.out.println("Счет 3: " + service.proceedBill1(test3)); 
 }
}

Конечно же для создания прокси в Spring можно использовать не только ProxyFactory, если объекты создаются с помощью внедрения зависимостей - тогда используется ProxyFactoryBean. Использование Spring АОП в сочетании с механизмом внедрения зависимостей позволяет создать очень гибкую, расширяемую архитектуру программы. В примере ProxyFactory используется для того, чтобы не писать лишнего. В результате выполнения нашего теста мы получим такой консольный вывод: Счет 1: Обработка счета по варианту #1 завершена Счет 2: Не указаны: сумма и/или валюта счета! Счет 3: Это счастливый счет, для него у нас есть специальный вариант обработки! Все именно так как и ожидалось!

Скачать пример

В книге рассматривается в том числе АОП в Spring Pro Spring 3 - Clarence Ho, Rob Harrop



Теги: programming javaEE java

comments powered by Disqus