Статьи

Простое введение в АОП

Зачем использовать AOP, простой способ ответить на этот вопрос — показать реализацию сквозной задачи без использования AOP. Рассмотрим простой сервис и его реализацию:

1
2
3
4
5
6
7
8
public interface InventoryService {
    public Inventory create(Inventory inventory);
    public List<inventory> list();
    public Inventory findByVin(String vin);
    public Inventory update(Inventory inventory);
    public boolean delete(Long id);
    public Inventory compositeUpdateService(String vin, String newMake);
}

и его реализация по умолчанию:

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
public class DefaultInventoryService implements InventoryService{
     
    @Override
    public Inventory create(Inventory inventory) {
        logger.info("Create Inventory called");
        inventory.setId(1L);
        return inventory;
    }
 
    @Override
    public List<inventory> list(){
        return new ArrayList<inventory>();
    }
 
    @Override
    public Inventory update(Inventory inventory) {
        return inventory;
    }
 
    @Override
    public boolean delete(Long id) {
        logger.info("Delete Inventory called");
        return true;
    }
....

Это всего лишь одна услуга. Предположим, что в этом проекте есть еще много услуг.

Так что теперь, если бы требовалось записать время, затрачиваемое каждым из сервисных методов, опция без AOP была бы такой: Создать декоратор для сервиса:

01
02
03
04
05
06
07
08
09
10
11
12
13
public class InventoryServiceDecorator implements InventoryService{
    private static Logger logger = LoggerFactory.getLogger(InventoryServiceDecorator.class);
    private InventoryService decorated;
 
    @Override
    public Inventory create(Inventory inventory) {
        logger.info("before method: create");
        long start = System.nanoTime();
        Inventory inventoryCreated = decorated.create(inventory);
        long end = System.nanoTime();
        logger.info(String.format("%s took %d ns", "create", (end-start)) );
        return inventoryCreated;
    }

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

Представьте себе, что вы делаете это для всех методов и услуг в проекте. Это тот сценарий, который рассматривается в AOP, он обеспечивает способ модульности сквозных задач (время записи, например, для вызовов сервисных методов), которые должны быть упакованы отдельно, не загрязняя ядро ​​классов.

Чтобы завершить сеанс, другой способ реализации декоратора будет использовать функцию динамического прокси Java:

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
public class AuditProxy implements java.lang.reflect.InvocationHandler {
     
    private static Logger logger = LoggerFactory.getLogger(AuditProxy.class);
    private Object obj;
 
    public static Object newInstance(Object obj) {
        return java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
                .getClass().getInterfaces(), new AuditProxy(obj));
    }
 
    private AuditProxy(Object obj) {
        this.obj = obj;
    }
 
    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
        Object result;
        try {
            logger.info("before method " + m.getName());
            long start = System.nanoTime();
            result = m.invoke(obj, args);
            long end = System.nanoTime();
            logger.info(String.format("%s took %d ns", m.getName(), (end-start)) );
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (Exception e) {
            throw new RuntimeException("unexpected invocation exception: " + e.getMessage());
        } finally {
            logger.info("after method " + m.getName());
        }
        return result;
    }
}

Итак, теперь при создании экземпляра InventoryService я бы создал его через динамический прокси-сервер AuditProxy:

1
InventoryService inventoryService = (InventoryService)AuditProxy.newInstance(new DefaultInventoryService());

переопределенный метод вызова java.lang.reflect.InvocationHandler будет перехватывать все вызовы InventoryService, созданные таким образом, где записывается сквозная задача аудита времени вызова метода. Таким образом, сквозная проблема модульная в одном месте (AuditProxy), но все же должна быть явно известна клиентам InventoryService при создании экземпляра InventoryService.

Теперь я покажу, как можно реализовать сквозную задачу с помощью Spring AOP — Spring предлагает несколько способов реализации аспектов — на основе конфигурации XML, на основе @AspectJ. В этом конкретном примере я буду использовать способ определения аспекта на основе файла конфигурации XML

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

1
2
3
4
@Service
public class DefaultInventoryService implements InventoryService{
...
}

Теперь я хочу записать время, затрачиваемое на каждый из вызовов метода моего DefaultInventoryService — сначала я собираюсь описать это как «совет»:

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
package org.bk.inventory.aspect;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class AuditAdvice {
 
    private static Logger logger = LoggerFactory.getLogger(AuditAdvice.class);
 
    public void beforeMethod() {
        logger.info("before method");
    }
 
    public void afterMethod() {
        logger.info("after method");
    }
 
    public Object aroundMethod(ProceedingJoinPoint joinpoint) {
        try {
            long start = System.nanoTime();
            Object result = joinpoint.proceed();
            long end = System.nanoTime();
            logger.info(String.format("%s took %d ns", joinpoint.getSignature(), (end - start)));
            return result;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
         
}

Ожидается, что этот совет будет фиксировать время, затрачиваемое методами в DefaultInventoryService. Так что теперь, чтобы связать этот совет с bean-компонентом DefaultInventoryService:

01
02
03
04
05
06
07
08
09
10
11
<bean id="auditAspect" class="org.bk.inventory.aspect.AuditAdvice" />
 
<aop:config>
 <aop:aspect ref="auditAspect">
  <aop:pointcut id="serviceMethods" expression="execution(* org.bk.inventory.service.*.*(..))" />
 
  <aop:before pointcut-ref="serviceMethods" method="beforeMethod" /> 
  <aop:around pointcut-ref="serviceMethods" method="aroundMethod" />
  <aop:after-returning pointcut-ref="serviceMethods" method="afterMethod" />
 </aop:aspect>
</aop:config>

Это работает, сначала определяя «pointcut» — места (в этом примере, методы обслуживания), к которым нужно добавить сквозную задачу (фиксируя время выполнения метода в этом примере). Здесь я определил это с помощью выражения pointcut —

1
execution(* org.bk.inventory.service.*.*(..))

, который по сути выбирает все методы всех типов в пакете org.bk.inventory.service. После того, как pointcut определен, он определяет, что нужно сделать вокруг pointcut (рекомендации), используя выражение:

1
<aop:around pointcut-ref="serviceMethods" method="aroundMethod" />

По сути, это говорит о том, что вокруг каждого метода любого типа сервиса выполняется roundMethod AspectAdvice, который был определен ранее. Теперь, если выполняются сервисные методы, я вижу, как во время выполнения метода вызывается совет, ниже приведен пример вывода метода DefaultInventoryService, метод createInventory:

1
2
org.bk.inventory.service.InventoryService - Create Inventory called
org.bk.inventory.aspect.AuditAdvice - Inventory org.bk.inventory.service.InventoryService.create(Inventory) took 82492 ns

Реализация AOP в Spring работает путем генерации динамического прокси во время выполнения для всех целевых bean-компонентов на основе определенного pointcut.

Другой способ определения Aspect — использование аннотаций @AspectJ, что изначально понимается Spring:

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
package org.bk.inventory.aspect;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
@Aspect
public class AuditAspect {
 
    private static Logger logger = LoggerFactory.getLogger(AuditAspect.class);
 
    @Pointcut("execution(* org.bk.inventory.service.*.*(..))")
    public void serviceMethods(){
        //
    }
 
     
    @Before("serviceMethods()")
    public void beforeMethod() {
        logger.info("before method");
    }
 
    @Around("serviceMethods()")
    public Object aroundMethod(ProceedingJoinPoint joinpoint) {
        try {
            long start = System.nanoTime();
            Object result = joinpoint.proceed();
            long end = System.nanoTime();
            logger.info(String.format("%s took %d ns", joinpoint.getSignature(), (end - start)));
            return result;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
       
    @After("serviceMethods()")
    public void afterMethod() {
        logger.info("after method");
    }   
}

Аннотация @Aspect к классу идентифицирует его как определение аспекта. Это начинается с определения точек:

1
2
@Pointcut("execution(* org.bk.inventory.service.*.*(..))")
    public void serviceMethods(){}

Вышесказанное в основном идентифицирует все методы всех типов в пакете org.bk.inventory.service, этот pointcut идентифицируется по имени метода, на котором размещена аннотация — в данном случае «serviceMethods». Далее, совет определяется с помощью аннотаций @Before (serviceMethods ()), @After (serviceMethods ()) и @Around (serviceMethods ()), а специфика того, что должно произойти, — это тело методов с этими аннотациями. Spring AOP изначально понимает аннотации @AspectJ, если этот аспект определен как bean-компонент:

1
<bean id="auditAspect" class="org.bk.inventory.aspect.AuditAspect" />

Spring создаст динамический прокси-сервер для применения рекомендаций ко всем целевым компонентам, указанным как часть нотации pointcut.

Еще один способ определить аспект — на этот раз, используя нативную нотацию aspectj.

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
package org.bk.inventory.aspect;
 
import org.bk.inventory.types.Inventory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public aspect AuditAspect {
    private static Logger logger = LoggerFactory.getLogger(AuditAspect.class);
 
    pointcut serviceMethods() : execution(* org.bk.inventory.service.*.*(..));
 
    pointcut serviceMethodsWithInventoryAsParam(Inventory inventory) : execution(* org.bk.inventory.service.*.*(Inventory)) && args(inventory);
 
    before() : serviceMethods() {
        logger.info("before method");
    }
 
    Object around() : serviceMethods() {
        long start = System.nanoTime();
        Object result = proceed();
        long end = System.nanoTime();
        logger.info(String.format("%s took %d ns", thisJoinPointStaticPart.getSignature(),
                (end - start)));
        return result;
    }
 
    Object around(Inventory inventory) : serviceMethodsWithInventoryAsParam(inventory) {
        Object result = proceed(inventory);
        logger.info(String.format("WITH PARAM: %s", inventory.toString()));
        return result;
    }
    after() : serviceMethods() {
        logger.info("after method");
    }
}

Это соответствует ранее определенной нотации @AspectJ

Так как это DSL специально для определения Аспектов, он не понят компилятором java. AspectJ предоставляет инструмент (ajc) для компиляции этих собственных файлов aspectj и для объединения аспектов в целевые pointcuts. Maven предоставляет плагин, который без проблем вызывает ajc на этапе компиляции:

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
<plugin>
 <groupId>org.codehaus.mojo</groupId>
 <artifactId>aspectj-maven-plugin</artifactId>
 <version>1.0</version>
 <dependencies>
  <dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjrt</artifactId>
   <version>${aspectj.version}</version>
  </dependency>
  <dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjtools</artifactId>
   <version>${aspectj.version}</version>
  </dependency>
 </dependencies>
 <executions>
  <execution>
   <goals>
    <goal>compile</goal>
    <goal>test-compile</goal>
   </goals>
  </execution>
 </executions>
 <configuration>
  <outxml>true</outxml>
  <aspectLibraries>
   <aspectLibrary>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
   </aspectLibrary>
  </aspectLibraries>
  <source>1.6</source>
  <target>1.6</target>
 </configuration>
</plugin>

Это будет подведение итогов AOP, с примером, который будет всесторонне использовать концепции, представленные на предыдущих сессиях.

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

01
02
03
04
05
06
07
08
09
10
11
package org.bk.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PerfLog {
     
}

Теперь, чтобы аннотировать некоторые сервисные методы с помощью этой аннотации:

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
@Service
public class DefaultInventoryService implements InventoryService{
     
    private static Logger logger = LoggerFactory.getLogger(InventoryService.class);
 
     
    @Override
    public Inventory create(Inventory inventory) {
        logger.info("Create Inventory called");
        inventory.setId(1L);
        return inventory;
    }
 
    @Override
    public List<Inventory> list() {
        return new ArrayList<Inventory>();
    }
 
    @Override
    @PerfLog
    public Inventory update(Inventory inventory) {
        return inventory;
    }
 
    @Override
    public boolean delete(Long id) {
        logger.info("Delete Inventory called");
        return true;
    }
 
    @Override
    @PerfLog
    public Inventory findByVin(String vin) {
        logger.info("find by vin called");
        return new Inventory("testmake", "testmodel","testtrim","testvin" );
    }
 
    @Override
    @PerfLog
    public Inventory compositeUpdateService(String vin, String newMake) {
        logger.info("composite Update Service called");
        Inventory inventory = findByVin(vin);
        inventory.setMake(newMake);
        update(inventory);
        return inventory;
    }
}

Здесь три метода DefaultInventoryService были аннотированы аннотацией @PerfLog — update, findByVin, CompositUpdateService, которая внутренне вызывает методы findByVin и update.

Теперь для аспекта, который будет перехватывать все вызовы методов, аннотированных @PerfLog, и записывать время, затраченное на вызов метода:

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
package org.bk.inventory.aspect;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
@Aspect
public class AuditAspect {
 
    private static Logger logger = LoggerFactory.getLogger(AuditAspect.class);
 
    @Pointcut("execution(@org.bk.annotations.PerfLog * *.*(..))")
    public void performanceTargets(){}
    
 
    @Around("performanceTargets()")
    public Object logPerformanceStats(ProceedingJoinPoint joinpoint) {
        try {
            long start = System.nanoTime();
            Object result = joinpoint.proceed();
            long end = System.nanoTime();
            logger.info(String.format("%s took %d ns", joinpoint.getSignature(), (end - start)));
            return result;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

Здесь выражение pointcut —

1
@Pointcut("execution(@org.bk.annotations.PerfLog * *.*(..))")

выбирает все методы, аннотированные аннотацией @PerfLog, а метод аспекта logPerformanceStats регистрирует время, затраченное на вызовы метода.

Чтобы проверить это:

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
package org.bk.inventory;
 
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
 
import org.bk.inventory.service.InventoryService;
import org.bk.inventory.types.Inventory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:/testApplicationContextAOP.xml")
public class AuditAspectTest {
 
    @Autowired
    InventoryService inventoryService;
         
    @Test
    public void testInventoryService() {
        Inventory inventory = this.inventoryService.create(new Inventory("testmake", "testmodel","testtrim","testvin" ));
        assertThat(inventory.getId(), is(1L));
         
        assertThat(this.inventoryService.delete(1L), is(true));
        assertThat(this.inventoryService.compositeUpdateService("vin","newmake").getMake(),is("newmake"));
    }
 
}

Когда этот тест вызывается, вывод будет следующим:

1
2
3
4
5
6
7
2011-09-08 20:54:03,521 org.bk.inventory.service.InventoryService - Create Inventory called
2011-09-08 20:54:03,536 org.bk.inventory.service.InventoryService - Delete Inventory called
2011-09-08 20:54:03,536 org.bk.inventory.service.InventoryService - composite Update Service called
2011-09-08 20:54:03,536 org.bk.inventory.service.InventoryService - find by vin called
2011-09-08 20:54:03,536 org.bk.inventory.aspect.AuditAspect - Inventory org.bk.inventory.service.DefaultInventoryService.findByVin(String) took 64893 ns
2011-09-08 20:54:03,536 org.bk.inventory.aspect.AuditAspect - Inventory org.bk.inventory.service.DefaultInventoryService.update(Inventory) took 1833 ns
2011-09-08 20:54:03,536 org.bk.inventory.aspect.AuditAspect - Inventory org.bk.inventory.service.DefaultInventoryService.compositeUpdateService(String, String) took 1371171 ns

Совет правильно вызывается для findByVin, update и композитный сервис обновления.

Этот образец доступен по адресу: git: //github.com/bijukunjummen/AOP-Samples.git

Ссылка: простое введение в АОП от нашего партнера по JCG Биджу Кунджуммена в блоге all and sundry.