Статьи

Аспектно-ориентированное программирование с помощью Spring AOP

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

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

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

Использование и внедрение AOP было усилено с помощью нашей любимой среды Spring . Spring облегчает интеграцию AOP в наши проекты, используя ненавязчивый подход. Джастин говорил ранее о Spring AOP в своем посте под названием « Аспектно-ориентированное программирование с Spring AspectJ и Maven » здесь, на JavaCodeGeeks . Тем не менее, наш последний партнер JCG , Siva из SivaLabs , также написал очень хорошую статью о Spring AOP, и я хотел поделиться ею с вами, так что вот она.

(ПРИМЕЧАНИЕ: оригинальный пост был слегка отредактирован для улучшения читабельности)

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

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

  • Экран входа / регистрации для входа в BookStore.
  • Пользователи должны иметь возможность просматривать различные категории книг
  • Пользователи должны иметь возможность искать книги по имени, имени автора, издателю
  • Пользователи должны иметь возможность добавлять / удалять книги в / из своей корзины
  • Пользователи должны иметь возможность видеть, какие товары в настоящее время существуют в их корзине.
  • Пользователи должны иметь возможность оформить заказ и оплатить соответствующую сумму через какой-либо платежный шлюз
  • Успешное сообщение должно быть показано пользователям со всеми деталями их покупок.
  • Сообщение об ошибке должно быть показано пользователям с причиной сбоя.
  • Администратору / менеджеру BookStore должен быть предоставлен доступ для добавления / удаления / обновления информации о книге.

Все вышеперечисленные требования относятся к категории «Функциональные требования». При реализации вышеизложенного мы также должны позаботиться о следующих вещах, даже если они явно не упомянуты:

  • Ролевый доступ к пользовательскому интерфейсу. Здесь только администраторы / менеджеры должны иметь доступ для добавления / удаления / обновления информации о книге. [Авторизация на основе ролей]
  • Атомность в закупках. Предположим, что пользователь вошел в Книжный магазин и добавил 5 книг в свою корзину, оформил заказ и завершил платеж. В серверной реализации нам может понадобиться ввести данные покупки в 3 таблицы. Если после вставки данных в 2 таблицы произошел сбой системы, необходимо выполнить откат всей операции. [Управление транзакциями].
  • Никто не совершенен, и ни одна система не является безупречной. Поэтому, если что-то пошло не так, и команда разработчиков должна выяснить, что пошло не так, ведение журнала будет полезно. Таким образом, ведение журнала должно быть реализовано таким образом, чтобы разработчик мог выяснить, где именно произошло сбой приложения, и исправить его. [Логирование]

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class OrderService
{
    private OrderDAO orderDAO;
     
    public boolean placeOrder(Order order)
    {
        boolean flag = false;
        logger.info("Entered into OrderService.placeOrder(order) method");
        try
        {
            flag = orderDAO.saveOrder(order);
        }
        catch(Exception e)
        {
            logger.error("Error occured in OrderService.placeOrder(order) method");
        }
        logger.info("Exiting from OrderService.placeOrder(order) method");
        return flag;
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class OrderDAO
{
    public boolean saveOrder(Order order)
    {
        boolean flag = false;
        logger.info("Entered into OrderDAO.saveOrder(order) method");
        Connection conn = null;
        try
        {
            conn = getConnection();//get database connection
            conn.setAutoCommit(false);
            // insert data into orders_master table which generates an order_id
            // insert order details into order_details table with the generated order_id
            // insert shipment details into order_shipment table
            conn.commit();
            conn.setAutoCommit(true);
            flag = true;
        }
        catch(Exception e)
        {
            logger.error("Error occured in OrderDAO.saveOrder(order) method");
            conn.rollback();
        }
        logger.info("Exiting from OrderDAO.saveOrder(order) method");
        return flag;
    }
}

В приведенном выше коде реализация функциональных требований и реализация нефункциональных требований смешаны в одном и том же месте. Регистрация ведется по классам OrderService и OrderDAO. В то же время управление транзакциями распространяется на DAO.

При таком подходе у нас есть несколько проблем:

  1. Классы должны быть изменены, чтобы изменить функциональные или нефункциональные требования. Например: на более позднем этапе разработки, если команда решает зарегистрировать информацию о входе / выходе метода вместе с меткой времени, нам необходимо изменить почти все классы.
  2. Код управления транзакциями, устанавливающий автоматическую фиксацию на false в начале, выполняющий операции с БД, фиксирующий / откатывающий логику операции, будет продублирован во всех DAO.

Этот вид требований, которые распространяются на модули / компоненты, называется сквозной проблемой. Чтобы лучше спроектировать систему, мы должны отделить эти сквозные проблемы от реальной бизнес-логики, чтобы было легче изменить, улучшить или поддерживать приложение на более позднем этапе.

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

1
2
3
4
public interface IOrderService
{
    public boolean placeOrder(Order order);
}
1
2
3
4
5
6
7
8
9
public class OrderService implements IOrderService
{
    private OrderDAO orderDAO;
     
    public boolean placeOrder(Order order)
    {
        return orderDAO.saveOrder(order);
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class OrderDAO
{
    public boolean saveOrder(Order order)
    {
        boolean flag =false;
         
        Connectoin conn = null;
        try
        {
            conn = getConnection();//get database connection
            // insert data into orders_master table which generates an order_id
            // insert order details into order_details table with the generated order_id
            // insert shipment details into order_shipment table
            flag = true;
        }
        catch(Exception e)
        {
            logger.error(e);           
        }       
        return flag;
    }
}

Теперь давайте создадим LoggingInterceptor, реализующий порядок ведения журнала, и создадим Proxy для OrderService, который принимает вызов от вызывающей стороны, регистрирует записи входа / выхода с использованием LoggingInterceptor и, наконец, делегирует действительный OrderService.

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

01
02
03
04
05
06
07
08
09
10
11
public class LoggingInterceptor
{
    public void logEntry(Method m)
    {
        logger.info("Entered into "+m.getName()+" method");
    }
    public void logExit(Method m)
    {
        logger.info("Exiting from "+m.getName()+" method");
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class OrderServiceProxy implements IOrderService extends LoggingInterceptor
{
    private OrderService orderService;
     
    public boolean placeOrder(Order order)
    {
        boolean flag =false;
        Method m = getThisMethod();//get OrderService.placeOrder() Method object
        logEntry(m);
        flag = orderService.placeOrder(order);
        logExit(m);
        return flag;
    }
}

Теперь вызывающая программа OrderService (OrderController) может получить OrderServiceProxy и разместить заказ как:

01
02
03
04
05
06
07
08
09
10
public class OrderController
{
    public void checkout()
    {
        Order order = new Order();
        //set the order details
        IOrderService orderService = getOrderServiceProxy();
        orderService.placeOrder(order);
    }
}

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

  • Весенний АОП
  • AspectJ
  • JBoss AOP

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

  • JoinPoint: точка соединения — это точка выполнения приложения, в которую может быть включен аспект. Это может быть вызываемый метод, выбрасываемое исключение или даже изменяемое поле.
  • Pointcut: определение pointcut соответствует одной или нескольким точкам соединения, в которых следует создавать рекомендации. Часто вы указываете эти pointcut с использованием явных имен классов и методов или с помощью регулярных выражений, которые определяют соответствующие шаблоны имен классов и методов.
  • Аспект: аспектом является объединение советов и идей.
  • Совет: Работа аспекта называется советом. Это дополнительный код, который мы применяем к существующей модели.

SpringAOP поддерживает несколько типов советов, а именно:

  • Before: Этот совет связывает аспект перед вызовом метода.
  • AfterReturning: этот совет переплетает аспект после вызова метода.
  • AfterThrowing: этот совет создает аспект, когда метод генерирует исключение.
  • Вокруг: Этот совет сплетает аспект до и после вызова метода.

Предположим, у нас есть следующие интерфейс ArithmeticCalculator и классы реализации.

1
2
3
4
5
6
7
8
9
package com.springapp.aop;
 
public interface ArithmeticCalculator
{
    public double add(double a, double b);
    public double sub(double a, double b);
    public double mul(double a, double b);
    public double div(double a, double b);
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.springapp.aop;
import org.springframework.stereotype.Component;
 
@Component("arithmeticCalculator")
public class ArithmeticCalculatorImpl implements ArithmeticCalculator
{
    public double add(double a, double b)
    {
        double result = a + b;
        System.out.println(a + " + " + b + " = " + result);
        return result;
    }
 
    public double sub(double a, double b)
    {
        double result = a - b;
        System.out.println(a + " - " + b + " = " + result);
        return result;
    }
 
    public double mul(double a, double b)
    {
        double result = a * b;
        System.out.println(a + " * " + b + " = " + result);
        return result;
    }
 
    public double div(double a, double b)
    {
        if(b == 0)
        {
            throw new IllegalArgumentException("b value must not be zero.");
        }
        double result = a / b;
        System.out.println(a + " / " + b + " = " + result);
        return result;
    }
}

Следующий класс LoggingAspect показывает различные фрагменты применения Рекомендации по ведению журнала с использованием Spring AOP:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.springapp.aop;
 
import java.util.Arrays;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
 
@Aspect
@Component
public class LoggingAspect
{
    private Log log = LogFactory.getLog(this.getClass());
     
    @Pointcut("execution(* *.*(..))")
    protected void loggingOperation() {}
     
    @Before("loggingOperation()")
    @Order(1)
    public void logJoinPoint(JoinPoint joinPoint)
    {
        log.info("Join point kind : " + joinPoint.getKind());
        log.info("Signature declaring type : "+ joinPoint.getSignature().getDeclaringTypeName());
        log.info("Signature name : " + joinPoint.getSignature().getName());
        log.info("Arguments : " + Arrays.toString(joinPoint.getArgs()));
        log.info("Target class : "+ joinPoint.getTarget().getClass().getName());
        log.info("This class : " + joinPoint.getThis().getClass().getName());
    }
         
    @AfterReturning(pointcut="loggingOperation()", returning = "result")
    @Order(2)
    public void logAfter(JoinPoint joinPoint, Object result)
    {
        log.info("Exiting from Method :"+joinPoint.getSignature().getName());
        log.info("Return value :"+result);
    }
     
    @AfterThrowing(pointcut="execution(* *.*(..))", throwing = "e")
    @Order(3)
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e)
    {
        log.error("An exception has been thrown in "+ joinPoint.getSignature().getName() + "()");
        log.error("Cause :"+e.getCause());
    }
     
    @Around("execution(* *.*(..))")
    @Order(4)
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable
    {
        log.info("The method " + joinPoint.getSignature().getName()+ "() begins with " + Arrays.toString(joinPoint.getArgs()));
        try
        {
            Object result = joinPoint.proceed();
            log.info("The method " + joinPoint.getSignature().getName()+ "() ends with " + result);
            return result;
        } catch (IllegalArgumentException e)
        {
            log.error("Illegal argument "+ Arrays.toString(joinPoint.getArgs()) + " in "+ joinPoint.getSignature().getName() + "()");
            throw e;
        }       
    }
     
}

Вот что должно включать наше applicationContext.xml:

1
 

А вот и отдельный тестовый клиент для проверки функциональности.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.springapp.aop;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class SpringAOPClient
{
 
    public static void main(String[] args)
    {
        ApplicationContext context =
  new ClassPathXmlApplicationContext("applicationContext.xml");
        ArithmeticCalculator calculator =
  (ArithmeticCalculator) context.getBean("arithmeticCalculator");
        double sum = calculator.add(12, 23);
        System.out.println(sum);
        double div = calculator.div(1, 10);
        System.out.println(div);
    }
 
}

Необходимые библиотеки:

  • Spring.jar (2.5.6 или выше)
  • Обще-logging.jar
  • aopalliance.jar
  • aspectjrt.jar
  • aspectjweaver.jar
  • CGLIB-nodep-2.1_3.jar

Мы можем определить тип совета, используя аннотации @Before, @AfterReturning, @Around и т. Д. Мы можем определить pointcuts по-разному. Например:

@Around («выполнение (* *. * (..))») означает, что это совет Around, который будет применяться ко всем классам во всех пакетах и ​​ко всем методам.

Предположим, что мы хотим применить рекомендации только для всех служб, которые находятся в пакете com.myproj.services. Тогда объявление pointcut будет:

@Around («выполнение (* com.myproj.services. *. * (..))»)

В этом случае «(..)» означает с любым типом аргументов.

Если мы хотим применить одни и те же pointcut для многих советов, мы можем определить pointcut для метода и можем сослаться на это позже, как показано ниже.

1
2
3
4
5
@Pointcut("execution(* *.*(..))")
protected void loggingOperation() {}
 
@Before("loggingOperation()")
public void logJoinPoint(JoinPoint joinPoint){}

Если к одному и тому же нарезке нужно применить несколько советов, мы можем указать порядок, используя аннотацию @Order, к которой будут применены советы. В предыдущем примере @Before будет применен первым. Затем @Around будет применяться при вызове метода add ().

Вот и все, ребята. Очень простой и понятный учебник от Шивы , одного из наших партнеров по JCG .

Удачного кодирования АОП. Не забудьте поделиться!

Статьи по Теме: